1# shellcheck shell=bash
2# Setup hook for the `installShellFiles` package.
3#
4# Example usage in a derivation:
5#
6# { …, installShellFiles, … }:
7# stdenv.mkDerivation {
8# …
9# nativeBuildInputs = [ installShellFiles ];
10# postInstall = ''
11# installManPage share/doc/foobar.1
12# installShellCompletion share/completions/foobar.{bash,fish,zsh}
13# '';
14# …
15# }
16#
17# See comments on each function for more details.
18
19# installManPage [--name <path>] <path> [...<path>]
20#
21# Each argument is checked for its man section suffix and installed into the appropriate
22# share/man/man<n>/ directory. The function returns an error if any paths don't have the man
23# section suffix (with optional .gz compression).
24#
25# Optionally accepts pipes as input, which when provided require the `--name` argument to
26# name the output file.
27#
28# installManPage --name foobar.1 <($out/bin/foobar --manpage)
29installManPage() {
30 local arg name='' continueParsing=1
31 while { arg=$1; shift; }; do
32 if (( continueParsing )); then
33 case "$arg" in
34 --name)
35 name=$1
36 shift || {
37 nixErrorLog "${FUNCNAME[0]}: --name flag expected an argument"
38 return 1
39 }
40 continue;;
41 --name=*)
42 # Treat `--name=foo` that same as `--name foo`
43 name=${arg#--name=}
44 continue;;
45 --)
46 continueParsing=0
47 continue;;
48 esac
49 fi
50
51 nixInfoLog "${FUNCNAME[0]}: installing $arg${name:+ as $name}"
52 local basename
53
54 # Check if path is empty
55 if test -z "$arg"; then
56 # It is an empty string
57 nixErrorLog "${FUNCNAME[0]}: path cannot be empty"
58 return 1
59 fi
60
61 if test -n "$name"; then
62 # Provided name. Required for pipes, optional for paths
63 basename=$name
64 elif test -p "$arg"; then
65 # Named pipe requires a file name
66 nixErrorLog "${FUNCNAME[0]}: named pipe requires --name argument"
67 else
68 # Normal file without a name
69 basename=$(stripHash "$arg") # use stripHash in case it's a nix store path
70 fi
71
72 # Check that it is well-formed
73 local trimmed=${basename%.gz} # don't get fooled by compressed manpages
74 local suffix=${trimmed##*.}
75 if test -z "$suffix" -o "$suffix" = "$trimmed"; then
76 nixErrorLog "${FUNCNAME[0]}: path missing manpage section suffix: $arg"
77 return 1
78 fi
79
80 # Create the out-path
81 local outRoot
82 if test "$suffix" = 3; then
83 outRoot=${!outputDevman:?}
84 else
85 outRoot=${!outputMan:?}
86 fi
87 local outPath="${outRoot}/share/man/man$suffix/"
88 nixInfoLog "${FUNCNAME[0]}: installing to $outPath"
89
90 # Install
91 if test -p "$arg"; then
92 # install doesn't work with pipes on Darwin
93 mkdir -p "$outPath" && cat "$arg" > "$outPath/$basename"
94 else
95 install -D --mode=644 --no-target-directory -- "$arg" "$outPath/$basename"
96 fi
97
98 # Reset the name for the next page
99 name=
100 done
101}
102
103# installShellCompletion [--cmd <name>] ([--bash|--fish|--zsh] [--name <name>] <path>)...
104#
105# Each path is installed into the appropriate directory for shell completions for the given shell.
106# If one of `--bash`, `--fish`, `--zsh`, or `--nushell` is given the path is assumed to belong to
107# that shell. Otherwise the file extension will be examined to pick a shell. If the shell is
108# unknown a warning will be logged and the command will return a non-zero status code after
109# processing any remaining paths. Any of the shell flags will affect all subsequent paths (unless
110# another shell flag is given).
111#
112# If the shell completion needs to be renamed before installing the optional `--name <name>` flag
113# may be given. Any name provided with this flag only applies to the next path.
114#
115# If all shell completions need to be renamed before installing the optional `--cmd <name>` flag
116# may be given. This will synthesize a name for each file, unless overridden with an explicit
117# `--name` flag. For example, `--cmd foobar` will synthesize the name `_foobar` for zsh and
118# `foobar.bash` for bash.
119#
120# For zsh completions, if the `--name` flag is not given, the path will be automatically renamed
121# such that `foobar.zsh` becomes `_foobar`.
122#
123# A path may be a named fd, such as produced by the bash construct `<(cmd)`. When using a named fd,
124# the shell type flag must be provided, and either the `--name` or `--cmd` flag must be provided.
125# This might look something like:
126#
127# installShellCompletion --zsh --name _foobar <($out/bin/foobar --zsh-completion)
128#
129# This command accepts multiple shell flags in conjunction with multiple paths if you wish to
130# install them all in one command:
131#
132# installShellCompletion share/completions/foobar.{bash,fish} --zsh share/completions/_foobar
133#
134# However it may be easier to read if each shell is split into its own invocation, especially when
135# renaming is involved:
136#
137# installShellCompletion --bash --name foobar.bash share/completions.bash
138# installShellCompletion --fish --name foobar.fish share/completions.fish
139# installShellCompletion --nushell --name foobar share/completions.nu
140# installShellCompletion --zsh --name _foobar share/completions.zsh
141#
142# Or to use shell newline escaping to split a single invocation across multiple lines:
143#
144# installShellCompletion --cmd foobar \
145# --bash <($out/bin/foobar --bash-completion) \
146# --fish <($out/bin/foobar --fish-completion) \
147# --nushell <($out/bin/foobar --nushell-completion)
148# --zsh <($out/bin/foobar --zsh-completion)
149#
150# If any argument is `--` the remaining arguments will be treated as paths.
151installShellCompletion() {
152 local shell='' name='' cmdname='' retval=0 parseArgs=1 arg
153 while { arg=$1; shift; }; do
154 # Parse arguments
155 if (( parseArgs )); then
156 case "$arg" in
157 --bash|--fish|--zsh|--nushell)
158 shell=${arg#--}
159 continue;;
160 --name)
161 name=$1
162 shift || {
163 nixErrorLog "${FUNCNAME[0]}: --name flag expected an argument"
164 return 1
165 }
166 continue;;
167 --name=*)
168 # treat `--name=foo` the same as `--name foo`
169 name=${arg#--name=}
170 continue;;
171 --cmd)
172 cmdname=$1
173 shift || {
174 nixErrorLog "${FUNCNAME[0]}: --cmd flag expected an argument"
175 return 1
176 }
177 continue;;
178 --cmd=*)
179 # treat `--cmd=foo` the same as `--cmd foo`
180 cmdname=${arg#--cmd=}
181 continue;;
182 --?*)
183 nixWarnLog "${FUNCNAME[0]}: unknown flag ${arg%%=*}"
184 retval=2
185 continue;;
186 --)
187 # treat remaining args as paths
188 parseArgs=0
189 continue;;
190 esac
191 fi
192 nixInfoLog "${FUNCNAME[0]}: installing $arg${name:+ as $name}"
193 # if we get here, this is a path or named pipe
194 # Identify shell and output name
195 local curShell=$shell
196 local outName=''
197 if [[ -z "$arg" ]]; then
198 nixErrorLog "${FUNCNAME[0]}: empty path is not allowed"
199 return 1
200 elif [[ -p "$arg" ]]; then
201 # this is a named fd or fifo
202 if [[ -z "$curShell" ]]; then
203 nixErrorLog "${FUNCNAME[0]}: named pipe requires one of --bash, --fish, --zsh, or --nushell"
204 return 1
205 elif [[ -z "$name" && -z "$cmdname" ]]; then
206 nixErrorLog "${FUNCNAME[0]}: named pipe requires one of --cmd or --name"
207 return 1
208 fi
209 else
210 # this is a path
211 local argbase
212 argbase=$(stripHash "$arg")
213 if [[ -z "$curShell" ]]; then
214 # auto-detect the shell
215 case "$argbase" in
216 ?*.bash) curShell=bash;;
217 ?*.fish) curShell=fish;;
218 ?*.nu) curShell=nushell;;
219 ?*.zsh) curShell=zsh;;
220 *)
221 if [[ "$argbase" = _* && "$argbase" != *.* ]]; then
222 # probably zsh
223 nixWarnLog "${FUNCNAME[0]}: assuming path \`$arg' is zsh; please specify with --zsh"
224 curShell=zsh
225 else
226 nixWarnLog "${FUNCNAME[0]}: unknown shell for path: $arg" >&2
227 retval=2
228 continue
229 fi;;
230 esac
231 fi
232 outName=$argbase
233 fi
234 # Identify output path
235 if [[ -n "$name" ]]; then
236 outName=$name
237 elif [[ -n "$cmdname" ]]; then
238 case "$curShell" in
239 bash|fish) outName=$cmdname.$curShell;;
240 nushell) outName=$cmdname.nu;;
241 zsh) outName=_$cmdname;;
242 *)
243 # Our list of shells is out of sync with the flags we accept or extensions we detect.
244 nixErrorLog "${FUNCNAME[0]}: internal: shell $curShell not recognized"
245 return 1;;
246 esac
247 fi
248 local sharePath
249 case "$curShell" in
250 bash) sharePath=bash-completion/completions;;
251 fish) sharePath=fish/vendor_completions.d;;
252 nushell) sharePath=nushell/vendor/autoload;;
253 zsh)
254 sharePath=zsh/site-functions
255 # only apply automatic renaming if we didn't have a manual rename
256 if [[ -z "$name" && -z "$cmdname" ]]; then
257 # convert a name like `foo.zsh` into `_foo`
258 outName=${outName%.zsh}
259 outName=_${outName#_}
260 fi;;
261 *)
262 # Our list of shells is out of sync with the flags we accept or extensions we detect.
263 nixErrorLog "${FUNCNAME[0]}: internal: shell $curShell not recognized"
264 return 1;;
265 esac
266 # Install file
267 local outDir="${!outputBin:?}/share/$sharePath"
268 local outPath="$outDir/$outName"
269 if [[ -p "$arg" ]]; then
270 # install handles named pipes on NixOS but not on macOS
271 mkdir -p "$outDir" \
272 && cat "$arg" > "$outPath"
273 else
274 install -D --mode=644 --no-target-directory "$arg" "$outPath"
275 fi
276
277 if [ ! -s "$outPath" ]; then
278 nixErrorLog "${FUNCNAME[0]}: installed shell completion file \`$outPath' does not exist or has zero size"
279 return 1
280 fi
281 # Clear the per-path flags
282 name=
283 done
284 if [[ -n "$name" ]]; then
285 nixErrorLog "${FUNCNAME[0]}: --name flag given with no path" >&2
286 return 1
287 fi
288 return $retval
289}
290
291# installBin <path> [...<path>]
292#
293# Install each argument to $outputBin
294installBin() {
295 local path
296 for path in "$@"; do
297 if test -z "$path"; then
298 nixErrorLog "${FUNCNAME[0]}: path cannot be empty"
299 return 1
300 fi
301 nixInfoLog "${FUNCNAME[0]}: installing $path"
302
303 local basename
304 # use stripHash in case it's a nix store path
305 basename=$(stripHash "$path")
306
307 local outRoot
308 outRoot=${!outputBin:?}
309
310 local outPath="${outRoot}/bin/$basename"
311 install -D --mode=755 --no-target-directory "$path" "${outRoot}/bin/$basename"
312 done
313}