Flake equivalent for nix-shell --command on mkShell



I enter well-defined temporary shells on a very regular basis. My use cases include data analysis, (distributed) software integration testing, sharing shells with other people and running computer science experiments. I’ve been doing it with non-flaky Nix for years by:

  • defining shells in Nix files in some repo with something like {myshell = mkShell ...;}
  • entering shells with nix-shell ... -A myshell --command ...

I am currently trying to transition into Nix flakes for most of my use cases. I am mostly interested in the significant performance improvements that evaluation caching enables on large/complex expressions (e.g., texlive) or when running lots of short-lived processes in isolated environments. I also think that the better composition properties and saner default behaviors of flakes (purity, version traceability…) are worth it in the long run.


How should shells be defined and entered with flakes?

I’m used to use the same command for different use cases, and I feel that the answer may depend on interactive vs non-interactive needs, and on the traceability level needs.

Solution attempts & discussion

I am currently defining shells with mkShell into the devShells outputs of my flakes, and using nix develop to enter each shell with a command such as nix develop ...#myshell --command zsh. I however feel like nix develop is not intended for this use case and got several problems with it (listed at the end of this post), and I wonder if other commands would be better. I feel like both nix shell and nix run would be worse, but I’m new to flakes and probably misunderstood commands or missed one of them.

It seems that nix shell is not intended to use shells defined in Nix files, but to use shells defined on command-line arguments. It could be fine for some of my use cases — e.g., to quickly enter a shell with R and a given list of packages to analyze some data. But this seems very bad for sharing environments, I typically want to provide my students shells that enable to do some tasks, such as debugging a specific software interaction. This also seems very bad IMO for defining reproducible/traceable execution pipelines in the context of scientific experiments, but I may be completely wrong about this?

nix run seems usable by creating fake/wrapper applications that run shells. This seems tedious to do but maybe worth it in contexts with a high traceability need. I am also afraid that such solutions would be very fragile and break with any nix flake update.

Finally, I have two main concerns about nix develop for this use case. The first is how to define a default shell with possible command overriding. I’ve been using aliases such as ns = nix-shell --command zsh for a long time to use zsh by default in my interactive shells without hacking how nix-build builds derivations. The very convenient part of this alias is that it still enables me to use bash when I want/need to, or to use any other interactive shell (ipython, R…) with calls such as ns ... --command ipython. This is because nix-shell accepts several --command options and uses the latest defined one. Is there a way to achieve something like this with flakes? Ideally, I would love to be able to define the default command of each shell. I tried hacking mkShell's shellHook for this purpose but I could not find how to enable this with an overridable shellHooks without ugly hacks, and this solution keeps a useless bash process that runs the shell you want (my nix-shell alias setup also suffers from this). Wrapping nix develop's CLI could be done for this purpose, but I feel like improving nix develop's CLI would be better than creating yet another wrapping tool with a use case as common as choosing which shell program to work with.

The second concern may actually be a nix develop bug? nix develop pollutes my system temporary directory by creating a directory for each shell I enter. I think that this directory is the temporary directory used when building derivations, as its path corresponds to the following environment variables: NIX_BUILD_TOP, TEMP, TEMPDIR and TMP. As I feel that nix develop has been made as a convenient (interactive) tool to debug the building of derivations, this behavior seems fine by default. The main problem I have with this is that the temporary directory is not cleaned when I leave the shell. This seems related to the keep-failed Nix daemon option but the option is not set in my NixOS configuration and should be false by default, and adding either --no-keep-failed or --option keep-failed false CLI options to nix develop did not fix the issue :/. I got the issue with Nix 2.11.0 and 2.12.0 and I am using NixOS 22.11.

I think your assertion that nix shell is meant for ad-hoc environments is correct, and that nix run would be a very tedious solution.

I would probably use nix develop for this purpose, but that also has its downsides as you mention. I think it tries to serve two purposes: creating sharable development environments, and creating shells for devlopping/debugging nix derivations. Both useful, but IMO they should not be handled by the same command. Don’t know if it has anything to do with the directories in your tmp folder, though.

About the shell alias, maybe something like this shell function would work? Or is there something else I’m missing?

# Use like `ns ...#myshell [ZSH_ARGS...]`
ns() (
  shift 1
  exec nix develop "$flakeref" --command zsh "$@"