1
\$\begingroup\$

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"
\$\endgroup\$
3
  • \$\begingroup\$ 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. \$\endgroup\$ – joanis Mar 13 at 15:43
  • 1
    \$\begingroup\$ 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. \$\endgroup\$ – Toby Speight Mar 13 at 15:56
  • \$\begingroup\$ 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. \$\endgroup\$ – Toby Speight Mar 13 at 15:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.