I appreciate Bash for its
stability and its near-ubiquity in Unix environments, but it has some clear drawbacks. I constantly get bitten by
things like variable handling and string interpolation, I almost always need to supplement my Bash scripts with
tools like awk
, sed
, curl
, jq
, and wget
to get
even basic things done, and I’d be hard-pressed to use “Bash” and “elegant” or “expressive” in the same sentence.
Despite shortcomings like these,
stdenv
), most notably in the stdenv.mkDerivation
function. This wrapper
over Nix’s built-in derivation function is used almost everywhere in
I believe that standardizing on Bash as a known quantity was absolutely the
right choice for the standard environment. But I’ve always been intrigued by
the possibility of using a more powerful, ergonomically-minded shell to
Why I chose Nushell
There are many “alt” shells out there, like Fish, Elvish, and Oil. These are all exciting projects but Nushell really stands out to me. Here’s a non-exhaustive list of reasons why:
- It offers built-in support for JSON, CSV, YAML, TOML, SQLite, and even DataFrames.
- It offers a broad range of built-in
commands that play nicely together and
more or less replace tools like
jq
,curl
,awk
,cut
,join
, andsed
. - It enables you to create custom commands with typed inputs and auto-generated (and pretty) help output.
- It supports nested data records, which gives it a kind of object-oriented flavor.
- It has built-in support for testing.
- It offers scoping mechanisms like modules and overlays.
While Nushell isn’t quite a general-purpose programming language, it certainly skirts the boundary between a shell and a full-fledged language. Not all of Nushell’s features are equally germane to an alternative environment for Nix, of course, but I think that these features show how ambitious the project is.
Here’s what I think Nushell has to offer a new Nix environment:
- It makes a clean separation between environment and standard variables.
$foo
would refer to a variable in the local scope but only$env.foo
could refer to an environment variable. This enables you to avoid those un-fun variable name clashes that pop up so often in Bash. - String handling is powerful enough to obviate the need for the usual smorgasbord of
tools required for string parsing, like
awk
,sed
, andtr
. - The custom commands
- with typed parameters that I mentioned above make the environment much more robust than Bash’s “stringly” typing and awkward function argument handling. Here’s an example custom command:
Now here’s the help output you get by running sayHello --help
:
As you can see, custom commands in Nushell feel like well-crafted CLI tools in their own right. This makes Nushell-based environments far more introspectable than their Bash counterparts.
How I created the environment
To create my new build environment for Nix,
Nuenv, I first wrote a Nix
function wrapping Nix’s built-in
The basic mechanics: Nushell (set as the builder instead of Bash) runs a script
called
bootstrap.nu
that performs some setup actions (like making the Nushell executable itself
discoverable in the env). The bootstrapper then runs a
builder.nu
script that performs the actual realisation. As in all Nix derivations, the
other attributes of the derivation are set as environment variables available
to that script (plus some others that Nix provides). The builder.nu
script
needs to do quite a lot, including:
- Creating the directory in the
Nix store where build output will end up (Nix provides anout
environment variable for that but it doesn’t automatically create that directory). - Copying all of the derivation’s sources (
src
orsrcs
in the standard environment) into the build sandbox, which resides in a temporary directory. Nix does put those sources in the Nix store at/nix/store/$HASH-source
but the builder script needs to copy them into the sandbox. - Making sure that the sandbox can discover any packages required by the
derivation process using the
PATH
.
That establishes a kind of bare minimum environment. Beyond that, a builder
script should provide things like helper functions (commands in Nushell)
for use in derivation logic. The standard environment provides many such
functions via the
user-env.nu
script, like
wrapProgram
and
substituteInPlace
.
For Nuenv, I’ve provided two so far:
substitute
, which performs string substitution in a specified file and writes the output to a new file.substituteInPlace
is likesubstitute
but rewrites the specified file in place.
In principle, however, the full battery of stdenv
functions could be replaced
with custom Nushell commands. Overall, I was extremely impressed with how
elegant the Nushell build script is compared to what its Bash equivalent would
be. I’ll provide an illustrative example. Here’s how I set
the
PATH
to discover packages in the Nix store:
Here’s how you would accomplish something like that in Bash:
As you can see, Nushell feels closer to something like Ruby.
Despite wins like this, Nuenv in its current state does have some major shortcomings vis-à-vis the Nixpkgs standard environment:
- There’s only one realisation phase, called
build
. By contrast, the stdenv is much more nuanced, offering support for Makefiles,configure
scripts, and differentiatedphases
likebuildPhase
,installPhase
,configurePhase
, andcheckPhase
. - You can only pass one set of packages to the environment, whereas stdenv
makes a
distinction
between
buildInputs
,nativeBuildInputs
,propagatedBuildInputs
, and others. The dependencies you supply using packages in Nuenv are only available at build time and aren’t included in the runtime package, while stdenv enables you to target dependencies to both build time and run time as well as things like cross-compilation (which is not supported in Nuenv). - Nuenv-based realisations are likely to be a bit more costly, in terms of
build time and disk space used by the Nix store, than
stdenv.mkDerivation
because the Nushell package is required, although caching and the fact that Nushell doesn’t require coreutils should make this cost relatively small. A related limitation is that you can only realise Nuenv-based derivations on systems that can run Nushell. While that should be a fairly sizable range of systems given that Nushell is built in Rust, it’s nowhere near the range of systems that can run Bash.
So definitely don’t use Nuenv for any serious purpose just yet.
Find out more about how Determinate Systems is transforming the developer experience around Nix
User-facing benefits of Nuenv
Although Nuenv is still at an early stage, I think that it already has some nice advantages over stdenv:
- Inside your derivation logic, you get access to the broad Nushell feature set I talked about above.
- It has much prettier log output thanks to Nushell’s ANSI support. While this is more of a quality-of-life thing, it does liven things up a bit.
- You can try out the Nuenv environment locally by running
nix develop .#nuenv
if you clone the repo (ornix develop github:DeterminateSystems/nuenv#nuenv
if you don’t want to clone the repo locally). This gives you access to Nuenv’s helper commands. RunsubstituteInPlace --help
to see an example. As far as I know, there isn’t really a way to introspect the stdenv in this way. You can access Bash with stdenv’s functions provided by runningnix-shell -p
but those functions don’t provide help output. You can also see a list of custom commands available to Nuenv derivations by runningnix run github:DeterminateSystems/nuenv#nuenv-commands
.
Try it out
You can try out Nuenv in several ways. To build an example package using the environment:
This is a pretty basic derivation that pipes a string to GNU
hello and writes that output to the
share
directory in the build output.
You can also build your own package with Nuenv! Here’s an example flake.nix
:
If you’ve nix build && cat ./result/share/hello.txt
to see the
result. To run a Nushell script instead of providing a raw string, you can
supply build = builtins.readFile ./my-nushell-script.nu
to the derivation.
Ramifications
I think that using a more powerful builder for Nix—maybe Nushell, maybe something else—has major implications:
- If can offer a dramatically improved developer experience around derivation. Right now, derivation remains a bit of a dark art and one of the more intimidating aspects of using Nix (which is already intimidating in itself).
- It can radically alter the division of labor between Nix and the builder. Because Bash isn’t terribly powerful, Nix has to do a lot of the work of making things palatable to Bash, like constructing complex strings. But with a more powerful shell, Nix can defer a lot of work to the builder. Wouldn’t it be nice to just use Nix to supply attribute sets and let the builder handle the rest?
- Because any Nix environment is just Nix, you could introduce something like Nushell piecemeal into the Nix dependency tree. Imagine replacing some core utilities in Nixpkgs with more robust, ergonomic equivalents or dramatically simplifying language-specific derivation wrappers.
While I don’t see Nushell or its ilk taking over the Nix world by storm any time soon, I do hope that this post gets you thinking about what a qualitatively better builder might mean for Nix.
Conclusion: an oh-so-worthy experiment
My work on Nuenv hasn’t yielded a particularly robust Nix environment just yet but it’s been a fantastic learning experience about some of the lower-level nitty gritty of how Nix works. If you’re looking to level up your Nix knowledge, I strongly encourage you to take on a similar project (or hack on Nuenv!). You may just end up blazing a fruitful new trail for yourself and others in the Nix community—or with a renewed appreciation for the tried-and-true standard environment.
Finally, using Nushell in conjunction with Nix has frankly sparked joy in a way that trusty old Bash just doesn’t (for me, at least). While that isn’t a “engineeringly” justification for building something, there’s much to be said for working on things that compel a dramatic shift of energy and attention. We don’t have any concrete plans to take Nuenv much further at this time but we’d love to see how the community reacts.