#!/bin/sh -e [ -n "$1" ] || { \ echo " usage: $(basename $0) [-c] [-f] [-s] [-t|-u] [--] 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 and . -u: try to undo a previous lmr from to . --: 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