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
9 # Copyright 2024 Ian Kelling
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
15 # http://www.apache.org/licenses/LICENSE-2.0
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.
24 _lnf_existing_link
() {
25 local target dest_file dest_dir
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.
37 if [[ $
(stat
-L -c%a
"$dest_dir") == 2???
]]; then
38 grp
=$
(stat
-L -c%g
"$dest_dir") ||
return $?
40 grp
=$
(id
-g) ||
return $?
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 $?
50 to_remove
+=("$dest_file")
51 elif [[ -e $dest_file ]]; then
52 to_remove
+=("$dest_file")
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
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
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.
72 -n|--dry-run Do verbose dry run.
73 -v|--verbose Print commands which modify the filesystem.
74 -h|--help Print help and exit.
82 temp
=$
(getopt
-l help,dry-run
,verbose hnTv
"$@") || usage
1
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 ;;
91 *) echo "$0: Internal error! unexpected args: $*" ; exit 1 ;;
95 if (( $# == 0 )); then
100 if [[ $nodir ]]; then
101 if (( $# != 2 )); then
102 echo "lnf: error: expected 2 arguments with -T flag. Got $#"
107 local reset_extglob
=false
108 ! shopt extglob
>/dev
/null
&& reset_extglob
=true
112 local -a to_remove to_link
113 local ret prefix dest_dir grp target
116 if [[ $nodir ]]; then
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
123 if [[ -e $dest_dir ||
-L $dest_dir ]]; then
124 to_remove
+=("$dest_dir")
127 to_link
+=("$dest_file")
128 elif (( $# >= 2 )); then
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"
140 to_link
+=("${@:1:$(( $# - 1 ))}")
143 if (( ${#to_link[@]} == 0 )); then
146 to_link
+=("$dest_dir")
147 elif [[ $# -eq 1 ]]; then
149 _lnf_existing_link
"$1" "$dest_file" . ||
return $?
150 if $do_exit; then return 0; fi
152 if (( ${#to_remove[@]} >= 1 )); then
154 echo "lnf: rm -rf -- ${to_remove[*]}"
157 rm -rf -- "${to_remove[@]}"
161 $reset_extglob && shopt -u extglob
165 echo "lnf: mkdir -p $dest_dir"
168 if ! $dry_run && ! mkdir
-p "$dest_dir"; then
169 echo "lnf error: failed to make directory $dest_dir"
175 echo "lnf: ln -s $nodir -- ${to_link[*]}"
178 ln -s $nodir -- "${to_link[@]}"