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"; }