# Swap a symbolic link with its target

This program takes a symbolic link as argument, and swaps the link with its referent. For example, given a -> b, the result will be b -> a. It creates a relative link if the original was relative, otherwise an absolute link.

I wanted to ensure that this is an all-or-nothing command, so if it fails, it leaves everything as it was. This isn't completely achievable in the face of concurrent modification, but it does make a best-effort attempt to undo changes if it was unsuccessful. I also use rm with --no-clobber and ln without --force to avoid data loss regardless.

#!/bin/bash

set -eu -o pipefail

die()
{
printf '%q: ' "$1"; shift printf '%s\n' "$@"
exit 1
}

usage()
{
cat <<END
Usage: $0 FILE FILE a symlink The symlink will be swapped with its target END } undo="" add_undo() { # prepend a command to the undo list local command printf -v command '%q ' "$@"
undo="$command"$'\n'"$undo" } restore() { # execute the undo commands eval "$undo" ||
die "$0" "Failed to restore to initial state!" } trap restore ERR; if [$# -ne 1 ]
then
usage >&2
exit 1
fi
case "$1" in -h|--help) usage; exit ;; esac test -L "$1" || die "$1" 'not a symlink' test -e "$1" || die "$1" 'dangling symlink' target=$(readlink -e "$1") lnopts=(--symbolic) case "$(readlink "$1")" in /*) # make$1 absolute, without following the link itself
set -- "$(realpath --no-symlinks "$1")"
;;
*)
lnopts+=(--relative)
;;
esac

# Create temporary working directory alongside the link
linkdir="$(mktemp --directory --tmpdir="$(dirname "$1")")" test -d "$linkdir"
add_undo rm -r "$linkdir" # Move the symlink into tempdir mv -T "$1" "$linkdir/link" add_undo mv -T "$linkdir/link" "$1" # Create a new symlink next to the target newlink="$(mktemp --dry-run --tmpdir="$(dirname "$target")")"
test -n "$newlink" ln "${lnopts[@]}" "$1" "$newlink"
add_undo rm "$newlink" # Move the target to its new location # This is the riskiest and most expensive thing to restore, so do it last mv -T --no-clobber "$target" "$1" add_undo mv "$1" "$target" # Move the new link into position left by target mv -T --no-clobber "$newlink" "$target" # Succeeded, so remove the temporary directory undo="" rm -r "$linkdir"

• Does this code work if your current working directory is not where the link is and the link is provided with a relative or absolute path? Does it work if the symlink itself points to an absolute or relative path? Reading the code, I get the impression it expects a and b both to be in the current directory, but I'm not confident in that impression. – joanis Mar 13 at 15:43
• It's supposed to work wherever the link and target are. I did my testing by hand, before I posted, so exactly what I tested is somewhat vague, but I'm reasonably certain I exercised the cases you mention. It really shouldn't matter where your working directory is, and quite a lot of the anticipated error conditions only occur when link and target are on different filesystems. – Toby Speight Mar 13 at 15:56
• It's definitely tested for the link being absolute and relative - there's logic there to ensure the replacement link is of the same kind. I think it works if the target itself is a symlink, though I probably forgot to test that. – Toby Speight Mar 13 at 15:57