diff --git a/doc/stdenv.xml b/doc/stdenv.xml index fe5929656565..15a13ba49e8e 100644 --- a/doc/stdenv.xml +++ b/doc/stdenv.xml @@ -2714,6 +2714,49 @@ nativeBuildInputs = [ breakpointHook ]; + + + installShellFiles + + + + This hook helps with installing manpages and shell completion files. It + exposes 2 shell functions installManPage and + installShellCompletion that can be used from your + postInstall hook. + + + The installManPage function takes one or more paths + to manpages to install. The manpages must have a section suffix, and may + optionally be compressed (with .gz suffix). This + function will place them into the correct directory. + + + The installShellCompletion 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 --bash, --fish, or + --zsh. 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 --name + NAME before the path. If this flag is not provided, zsh + completions will be renamed automatically such that + foobar.zsh becomes _foobar. + +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} +''; + + + + libiconv, libintl diff --git a/pkgs/build-support/install-shell-files/default.nix b/pkgs/build-support/install-shell-files/default.nix new file mode 100644 index 000000000000..e1f2e24dd875 --- /dev/null +++ b/pkgs/build-support/install-shell-files/default.nix @@ -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 diff --git a/pkgs/build-support/setup-hooks/install-shell-files.sh b/pkgs/build-support/setup-hooks/install-shell-files.sh new file mode 100644 index 000000000000..e0ea1f7f30a7 --- /dev/null +++ b/pkgs/build-support/setup-hooks/install-shell-files.sh @@ -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 [...] +# +# Each argument is checked for its man section suffix and installed into the appropriate +# share/man/ 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 ] )... +# +# 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 ` 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 +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 21f5f14a2dcb..a39687a80f6f 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -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"; }