116 lines
3.7 KiB
Bash
Executable File
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
|