license change
[lnf] / lnf
1 #!/bin/bash
2 # I, Ian Kelling, follow the GNU license recommendations at
3 # https://www.gnu.org/licenses/license-recommendations.en.html. They
4 # recommend that small programs, < 300 lines, be licensed under the
5 # Apache License 2.0. This file contains or is part of one or more small
6 # programs. If a small program grows beyond 300 lines, I plan to switch
7 # its license to GPL.
8
9 # Copyright 2024 Ian Kelling
10
11 # Licensed under the Apache License, Version 2.0 (the "License");
12 # you may not use this file except in compliance with the License.
13 # You may obtain a copy of the License at
14
15 # http://www.apache.org/licenses/LICENSE-2.0
16
17 # Unless required by applicable law or agreed to in writing, software
18 # distributed under the License is distributed on an "AS IS" BASIS,
19 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 # See the License for the specific language governing permissions and
21 # limitations under the License.
22
23
24 _lnf_existing_link() {
25 local target dest_file dest_dir
26 target="$1"
27 dest_file="$2"
28 dest_dir="$3"
29 if [[ -L $dest_file ]]; then
30 if [[ $(readlink $dest_file) == "$target" ]]; then
31 # Leave the link in place, but make sure it's
32 # owner & group is as if we created it.
33 # links all get 777 perms, so
34 # we already know that is right.
35
36 # test for setgid.
37 if [[ $(stat -L -c%a "$dest_dir") == 2??? ]]; then
38 grp=$(stat -L -c%g "$dest_dir") || return $?
39 else
40 grp=$(id -g) || return $?
41 fi
42 if [[ $EUID == 0 && $(stat -c%u "$dest_file") != 0 ]]; then
43 chown -h 0:$grp "$dest_file" || return $?
44 elif [[ $(stat -c%g "$dest_file") != "$grp" ]]; then
45 chgrp -h $grp "$dest_file" || return $?
46 fi
47 do_exit=true
48 return 0
49 fi
50 to_remove+=("$dest_file")
51 elif [[ -e $dest_file ]]; then
52 to_remove+=("$dest_file")
53 fi
54 to_link+=("$target")
55 }
56 lnf() {
57 local help="Usage:
58 lnf [OPTIONS] -T TARGET LINK_NAME (1st form)
59 lnf [OPTIONS] TARGET (2nd form)
60 lnf [OPTIONS] TARGET... DIRECTORY (3rd form)
61 Create symlinks forcefully
62
63 If the link already exists, make it's ownership be the same as if it was
64 newly created (only chown if we are root). Removes existing files using
65 rm -rf. Create directory of link if needed. Slightly more restrictive
66 arguments than ln.
67
68 In the 1st form, create a link to TARGET with the name LINK_NAME. In the 2nd
69 form, create a link to TARGET in the current directory. In the 3rd form, create
70 links to each TARGET in DIRECTORY.
71
72 -n|--dry-run Do verbose dry run.
73 -v|--verbose Print commands which modify the filesystem.
74 -h|--help Print help and exit.
75 "
76
77
78 local temp nodir
79 local verbose=false
80 local dry_run=false
81 local do_exit=false
82 temp=$(getopt -l help,dry-run,verbose hnTv "$@") || usage 1
83 eval set -- "$temp"
84 while true; do
85 case $1 in
86 -n|--dry-run) dry_run=true; verbose=true; shift ;;
87 -T) nodir=-T; shift ;;
88 -v|--verbose) verbose=true; shift ;;
89 -h|--help) echo "$help"; return 0 ;;
90 --) shift; break ;;
91 *) echo "$0: Internal error! unexpected args: $*" ; exit 1 ;;
92 esac
93 done
94
95 if (( $# == 0 )); then
96 echo "$help"
97 return 1
98 fi
99
100 if [[ $nodir ]]; then
101 if (( $# != 2 )); then
102 echo "lnf: error: expected 2 arguments with -T flag. Got $#"
103 return 1
104 fi
105 fi
106
107 local reset_extglob=false
108 ! shopt extglob >/dev/null && reset_extglob=true
109 shopt -s extglob
110
111
112 local -a to_remove to_link
113 local ret prefix dest_dir grp target
114 local mkdir=false
115
116 if [[ $nodir ]]; then
117 dest_file="$2"
118 dest_dir="$(dirname "$dest_file")"
119 _lnf_existing_link "$1" "$dest_file" "$dest_dir" || return $?
120 if $do_exit; then return 0; fi
121 if [[ ! -d $dest_dir ]]; then
122 mkdir=true
123 if [[ -e $dest_dir || -L $dest_dir ]]; then
124 to_remove+=("$dest_dir")
125 fi
126 fi
127 to_link+=("$dest_file")
128 elif (( $# >= 2 )); then
129 dest_dir="${!#}"
130 if [[ -d $dest_dir ]]; then
131 prefix="$dest_dir" # last arg
132 for target in "${@:1:$(( $# - 1 ))}"; do # all but last arg
133 # Remove 1 or more trailing slashes, using.
134 dest_file="${target%%+(/)}"
135 # remove any leading directory components, add prefix
136 dest_file="$prefix/${target##*/}"
137 _lnf_existing_link "$target" "$dest_file" "$dest_dir"
138 done
139 else
140 to_link+=("${@:1:$(( $# - 1 ))}")
141 mkdir=true
142 fi
143 if (( ${#to_link[@]} == 0 )); then
144 return 0
145 fi
146 to_link+=("$dest_dir")
147 elif [[ $# -eq 1 ]]; then
148 dest_file="${1##*/}"
149 _lnf_existing_link "$1" "$dest_file" . || return $?
150 if $do_exit; then return 0; fi
151 fi
152 if (( ${#to_remove[@]} >= 1 )); then
153 if $verbose; then
154 echo "lnf: rm -rf -- ${to_remove[*]}"
155 fi
156 if ! $dry_run; then
157 rm -rf -- "${to_remove[@]}"
158 fi
159 fi
160
161 $reset_extglob && shopt -u extglob
162
163 if $mkdir; then
164 if $verbose; then
165 echo "lnf: mkdir -p $dest_dir"
166 fi
167
168 if ! $dry_run && ! mkdir -p "$dest_dir"; then
169 echo "lnf error: failed to make directory $dest_dir"
170 return 1
171 fi
172 fi
173
174 if $verbose; then
175 echo "lnf: ln -s $nodir -- ${to_link[*]}"
176 fi
177 if ! $dry_run; then
178 ln -s $nodir -- "${to_link[@]}"
179 fi
180 }
181 lnf "$@"