1056c90902a676b2e4c15223ae4019a3589fae55
[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 rm -rf. Create directory of link if needed. Slightly more restrictive
47 arguments than ln.
48
49 In the 1st form, create a link to TARGET with the name LINK_NAME. In the 2nd
50 form, create a link to TARGET in the current directory. In the 3rd form, create
51 links to each TARGET in DIRECTORY.
52
53 -n|--dry-run Do verbose dry run.
54 -v|--verbose Print commands which modify the filesystem.
55 -h|--help Print help and exit.
56 "
57
58
59 local temp nodir
60 local verbose=false
61 local dry_run=false
62 local do_exit=false
63 temp=$(getopt -l help,dry-run,verbose hnTv "$@") || usage 1
64 eval set -- "$temp"
65 while true; do
66 case $1 in
67 -n|--dry-run) dry_run=true; verbose=true; shift ;;
68 -T) nodir=-T; shift ;;
69 -v|--verbose) verbose=true; shift ;;
70 -h|--help) echo "$help"; return 0 ;;
71 --) shift; break ;;
72 *) echo "$0: Internal error! unexpected args: $*" ; exit 1 ;;
73 esac
74 done
75
76 if (( $# == 0 )); then
77 echo "$help"
78 return 1
79 fi
80
81 if [[ $nodir ]]; then
82 if (( $# != 2 )); then
83 echo "lnf: error: expected 2 arguments with -T flag. Got $#"
84 return 1
85 fi
86 fi
87
88 local reset_extglob=false
89 ! shopt extglob >/dev/null && reset_extglob=true
90 shopt -s extglob
91
92
93 local -a to_remove to_link
94 local ret prefix dest_dir grp target
95 local mkdir=false
96
97 if [[ $nodir ]]; then
98 dest_file="$2"
99 dest_dir="$(dirname "$dest_file")"
100 _lnf_existing_link "$1" "$dest_file" "$dest_dir" || return $?
101 if $do_exit; then return 0; fi
102 if [[ ! -d $dest_dir ]]; then
103 mkdir=true
104 if [[ -e $dest_dir || -L $dest_dir ]]; then
105 to_remove+=("$dest_dir")
106 fi
107 fi
108 to_link+=("$dest_file")
109 elif (( $# >= 2 )); then
110 dest_dir="${!#}"
111 if [[ -d $dest_dir ]]; then
112 prefix="$dest_dir" # last arg
113 for target in "${@:1:$(( $# - 1 ))}"; do # all but last arg
114 # Remove 1 or more trailing slashes, using.
115 dest_file="${target%%+(/)}"
116 # remove any leading directory components, add prefix
117 dest_file="$prefix/${target##*/}"
118 _lnf_existing_link "$target" "$dest_file" "$dest_dir"
119 done
120 else
121 to_link+=("${@:1:$(( $# - 1 ))}")
122 mkdir=true
123 fi
124 if (( ${#to_link[@]} == 0 )); then
125 return 0
126 fi
127 to_link+=("$dest_dir")
128 elif [[ $# -eq 1 ]]; then
129 dest_file="${1##*/}"
130 _lnf_existing_link "$1" "$dest_file" . || return $?
131 if $do_exit; then return 0; fi
132 fi
133 if (( ${#to_remove[@]} >= 1 )); then
134 if $verbose; then
135 echo "lnf: rm -rf -- ${to_remove[*]}"
136 fi
137 if ! $dry_run; then
138 rm -rf -- "${to_remove[@]}"
139 fi
140 fi
141
142 $reset_extglob && shopt -u extglob
143
144 if $mkdir; then
145 if $verbose; then
146 echo "lnf: mkdir -p $dest_dir"
147 fi
148
149 if ! $dry_run && ! mkdir -p "$dest_dir"; then
150 echo "lnf error: failed to make directory $dest_dir"
151 return 1
152 fi
153 fi
154
155 if $verbose; then
156 echo "lnf: ln -s $nodir -- ${to_link[*]}"
157 fi
158 if ! $dry_run; then
159 ln -s $nodir -- "${to_link[@]}"
160 fi
161 }
162 lnf "$@"