Skip to content

Instantly share code, notes, and snippets.

@sampersand
Created November 4, 2024 19:38
Show Gist options
  • Save sampersand/e7415bd9c8b2dab02885b0bd3a3ffe12 to your computer and use it in GitHub Desktop.
Save sampersand/e7415bd9c8b2dab02885b0bd3a3ffe12 to your computer and use it in GitHub Desktop.
#!/bin/sh
# Safety first
set -o nounset
scriptname="$(basename -- "$0"; echo x)"; scriptname="${scriptname%?x}"
warn () {
msg="%s: $1"'\n'
shift
printf "$msg" "$scriptname" "$@" >&2
}
shortusage () { cat; } <<EOS >&2
usage: ${scriptname} [-h/--help] ...
${scriptname} [options] [--] source target
${scriptname} [options] [--] source ... directory
EOS
longusage () { shortusage; cat <<EOS; } >&2
options:
-h, --help print help, then exit.
-i, --interactive ask to overwrite files that exist (disables -n)
-n, --not-interactive do not overwrite not overwrite existing files at all (disables -i); default
-r, --rename rename files to prevent conflicts mode
-R, --no-rename disables '-r'
-c, --clobber-empty overwrite empty files/folders
-C, --no-clobber-empty disables -'c'
returns: (for non-zero exit status, it's the last error status encountered.)
0 If everything was successful
2 There's a problem running 'mv' (or 'rm' when --clobber-empty is enabled)
3 If 'directory' is not actually a directory ((, or is not executable))
4 A 'source' does not exist
5 The 'source' and the destination are identical
253 not enough args were given
254 If an unknown argument was given
255 When -h or --help supplied provided
Without any flags, this acts the same as 'mv -n' (or 'mv -i' if --interactive).
If the '--clobber-empty' flag is given, then attempting to overwrite empty
files/folders will succeed. (Note that this is susceptible to race conditions)
If the '--rename' flag is given, then files will first be moved via 'mv -n',
but if the target file exists, the the first 'num' where the path
'<dir>/<base> <num><ext>' does not exist will be used instead.
EOS
do_rename=
clobber_empty=
move_mode=-n
# Parse command line options
while [ "$#" -ne 0 ]; do
case "$1" in
--) shift; break ;;
-h) shortusage; exit 255 ;;
--help) longusage; exit 255 ;;
-r | --rename) do_rename=1 ;;
-R | --no-rename) do_rename= ;;
-c | --clobber-empty) clobber_empty=1 ;;
-C | --no-clobber-empty) clobber_empty= ;;
-n | --not-interactive) move_mode=-n ;;
-i | --interactive) move_mode=-i ;;
-??*) # Support `-abc` options
rest2="${1#-?}"
rest1="${1%"$rest2"}"
shift
set -- "${rest1}" "-${rest2}" "$@"
continue ;;
-?) warn 'unknown option: %s' "$1"; exit 254 ;;
*) break ;;
esac
shift
done
# If no arguments are given, then error out with the usage.
if [ "$#" -le 1 ]; then
shortusage
exit 253
fi
try_move () {
# Resolve the paths to absolute values.
source="$1"
target="$2"
# If the thing we're trying to move does not exist, then print out an error
# and go to the next one
if [ ! -e "${source}" ]; then
warn 'cannot move %q: No such file or directory' "${source}"
return 4
fi
# If the paths are identical, then just don't move anything.
if [ "${source}" -ef "${target}" ]; then
warn 'cannot move %q to %q: Paths are identical' "${source}" "${target}"
return 5
fi
idx=1
while
# If clobber empty is defined, then delete the target if it doesn't exist
# before we attempt to move our file.
if [ -n "${clobber_empty}" ]; then
# If the target is a file and it's empty, then forcibly remove it.
if [ -f "${target}" ] && [ ! -s "${target}" ]; then
command -p rm -f -- "${target}" || return 2
# If the target is a folder, and there's nothing in it, then delete it.
elif [ -d "${target}" ] && ! ls -A1q -- "${target}" | grep -q .; then
command -p rm -fd -- "${target}" || return 2
fi
fi
# Try to rename '$source' to '$target', making sure not to override
# '$target' if it exists.
command -p mv "${move_mode}" -- "${source}" "${target}" || return 2
# Now, after moving it, does the source still exist? ...
[ -e "${source}" ]
do
# ... It does still exist!
# If we're not renaming files, and the original file still exists, then
# return an error.
[ -z "${do_rename}" ] && return 1
# If we have an index of 1, that means this is our first time in
# the body. Setup all the required variables.
if [ "$idx" = 1 ]; then
base="$(basename -- "$2"; echo x)"; base="${base%?x}"
stem="${base%%.*}"
ext="${base#"$stem"}"
dir="$(dirname -- "$2"; echo x)"; dir="${dir%?x}"
root="$dir/$stem"
fi
# Now set the target and let's try again
target="$root $((idx=idx+1))$ext"
done
return 0
}
# If two arguments are given, and the second isn't a directory, assume the first
# command form
if [ "$#" -eq 2 ] && [ ! -d "$2" ]; then
try_move "$1" "$2"
exit
fi
# The target folder is the last argument; make sure it exists.
target_folder="$(eval "echo \"\${$#}\"" && echo x)"
target_folder="${target_folder%?x}"
last_status=0
while [ "$#" -gt 1 ]; do # Stop at 1, because the last argument is the folder.
base="$(basename -- "$1"; echo x)"; base="${base%?x}"
try_move "$1" "$target_folder/$base" || last_status=$?
shift
done
# Return the most recent failure
exit "${last_status}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment