Bundling Scripts with Nix
I write a lot of shell scripts.
Many are one-offs or specific to a project,
but every so often,
I’ll have a script that transcends to become a part of my toolbelt.
nix-op-key is a script I wrote to generate new Nix signing keys
and place them in 1Password.
It’s not a task that requires a dedicated program,
it just needs to glue two existing programs together:
nix key generate-secret and
op (the 1Password CLI).
These sorts of scripts are great,
but if you want to share them with someone else
(or even just use it on a different computer),
how do you do it?
Scripts like these depend on specific programs (or maybe even specific versions) being installed
and Bash does not have a package manager like
pip or the
As it turns out, Nix is such a package manager.
And with flakes, there’s built-in support for installing and running scripts
with well-specified dependencies
in a single command.
For example, you can run my
nix-op-key script I mentioned earlier
(pinned to a specific version)
nix run "github:zombiezen/dotfiles?dir=nix&rev=$COMMIT#nix-op-key" -- --help
Furthermore, you can install the script using the
nix profile install command:
nix profile install "github:zombiezen/dotfiles?dir=nix&rev=$COMMIT#nix-op-key"
(If you try this out yourself, you can uninstall the script with
nix profile remove '.*.nix-op-key').
In this blog post, I’ll show you how you can package your own shell scripts with Nix to make them more reliable and easier to share. This article assumes familiarity with Unix command line and Bash shell scripting. I’m using Nix 2.17.1. All of the source code in this post is released under the Unlicense and is available on GitHub.
To follow along, you will need to install Nix.
We’re also going to be using flakes,
which is an experimental feature at time of writing,
so you will need to add the following line to your
extra-experimental-features = nix-command flakes
Create a new directory somewhere and let’s make a script called
set -euo pipefail
hello --greeting='Hello, World!' "$@"
Next, create a
flake.nix file in the same directory:
Some of this is flakes boilerplate, but let me guide you through the notable parts:
Line 13 gives our script its “attribute name” of
hello, which is what gets used on the command line to build and run it. We’re using the standard Nixpkgs
writeTextFilefunction to write a string as a file in a folder structure.
Line 14 specifies the name of the “derivation” that will be built. In practice, this just means the name of the directory. If you’re interested in learning more, see my previous post about how derivations work.
writeTextFileon line 16 tells the function where to place the script inside its output directory. This allows us to specify the script’s name in its
Line 24 is an interpolated expression that evaluates to Nixpkgs’s copy of Bash.
Line 25 is where things get really interesting: we can grab any package from Nixpkgs and prepend it to the
PATH. This is where the Nix magic kicks in: just by writing the path to the
bin/directories into the script, Nix will automatically register those packages as runtime dependencies. From the derivation reference:
Nix scans each output path for references to input paths by looking for the hash parts of the input paths. Since these are potential runtime dependencies, Nix registers them as dependencies of the output paths.
In this example, we add GNU Hello and GNU Coreutils to our
PATH. Lines 19-22 generate a colon-separated string of
bindirectories using the Nixpkgs
makeBinPathfunction. Including Coreutils is pretty handy for most scripts because it guarantees that we’re using the same standard Unix utilities regardless of platform. For example, you no longer have to resort to strange hacks in order to portably create a temporary directory in your script: you can just write
And finally, on Line 26 we concatenate our Nix-fueled prelude with the script that we’ve written.
Let’s see what our build generates:
(If you’re following along in a Git repository, you will need to
git add the files first.)
nix build '.#hello' &&
On my (64-bit Intel Linux) machine, I get:
set -euo pipefail
hello --greeting='Hello, World!' "$@"
If you use the same
flake.lock file as I did on a 64-bit Intel-based Linux environment,
your script will be identical.
In fact, you can build my Gist directly to compare:
nix build 'git+https://gist.github.com/2288c85813d9a8a161484e334c858a5f.git#hello' &&
You can run your script using
$ nix run '.#hello'
Or you can install it into your Nix profile with
nix profile install:
nix profile install '.#hello' &&
hash -r &&
I hope this helps you see how helpful Nix can be, even for simple scripts. Nix can package even more complex software, so even if your script requires data files or you want to write it in another scripting language, you can still use Nix to package it up. If you’re curious to learn more about Nix works, see my post Connecting Bash to Nix.
Edit 2023-12-15: Travis A. Everett pointed out
writeShellApplication can simplify the Nix expression.
writeShellApplication also validates with
sets a few extra shell options in the resulting script.
For pedagogical purposes, I’m keeping
writeTextFile in the examples
to cut down on how much I have to explain,
but this is very useful in production shell scripts.
writeShellApplication, I realized I had the opportunity
to make the
PATH expression easier to add to by using
so I edited the example to incorporate it.
This does not change the output of the resulting script
and makes the example easier to read and build upon.