installShellFiles: init (#65211)

This is a new package that provides a shell hook to make it easy to
declare manpages and shell completions in a manner that doesn't require
remembering where to actually install them. Basic usage looks like

  { stdenv, installShellFiles, ... }:
  stdenv.mkDerivation {
    # ...
    nativeBuildInputs = [ installShellFiles ];
    postInstall = ''
      installManPage doc/foobar.1
      installShellCompletion --bash share/completions/foobar.bash
      installShellCompletion --fish share/completions/foobar.fish
      installShellCompletion --zsh share/completions/_foobar
    '';
    # ...
  }

See source comments for more details on the functions.
This commit is contained in:
Lily Ballard 2019-07-20 19:53:19 -07:00 committed by Robert Helgesson
parent 37e333af9a
commit 43dade238f
No known key found for this signature in database
GPG key ID: 36BDAA14C2797E89
4 changed files with 214 additions and 0 deletions

View file

@ -2714,6 +2714,49 @@ nativeBuildInputs = [ breakpointHook ];
</note>
</listitem>
</varlistentry>
<varlistentry>
<term>
installShellFiles
</term>
<listitem>
<para>
This hook helps with installing manpages and shell completion files. It
exposes 2 shell functions <literal>installManPage</literal> and
<literal>installShellCompletion</literal> that can be used from your
<literal>postInstall</literal> hook.
</para>
<para>
The <literal>installManPage</literal> function takes one or more paths
to manpages to install. The manpages must have a section suffix, and may
optionally be compressed (with <literal>.gz</literal> suffix). This
function will place them into the correct directory.
</para>
<para>
The <literal>installShellCompletion</literal> function takes one or more
paths to shell completion files. By default it will autodetect the shell
type from the completion file extension, but you may also specify it by
passing one of <literal>--bash</literal>, <literal>--fish</literal>, or
<literal>--zsh</literal>. These flags apply to all paths listed after
them (up until another shell flag is given). Each path may also have a
custom installation name provided by providing a flag <literal>--name
NAME</literal> before the path. If this flag is not provided, zsh
completions will be renamed automatically such that
<literal>foobar.zsh</literal> becomes <literal>_foobar</literal>.
<programlisting>
nativeBuildInputs = [ installShellFiles ];
postInstall = ''
installManPage doc/foobar.1 doc/barfoo.3
# explicit behavior
installShellCompletion --bash --name foobar.bash share/completions.bash
installShellCompletion --fish --name foobar.fish share/completions.fish
installShellCompletion --zsh --name _foobar share/completions.zsh
# implicit behavior
installShellCompletion share/completions/foobar.{bash,fish,zsh}
'';
</programlisting>
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>
libiconv, libintl

View file

@ -0,0 +1,4 @@
{ makeSetupHook }:
# See the header comment in ../setup-hooks/install-shell-files.sh for example usage.
makeSetupHook { name = "install-shell-files"; } ../setup-hooks/install-shell-files.sh

View file

@ -0,0 +1,165 @@
#!/bin/bash
# Setup hook for the `installShellFiles` package.
#
# Example usage in a derivation:
#
# { …, installShellFiles, … }:
# stdenv.mkDerivation {
# …
# nativeBuildInputs = [ installShellFiles ];
# postInstall = ''
# installManPage share/doc/foobar.1
# installShellCompletion share/completions/foobar.{bash,fish,zsh}
# '';
# …
# }
#
# See comments on each function for more details.
# installManPage <path> [...<path>]
#
# Each argument is checked for its man section suffix and installed into the appropriate
# share/man<n>/ directory. The function returns an error if any paths don't have the man section
# suffix (with optional .gz compression).
installManPage() {
local path
for path in "$@"; do
if (( "${NIX_DEBUG:-0}" >= 1 )); then
echo "installManPage: installing $path"
fi
if test -z "$path"; then
echo "installManPage: error: path cannot be empty" >&2
return 1
fi
local basename
basename=$(stripHash "$path") # use stripHash in case it's a nix store path
local trimmed=${basename%.gz} # don't get fooled by compressed manpages
local suffix=${trimmed##*.}
if test -z "$suffix" -o "$suffix" = "$trimmed"; then
echo "installManPage: error: path missing manpage section suffix: $path" >&2
return 1
fi
local outRoot
if test "$suffix" = 3; then
outRoot=${!outputDevman:?}
else
outRoot=${!outputMan:?}
fi
install -Dm644 -T "$path" "${outRoot}/share/man/man$suffix/$basename" || return
done
}
# installShellCompletion [--bash|--fish|--zsh] ([--name <name>] <path>)...
#
# Each path is installed into the appropriate directory for shell completions for the given shell.
# If one of `--bash`, `--fish`, or `--zsh` is given the path is assumed to belong to that shell.
# Otherwise the file extension will be examined to pick a shell. If the shell is unknown a warning
# will be logged and the command will return a non-zero status code after processing any remaining
# paths. Any of the shell flags will affect all subsequent paths (unless another shell flag is
# given).
#
# If the shell completion needs to be renamed before installing the optional `--name <name>` flag
# may be given. Any name provided with this flag only applies to the next path.
#
# For zsh completions, if the `--name` flag is not given, the path will be automatically renamed
# such that `foobar.zsh` becomes `_foobar`.
#
# This command accepts multiple shell flags in conjunction with multiple paths if you wish to
# install them all in one command:
#
# installShellCompletion share/completions/foobar.{bash,fish} --zsh share/completions/_foobar
#
# However it may be easier to read if each shell is split into its own invocation, especially when
# renaming is involved:
#
# installShellCompletion --bash --name foobar.bash share/completions.bash
# installShellCompletion --fish --name foobar.fish share/completions.fish
# installShellCompletion --zsh --name _foobar share/completions.zsh
#
# If any argument is `--` the remaining arguments will be treated as paths.
installShellCompletion() {
local shell='' name='' retval=0 parseArgs=1 arg
while { arg=$1; shift; }; do
# Parse arguments
if (( parseArgs )); then
case "$arg" in
--bash|--fish|--zsh)
shell=${arg#--}
continue;;
--name)
name=$1
shift || {
echo 'installShellCompletion: error: --name flag expected an argument' >&2
return 1
}
continue;;
--name=*)
# treat `--name=foo` the same as `--name foo`
name=${arg#--name=}
continue;;
--?*)
echo "installShellCompletion: warning: unknown flag ${arg%%=*}" >&2
retval=2
continue;;
--)
# treat remaining args as paths
parseArgs=0
continue;;
esac
fi
if (( "${NIX_DEBUG:-0}" >= 1 )); then
echo "installShellCompletion: installing $arg${name:+ as $name}"
fi
# if we get here, this is a path
# Identify shell
local basename
basename=$(stripHash "$arg")
local curShell=$shell
if [[ -z "$curShell" ]]; then
# auto-detect the shell
case "$basename" in
?*.bash) curShell=bash;;
?*.fish) curShell=fish;;
?*.zsh) curShell=zsh;;
*)
if [[ "$basename" = _* && "$basename" != *.* ]]; then
# probably zsh
echo "installShellCompletion: warning: assuming path \`$arg' is zsh; please specify with --zsh" >&2
curShell=zsh
else
echo "installShellCompletion: warning: unknown shell for path: $arg" >&2
retval=2
continue
fi;;
esac
fi
# Identify output path
local outName sharePath
outName=${name:-$basename}
case "$curShell" in
bash) sharePath=bash-completion/completions;;
fish) sharePath=fish/vendor_completions.d;;
zsh)
sharePath=zsh/site-functions
# only apply automatic renaming if we didn't have a manual rename
if test -z "$name"; then
# convert a name like `foo.zsh` into `_foo`
outName=${outName%.zsh}
outName=_${outName#_}
fi;;
*)
# Our list of shells is out of sync with the flags we accept or extensions we detect.
echo 'installShellCompletion: internal error' >&2
return 1;;
esac
# Install file
install -Dm644 -T "$arg" "${!outputBin:?}/share/$sharePath/$outName" || return
# Clear the name, it only applies to one path
name=
done
if [[ -n "$name" ]]; then
echo 'installShellCompletion: error: --name flag given with no path' >&2
return 1
fi
return $retval
}

View file

@ -360,6 +360,8 @@ in
inherit url;
};
installShellFiles = callPackage ../build-support/install-shell-files {};
lazydocker = callPackage ../tools/misc/lazydocker { };
ld-is-cc-hook = makeSetupHook { name = "ld-is-cc-hook"; }