From 72cbc59f70288b72300fc4f2743c45e68f30054e Mon Sep 17 00:00:00 2001 From: yafox Date: Wed, 25 Nov 2020 06:32:47 +0000 Subject: [PATCH] initial commit. --- LICENSE | 20 ++++++++++ README | 26 +++++++++++++ lmr.sh | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ makefile | 9 +++++ sloc.sh | 14 +++++++ 5 files changed, 184 insertions(+) create mode 100644 LICENSE create mode 100644 README create mode 100755 lmr.sh create mode 100644 makefile create mode 100755 sloc.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd8c239 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright 2020 "yafox" + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README b/README new file mode 100644 index 0000000..43ce6c1 --- /dev/null +++ b/README @@ -0,0 +1,26 @@ +lmr +==== + +pronounced "lemur." an abbreviation of "link merge." + +merges the contents of one directory into another using filesystem links. + +can reverse a merge as long as the contents of the source directory have +not changed since the merge was completed and no files were overwritten +during the merge. if files were removed from the source directory, "dangling" +symlinks will be left behind. + +lmr exists for much the same reason as gnu stow, but has significantly fewer +features and operates more like a recursive `ln` than a simplified `stow`. + +lmr is implemented as a single, 80 SLOC long, POSIX-compliant shell script. + +usage: lmr [-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. diff --git a/lmr.sh b/lmr.sh new file mode 100755 index 0000000..239ba52 --- /dev/null +++ b/lmr.sh @@ -0,0 +1,115 @@ +#!/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 diff --git a/makefile b/makefile new file mode 100644 index 0000000..0c64a12 --- /dev/null +++ b/makefile @@ -0,0 +1,9 @@ +SRCDIR = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +PREFIX ?= /usr +DESTDIR ?= $(PREFIX)/bin + +install: + cp -a $(SRCDIR)/lmr.sh $(DESTDIR)/lmr + +uninstall: + rm $(DESTDIR)/lmr diff --git a/sloc.sh b/sloc.sh new file mode 100755 index 0000000..c131740 --- /dev/null +++ b/sloc.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# SLOC is here defined as lines which are not comments or empty. + +# find all *.sh files not under `pkg` that are not symbolic links, strip all +# trailing whitespace, then all leading whitespace, then all lines starting +# with '#', then all empty lines. then count the remaining lines. + +find . -name "*.sh" ! -path "**/pkg/**" ! -type l \ +| xargs sed 's/[[:space:]]*$//g; s/^[[:space:]]*//g; s/^#.*$//g; /^$/d' \ +| wc -l - \ +| cut -d' ' -f1 + +# note that this script's SLOC is also included in the count.