diff --git a/Makefile.am b/Makefile.am index a56eeda8..aa25b647 100644 --- a/Makefile.am +++ b/Makefile.am @@ -49,6 +49,7 @@ TESTS = \ test/rcdn-hooks.t \ test/rcdn-hooks-run-in-situ.t \ test/rcdn-hooks-run-in-order.t \ + test/rcdn-dangling-symlinks.t \ test/rcup-hooks.t \ test/rcup-hooks-run-in-situ.t \ test/rcup-hooks-run-in-order.t \ diff --git a/bin/rcdn.in b/bin/rcdn.in index d69a175a..94a5856a 100755 --- a/bin/rcdn.in +++ b/bin/rcdn.in @@ -20,10 +20,116 @@ remove_link() { fi } +is_selected_file() { + local dest="$1" + local selected_file + local rc_file="$(de_dot "$dest")" + + if [ -z "$FILES" ]; then + return 0 + fi + + for selected_file in $FILES; do + if [ "x$selected_file" = "x$rc_file" ]; then + return 0 + fi + done + + return 1 +} + +is_managed_target() { + local target="$1" + local dotfiles_dir + + case "$target" in + /*) ;; + *) return 1 ;; + esac + + for dotfiles_dir in $RESOLVED_DOTFILES_DIRS; do + if [ "x$target" = "x$dotfiles_dir" ]; then + return 0 + fi + + case "$target" in + "$dotfiles_dir"/*) return 0 ;; + esac + done + + return 1 +} + +is_dotfiles_path() { + local file="$1" + local dotfiles_dir + + for dotfiles_dir in $RESOLVED_DOTFILES_DIRS; do + if [ "x$file" = "x$dotfiles_dir" ]; then + return 0 + fi + + case "$file" in + "$dotfiles_dir"/*) return 0 ;; + esac + done + + return 1 +} + +resolve_dotfiles_dirs() { + local relative_root_dir="$PWD" + local dotfiles_dir + local selected_dirs="${SELECTED_DOTFILES_DIRS:-$DOTFILES_DIRS}" + + for dotfiles_dir in $selected_dirs; do + cd -- "$relative_root_dir" + dotfiles_dir="$(eval echo "$dotfiles_dir")" + + if is_relative "$dotfiles_dir"; then + dotfiles_dir="$PWD/$dotfiles_dir" + fi + + RESOLVED_DOTFILES_DIRS="$(append_variable "$RESOLVED_DOTFILES_DIRS" "$dotfiles_dir")" + done + + cd -- "$relative_root_dir" +} + +remove_dangling_links() { + local dangling_link + local target + local saved_ifs="$IFS" + + resolve_dotfiles_dirs + $DEBUG "resolved dotfiles dirs: $RESOLVED_DOTFILES_DIRS" + + IFS=' +' + for dangling_link in $(find "$DEST_DIR" -type l ! -exec test -e {} \; -print); do + target="$(readlink "$dangling_link")" + + if is_dotfiles_path "$dangling_link"; then + continue + fi + + if ! is_managed_target "$target"; then + continue + fi + + if ! is_selected_file "$dangling_link"; then + continue + fi + + remove_link "$dangling_link" "$dangling_link" + done + IFS="$saved_ifs" +} + show_help() { local exit_code=${1:-0} - $PRINT "Usage: rcdn [-hqVv] [-B HOSTNAME] [-d DOT_DIR] [-I EXCL_PAT] [-S EXCL_PAT] [-s EXCL_PAT] [-t TAG] [-U EXCL_PAT] [-u EXCL_PAT] [-x EXCL_PAT]" + $PRINT "Usage: rcdn [-DhqVv] [-B HOSTNAME] [-d DOT_DIR] [-I EXCL_PAT] [-S EXCL_PAT] [-s EXCL_PAT] [-t TAG] [-U EXCL_PAT] [-u EXCL_PAT] [-x EXCL_PAT]" $PRINT "see rcdn(1) and rcm(7) for more details" exit $exit_code @@ -43,9 +149,11 @@ handle_command_line() { local undotted= local never_undotted= local hostname= + local remove_dangling=0 - while getopts :VqvhIKk:x:S:s:U:u:t:d:B: opt; do + while getopts :DVqvhIKk:x:S:s:U:u:t:d:B: opt; do case "$opt" in + D) remove_dangling=1 ;; h) show_help ;; B) hostname="$OPTARG" ;; I) includes="$(append_variable "$includes" "$OPTARG")" ;; @@ -73,6 +181,9 @@ handle_command_line() { dotfiles_dirs="${dotfiles_dirs:-$DOTFILES_DIRS}" files="$@" RUN_HOOKS="$run_hooks" + FILES="$files" + REMOVE_DANGLING=$remove_dangling + SELECTED_DOTFILES_DIRS="$dotfiles_dirs" for tag in "$tags"; do LS_ARGS="$LS_ARGS -t \"$tag\"" @@ -105,9 +216,13 @@ handle_command_line() { } LS_ARGS=-F +RESOLVED_DOTFILES_DIRS= handle_command_line "$@" : ${DOTFILES_DIRS:=$DOTFILES_DIRS $DEFAULT_DOTFILES_DIR} +if [ -z "$SELECTED_DOTFILES_DIRS" ]; then + SELECTED_DOTFILES_DIRS="$DOTFILES_DIRS" +fi run_hooks pre down @@ -127,4 +242,7 @@ for dest_and_src in $dests_and_srcs; do done IFS="$saved_ifs" +if [ $REMOVE_DANGLING -eq 1 ]; then + remove_dangling_links +fi run_hooks post down diff --git a/man/rcdn.1 b/man/rcdn.1 index df83e60e..5be4922c 100644 --- a/man/rcdn.1 +++ b/man/rcdn.1 @@ -6,7 +6,7 @@ .Nd remove dotfiles as managed by rcm .Sh SYNOPSIS .Nm rcdn -.Op Fl hKkqVv +.Op Fl DhKkqVv .Op Fl B Ar hostname .Op Fl d Ar dir .Op Fl I Ar excl_pat @@ -65,6 +65,14 @@ as the host-specific directory instead of computing it remove rc files from the .Ar DIR . This can be specified multiple times. +.It Fl D +remove dangling symlinks that are managed by +.Xr rcm 7 . +Managed dangling symlinks are symlinks in +the destination directory +whose targets are inside the selected dotfiles directories. When +.Ar files +are provided, only dangling symlinks matching those files are removed. .It Fl h show usage instructions. .It Fl I Ar EXCL_PAT diff --git a/test/rcdn-dangling-symlinks.t b/test/rcdn-dangling-symlinks.t new file mode 100644 index 00000000..3c50ec14 --- /dev/null +++ b/test/rcdn-dangling-symlinks.t @@ -0,0 +1,37 @@ + $ . "$TESTDIR/helper.sh" + +Without -D, rcdn should not remove managed dangling symlinks + + $ touch .dotfiles/alpha + > rcup >/dev/null + > rm .dotfiles/alpha + > rcdn >/dev/null + $ assert "alpha should still be a dangling symlink" -h "$HOME/.alpha" + +With -D, rcdn should remove managed dangling symlinks + + $ rcdn -D >/dev/null + $ refute "alpha should be removed when -D is passed" -e "$HOME/.alpha" + +With -D, rcdn should keep unmanaged dangling symlinks + + $ ln -s "$HOME/not-managed-target" "$HOME/.outside" + $ rcdn -D >/dev/null + $ assert "outside should still be a dangling symlink" -h "$HOME/.outside" + +With -D and FILE arguments, cleanup should be restricted to those files + + $ touch .dotfiles/one .dotfiles/two + > rcup >/dev/null + > rm .dotfiles/one .dotfiles/two + $ assert "one should be dangling symlink" -h "$HOME/.one" + $ assert "two should be dangling symlink" -h "$HOME/.two" + $ rcdn -D one >/dev/null + $ refute "one should be removed" -e "$HOME/.one" + $ assert "two should remain because it was not requested" -h "$HOME/.two" + +With -D, rcdn should not remove dangling symlinks inside source dotfiles dirs + + $ ln -s "$HOME/.dotfiles/missing-in-source" "$HOME/.dotfiles/internal-dangling" + $ rcdn -D >/dev/null + $ assert "internal source-dir dangling link should not be removed" -h "$HOME/.dotfiles/internal-dangling"