First the commands and then we will discuss the why and how.
# Usage:
# nix-exec PKG CMD [ARG]...
nix-exec() {
local thepkg="$1"
shift
local thecmd="$1"
shift
nix-shell -p "$thepkg" --run "exit 0"
if [ "$?" != "0" ]; then
return
fi
local pkgpath=$(nix-instantiate --eval-only --expr "(import <nixpkgs> {}).$thepkg" --raw)
"$pkgpath"/bin/"$thecmd" "$@"
}
# Usage:
# nix-run -- CMD [ARG]...
# nix-run PKG CMD [ARG]...
nix-run() {
local thepkg="$1"
shift
if [ "$thepkg" = "--" ]; then
thepkg="$1"
shift
nix-exec "$thepkg" "$thepkg" "$@"
return
fi
local thecmd="$1"
shift
nix-exec "$thepkg" "$thecmd" "$@"
}
Many users of NixOS will be familiar with the venerable nix-shell command. For anyone unaware, this is a utility that comes with the Nix
package manager that allows you to, among other features, create a temporary shell with any package from nixpkgs in PATH without installing it into your environment.
This is fantastic for calling one-off programs that you may not use often or for keeping your main environment slim.
nix-shell is primarily meant to be used interactively, eg. you call nix-shell (or eg nix-shell -p sqlitebrowser) and this calls bash with the right arguments
starting a new session. However I found when scripting that this was not always ideal. nix-shell offers a --run parameter to execute a command non-interactively
in the newly created session and then exit but this gave me problems which I will now demonstrate. This issue is what prevented me from publishing this earlier.
Here’s an earlier version of this command:
nix-run() {
local thecmd="$1"
shift
nix-shell -p "$thecmd" --run "$thecmd $@"
}
Now if you run this with say nix-run echo a b c then you get an error like so:
$ nix-run echo a b c
error:
<snip>
error: undefined variable 'echo'
at «string»:1:107:
1| {...}@args: with import <nixpkgs> args; (pkgs.runCommandCC or pkgs.runCommand) "shell" { buildInputs = [ (echo) (b) (c) ]; } ""
| ^
See how it’s trying to evaluate b and c as packages instead of arguments to echo? Well now that I was writing out this blog post I realized a version I could’ve
been using all along but it may have one more issue I was concerned about…
nix-run() {
local thecmd="$1"
local allofthem="$*"
shift
nix-shell -p "$thecmd" --run "$allofthem"
}
Now, $allofthem captures all the arguments into a single string before the array is separated and we get the error we were expecting:
error:
<snip>
error: undefined variable 'echo'
at «string»:1:107:
1| {...}@args: with import <nixpkgs> args; (pkgs.runCommandCC or pkgs.runCommand) "shell" { buildInputs = [ (echo) ]; } ""
| ^
This is great but as I hinted at earlier I like to use this while scripting. Consider the case where the argument we want to pass is not a literal. I use a compiled-from-source
version of Zigmod so perhaps I want to run that in GDB and run nix-run gdb $(which zigmod). With the version we just defined this unfortunately does not work:
error: '--run' requires an argument
Try 'nix-shell --help' for more information.
So now we’re in a good place to get to what I landed on.
# Usage:
# nix-run -- CMD [ARG]...
# nix-run PKG CMD [ARG]...
nix-run() {
This is an expanded version that also accepts a use-case where the name of the package doesn’t match the name of the binary. The general idea for this command is
inspired by npx, bunx, uvx, etc but
allows querying nixpkgs instead.
local thepkg="$1"
shift
This grabs the first parameter but at this point we’re not sure what we are going to do with it yet.
if [ "$thepkg" = "--" ]; then
thepkg="$1"
shift
nix-exec "$thepkg" "$thepkg" "$@"
return
fi
This section allows inferring the package name from the command name and is often the happy path, so we capture that name and call nix-exec. That command has
the meat of the detail here but always expects the package and command name.
local thecmd="$1"
shift
nix-exec "$thepkg" "$thecmd" "$@"
}
This section is the fallback that assumes you passed both names. Using the quoted "$@" ensures the arguments are passed to argv the same way the user did. Conversely,
$@ would have bash re-evaluate them and ruining quotation and "$*" would capture all arguments into a single string.
# Usage:
# nix-exec PKG CMD [ARG]...
nix-exec() {
local thepkg="$1"
shift
local thecmd="$1"
shift
nix-run is now a wrapper around this so we can be cleaner about our assumptions around how the arguments are passed. For this one a package name and command name
will always be present so we can grab those now.
nix-shell -p "$thepkg" --run "exit 0"
if [ "$?" != "0" ]; then
return
fi
We still rely on nix-shell to populate the Nix Store and show any errors to the user around a package not existing etc. Make sure to bail out if Nix did.
local pkgpath=$(nix-instantiate --eval-only --expr "(import <nixpkgs> {}).$thepkg" --raw)
"$pkgpath"/bin/"$thecmd" "$@"
}
Now the magic sauce of this version of the command is that we use nix-shell to populate the store but call the command outside. nix-instantiate returns the
absolute path of the package and then we call it with the rest of the arguments from earlier.
All together it works exactly as we desired!
$ nix-run -- gdb `which zigmod`
GNU gdb (GDB) 16.3
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-unknown-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /home/meghan/dev/zigmod/zig-out/bin/zigmod...
(gdb)
After landing on the version above and telling some friends I was introduced to https://github.com/nix-community/comma. This is a great utility as well and I will definitely be adding it to my repertoire.
Happy coding!