Skip to content

Instantly share code, notes, and snippets.

@cmcaine
Last active October 13, 2016 00:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cmcaine/1b5fab9e9e958877166742f42d5b831d to your computer and use it in GitHub Desktop.
Save cmcaine/1b5fab9e9e958877166742f42d5b831d to your computer and use it in GitHub Desktop.
Swap two files reasonably safely
#!/bin/sh
# Swap two files safely.
#
# Under normal circumstances, just do the right thing. Under unusual
# circumstances, give a reasonably helpful error message.
#
# This was an exercise in thinking about how generally reliable functions might
# fail and what to communicate to users.
#
# Copyright Colin Caine. License: GPLv2
USAGE="Usage: `basename $0` file1 file2
Basically the same as:
mv file1 \$tmpfile
mv file2 file1
mv \$tmpfile file1
But does something sensible if errors occur.
"
use_error () {
[ -n "$1" ] && echo -e "$1\n" 2>&1
echo "$USAGE"
exit 1
}
descriptive_mv () {
# $1 and $2 are the paths to two files in the same directory.
# As of a moment ago, both files existed and the directory was writable by
# our user (we could make the $tmpfile).
if ! mv "$1" "$2"; then
# Something bananas has happened. mv won't necessarily say something
# sensible, so let's be as helpful as we can.
echo "Error: mv '$1' '$2' failed!
This is quite unusual.
Some possible reasons:
1) $USER's write access to dir `dirname $1` or execute permission on
parent directories has been changed during the runtime of this
script.
--> Data should be in '$1'
2) '$1' has been moved or deleted by something else
3) You're using a filesystem or kernel with odd properties (FUSE?)
--> Your data may be in '$1' or '$2'" >&2
exit 100
fi
}
file1="$1"
file2="$2"
if [ ! $# = 2 ]; then
use_error
elif [ ! -e "$file1" ]; then
use_error "Error: '$file1' does not exist!"
elif [ ! -e "$file2" ]; then
use_error "Error: '$file2' does not exist!"
else
if ! tmpfile=$(mktemp $(dirname "$file1")/"$file1".tmp.XXXXXXXXXXX); then
# The error message from mktemp or whatever should be enough.
echo "Error: Could not create temporary file!" >&2
exit 2
fi
descriptive_mv "$file1" "$tmpfile"
if ! mv "$file2" "$file1"; then
if [ ! -e "$file1" ]; then
# $USER probably doesn't have permission to delete or rename $file2.
# $file2 will be unmodified, so just put $file1 back where it was to
# rollback the swap.
descriptive_mv "$tmpfile" "$file1"
echo "Error: mv '$file2' '$file1' failed.
Swap rolled back: files unchanged." >&2
exit 3
else
# If $file1 exists, `mv $file2 $file1` failed in a highly unusual way, or
# $file1 was created again after we moved it the first time.
echo "Error: mv '$file2' '$file1' failed.
Tried to move '$tmpfile' back to '$file1', but '$1' already exists!
Possible reasons:
1) A new '$file1' was created after this script renamed the original
--> original '$file1' is in '$tmpfile'
'$file2' is where it started
2) mv failed in a highly unusual way (weird filesystem?)
--> original '$file1' is in '$tmpfile'
original '$file2' might be in '$file1' or '$file2'" >&2
exit 5
fi
fi
if ! mv "$tmpfile" "$file2"; then
# Maybe $file1 and $file2 were on different devices and $file1 (now
# $tmpfile) won't fit onto $file2's device. Maybe something else exciting.
echo "Error: Couldn't complete final move.
Succeeded: 'mv '$file1' '$tmpfile;' mv '$file2' '$file1;''
Failed: 'mv '$tmpfile' '$file2''
Content of the file originally called '$file1' is probably in '$tmpfile'.
In extraordinary circumstances, the content may be in '$file2'." >&2
exit 4
fi
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment