keep existing links
[lnf] / lnf
1 #!/bin/bash
2 # Copyright (C) 2014-2016 Ian Kelling
3 # This program is under GPL v. 3 or later, see <http://www.gnu.org/licenses/>
4
5 _lnf_existing_link() {
6 local target dest_file dest_dir
7 target="$1"
8 dest_file="$2"
9 dest_dir="$3"
10 if [[ -L $dest_file ]]; then
11 if [[ $(readlink $dest_file) == "$target" ]]; then
12 # Leave the link in place, but make sure it's
13 # ownership is right.
14 # already exists. links all get 777 perms, so
15 # we dun have to mess with that.
16 if [[ $(stat -L -c%a "$dest_dir") == 2* ]]; then
17 grp=$(stat -L -c%g "$dest_dir")
18 else
19 grp=$(id -g)
20 fi
21 if [[ $EUID == 0 && $(stat -c%u "$dest_file") != 0 ]]; then
22 chown 0:$grp "$dest_file"
23 elif [[ $(stat -c%g "$dest_file") != "$grp" ]]; then
24 chgrp $grp "$dest_file"
25 fi
26 return 1
27 fi
28 to_remove+=("$dest_file")
29 elif [[ -e $dest_file ]]; then
30 to_remove+=("$dest_file")
31 fi
32 to_link+=("$target")
33 }
34 lnf() {
35 local help="Usage:
36 lnf [OPTIONS] -T TARGET LINK_NAME (1st form)
37 lnf [OPTIONS] TARGET (2nd form)
38 lnf [OPTIONS] TARGET... DIRECTORY (3rd form)
39 Create symlinks forcefully
40
41 If the link already exists, make it's ownership be the same as if it was
42 newly created (only chown if we are root). Removes existing files using
43 trash-put or rm -rf if it is not available, or if trash-put fails due to a
44 limitation such as a cross-filesystem link. Create directory of link if
45 needed. Slightly more restrictive arguments than ln.
46
47 In the 1st form, create a link to TARGET with the name LINK_NAME. In the 2nd
48 form, create a link to TARGET in the current directory. In the 3rd form, create
49 links to each TARGET in DIRECTORY.
50
51 -n|--dry-run Do verbose dry run.
52 -v|--verbose Print commands which modify the filesystem.
53 -h|--help Print help and exit.
54 "
55
56
57 local temp
58 local verbose=false
59 local dry_run=false
60 local nodir
61 temp=$(getopt -l help,dry-run,verbose hnTv "$@") || usage 1
62 eval set -- "$temp"
63 while true; do
64 case $1 in
65 -n|--dry-run) dry_run=true; verbose=true; shift ;;
66 -T) nodir=-T; shift ;;
67 -v|--verbose) verbose=true; shift ;;
68 -h|--help) echo "$help"; return 0 ;;
69 --) shift; break ;;
70 *) echo "$0: Internal error! unexpected args: $*" ; exit 1 ;;
71 esac
72 done
73
74 if (( $# == 0 )); then
75 echo "$help"
76 return 1
77 fi
78
79 if [[ $nodir ]]; then
80 if (( $# != 2 )); then
81 echo "lnf: error: expected 2 arguments with -T flag. Got $#"
82 return 1
83 fi
84 fi
85
86 local reset_extglob=false
87 ! shopt extglob >/dev/null && reset_extglob=true
88 shopt -s extglob
89
90
91 local -a to_remove to_link
92 local x ret prefix dest_dir grp target
93 local mkdir=false
94
95 if [[ $nodir ]]; then
96 dest_file="$2"
97 dest_dir="$(dirname "$dest_file")"
98 _lnf_existing_link "$1" "$dest_file" "$dest_dir" || return 0
99 if [[ ! -d $dest_dir ]]; then
100 mkdir=true
101 if [[ -e $dest_dir || -L $dest_dir ]]; then
102 to_remove+=("$dest_dir")
103 fi
104 fi
105 to_link+=("$dest_file")
106 elif (( $# >= 2 )); then
107 dest_dir="${!#}"
108 if [[ -d $dest_dir ]]; then
109 prefix="$dest_dir" # last arg
110 for target in "${@:1:$(( $# - 1 ))}"; do # all but last arg
111 # Remove 1 or more trailing slashes, using.
112 dest_file="${target%%+(/)}"
113 # remove any leading directory components, add prefix
114 dest_file="$prefix/${target##*/}"
115 _lnf_existing_link "$target" "$dest_file" "$dest_dir"
116 done
117 else
118 to_link+=("${@:1:$(( $# - 1 ))}")
119 mkdir=true
120 fi
121 if (( ${#to_link[@]} == 0 )); then
122 return 0
123 fi
124 to_link+=("$dest_dir")
125 elif [[ $# -eq 1 ]]; then
126 dest_file="${1##*/}"
127 _lnf_existing_link "$1" "$dest_file" . || return 0
128 fi
129 if (( ${#to_remove[@]} >= 1 )); then
130 if type -P trash-put >/dev/null; then
131 if $verbose; then
132 echo "lnf: trash-put -- ${to_remove[*]}"
133 fi
134 if ! $dry_run; then
135 trash-put -- "${to_remove[@]}" || ret=$?
136 fi
137 # trash-put will fail to trash a link that goes across filesystems (72),
138 # and for empty files (74)
139 # so revert to rm -rf in that case
140 if [[ $ret == 72 ]]; then
141 echo "lnf: using rm -rf to overcome cross filesystem trash-put limitation"
142 rm -rf -- "${to_remove[@]}"
143 elif [[ $ret == 74 ]]; then
144 echo "lnf: using rm -rf to overcome empty file & hardlink trash-put limitation"
145 rm -rf -- "${to_remove[@]}"
146 elif [[ $ret && $ret != 0 ]]; then
147 return $x
148 fi
149 else
150 if $verbose; then
151 echo "lnf: rm -rf -- ${to_remove[*]}"
152 fi
153 if ! $dry_run; then
154 rm -rf -- "${to_remove[@]}"
155 fi
156 fi
157 fi
158
159 $reset_extglob && shopt -u extglob
160
161 if $mkdir; then
162 if $verbose; then
163 echo "lnf: mkdir -p $dest_dir"
164 fi
165
166 if ! $dry_run && ! mkdir -p "$dest_dir"; then
167 echo "lnf error: failed to make directory $dest_dir"
168 return 1
169 fi
170 fi
171
172 if $verbose; then
173 echo "lnf: ln -s $nodir -- ${to_link[*]}"
174 fi
175 if ! $dry_run; then
176 ln -s $nodir -- "${to_link[@]}"
177 fi
178 }
179 lnf "$@"