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