Writing Nushell scripts to PATH using Nix and home-manager
- Published
If you're familar with Home Manager using Nix, you likely have used writeShellApplication to write an executable Bash shell script to /nix/store/<store path>/bin/<name> and make it available to any new shell sessions.
I wanted something similar but for any Nushell scripts that I write. I still write Bash scripts if it's something really small or if I need the script to be highly portable to systems where I can't install anything. Since my interactive shell is Nushell, I sometimes end up creating something interactively that I want to keep around as a script for future use, but I don't want to have to rewrite it into Bash or python. Unfortunately there isn't a native 1:1 equivalent for Nushell such as writeNushellApplication, but with a little bit of tinkering I was able to build my own.
How writeShellApplication is implemented
Taking a look at the nixpkgs source to understand how writeShellApplication is implemented, I saw that it runs through the following steps:
- Write the interpreter shebang as the first line in the file.
- Add provided file content to the file after the shebang.
- Make the file executable using
chmod +x. - Update the
PATHenvironment variable to include the path to the new file. - Run shellcheck to lint the generated file.
For its part, home-manager creates a symlink from the built script in the store path to the user profile.
With those steps in mind, I took a look at what existed in the official docs and source code.
Using what Nixpkgs already has
Looking in the Nixpkgs Reference Manual and Home Manager Manual, I couldn't find anything specific about writing Nushell scripts.
However, looking through the files in https://github.com/NixOS/nixpkgs/pkgs/build-support, I did find writeNu and writeNuBin:
writeNu =
name: argsOrScript:
if lib.isAttrs argsOrScript && !lib.isDerivation argsOrScript then
makeScriptWriter (
argsOrScript // { interpreter = "${lib.getExe pkgs.nushell} --no-config-file"; }
) name
else
makeScriptWriter { interpreter = "${lib.getExe pkgs.nushell} --no-config-file"; } name argsOrScript;
writeNuBin = name: writeNu "/bin/${name}"; These functions are thin wrappers around makeScriptWriter. Calling writeNuBin will perform the following steps:
- Write the interpreter shebang as the first line in the file.
- Write the provided file content to the file after the shebang.
- Make the file executable using
chmod +x.
The only steps that are missing then are adding the file to the PATH environment variable, and implementing a linting step.
Creating the initial wrapper
I started by creating a new writeNushellApplication function that calls writeNuBin and ensures that the generated script is added to the PATH. I was able to copy this almost verbatim from the comments on the writeNuBin function.
writeNushellApplication =
{
name,
text,
runtimeInputs ? [ ],
makeWrapperArgs ? [ ],
}:
pkgs.writers.writeNuBin name {
makeWrapperArgs =
makeWrapperArgs
++ pkgs.lib.optionals (runtimeInputs != [ ]) [
"--prefix"
"PATH"
":"
(pkgs.lib.makeBinPath runtimeInputs)
];
} text; Adding this to my home-manager config's let block allows me to use it exactly as I would writeShellApplication:
(writeNushellApplication {
name = "dither";
text = builtins.readFile ./scripts/dither.nu;
}) After rebuilding my home-manager config, dither is now a command that is symlinked under my profile (/etc/profiles/per-user/lucas/bin/), and it uses #!/nix/store/<omitted...>/nu --no-config-file as its shebang.
There's just one more step left.
Adding a lint check
makeScriptWriter provides support for a check hook, it's just unused by default. Since Nushell ships with a syntax checker, nu-check, I decided to use that to lint my scripts.
There's just one small wrinkle. makeScriptWriter calls the provided check using ${check} out, which means it expects a shell command that it can pass the built script into. But nu-check isn't a standalone executable, it's a command inside Nushell. So I needed to create a small wrapper that passes the script to nu -c "nu-check".
writeNushellApplication =
{
name,
text,
runtimeInputs ? [ ],
makeWrapperArgs ? [ ],
check ? true,
}:
pkgs.writers.writeNuBin name {
makeWrapperArgs =
makeWrapperArgs
++ pkgs.lib.optionals (runtimeInputs != [ ]) [
"--prefix"
"PATH"
":"
(pkgs.lib.makeBinPath runtimeInputs)
];
check = pkgs.lib.optionalString check (
pkgs.writeShellScript "nu-check-wrapper" ''
${pkgs.lib.getExe pkgs.nushell} --no-config-file -c "nu-check --debug '$1'"
''
);
} text; Now, if nu-check finds a syntax error, the build step will fail. By default all Nushell scripts will be linted at build time, but to disable the check for a specific script, I can just pass check = false; to the function.
(writeNushellApplication {
name = "dither";
text = builtins.readFile ./scripts/dither.nu;
check = false;
})