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