lmr/lmr.sh

116 lines
3.7 KiB
Bash
Executable File

#!/bin/sh -e
[ -n "$1" ] || { \
echo "
usage: $(basename $0) [-c] [-f] [-s] [-t|-u] [--] <src> <dest>
options:
-c: check for file conflicts. link nothing.
-f: force link creation, even if it overwrites files.
-s: use symlinks instead of hard links.
-t: exit if it seems a lmr has already been done between <src> and <dest>.
-u: try to undo a previous lmr from <src> to <dest>.
--: optional "end of flags" marker.
" && exit 0; }
# if this is running on an interactive terminal (as opposed to in a script), if
# tput is installed, and if tput knows some color codes, then set color values.
if [ -t 1 ] && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then
_clr="$(tput sgr0)"
_blu="$(tput setaf 6)"
_ylw="$(tput setaf 3)"
_red="$(tput setaf 1)"
fi
log() { echo "$_blu[LOG]$_clr $@"; }
wrn() { echo "$_ylw[WRN]$_clr $@" >&2; }
err() { echo "$_red[ERR]$_clr $@" >&2; exit 1; }
# -e checks if a file exists, but also dereferences symlinks and returns false
# if the link's target does not exist. -L returns true if the file exists and
# is a symlink; it does not dereference symlinks.
exists() { [ -e "$1" ] || [ -L "$1" ]; }
linked() {
# -ef returns true if the files exist and refer to the same file.
{ [ -z "$soft" ] && [ "$1" -ef "$2" ]; } \
|| [ "$1" = "$(readlink "$2" 2> /dev/null)" ] \
|| return 1
}
export linked
while [ "$1" != "${1#-}" ]; do
case "$1" in
-c) check="yes";;
-f) flags="$flags -f";;
-s) soft="yes";;
-t) test="yes";;
-u) undo="yes";;
--) break;;
*) err "unrecognized option '$1'";;
esac
shift
done
src="$1"
dest="$2"
[ -d "$1" ] || err "not a directory: $1"
[ -d "$2" ] || err "not a directory: $2"
if [ -z "$undo" ]; then
[ -z "$soft" ] && flags="$flags -P" || flags="$flags -s"
find "$src" -type d ! -path "$src" -exec sh -c \
"mkdir -p \$(echo \"$dest/\${0#$src}\" | tr -s '/')" {} \;
find "$src" \( -type f -o -type l \) | while read lnsrc; do
lndest="$dest/${lnsrc#$src}"
if [ -z "$test" ]; then
linked "$lnsrc" "$lndest" \
&& exit 0 \
|| exit 1
elif [ -z "$check" ]; then
{ ! exists "$lndest"; } \
|| linked "$lnsrc" "$lndest" \
|| wrn "conflict: ${lnsrc#$src}"
else
ln$flags "$lnsrc" "$lndest"
fi
done
else
# delete all files in the destination directory which are either soft or
# hard links (depending on whether the -s flag was set) to files in the
# source directory.
find "$src" \( -type f -o -type l \) | while read lnsrc; do
lndest="$dest/${lnsrc#$src}"
exists "$lndest" || continue;
# only remove the file in the "destination" directory if it is a hard or
# a soft link to the source file.
{ ! linked "$lnsrc" "$lndest"; } || rm "$lndest"
done
# recursively remove any empty directories in the destination directory
# which also exist in the source directory. while no guarantee that these
# were created by a previous lmr operation, this seems a decent heuristic.
# reverse-sorting by path length to ensure "leaves" are processed first.
find "$src" -type d ! -path "$src" \
| awk '{ print length() "\t" $0 | "sort -nr" }' \
| cut -f2 \
| while read dir; do
destdir="$dest/${dir#$src}"
# avoid "directory not empty" errors below.
[ -d "$destdir" ] && [ -z "$(ls -A $destdir/)" ] || continue;
# rmdir will only delete empty directories.
# use it in case the line above fails to detect something.
rmdir "$destdir" || true # continue even if this fails for some reason.
done
fi