. / c u r s e d . n i x

Edit: Want to execute a .nix like “./whatever”?
Use this and add parameters to the inner arguments. There must always be at least one.

#! /usr/bin/env nix-shell
#! nix-shell -i "nix-shell -v" -p ""

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

This was edited nonlinearly and may contain inconsistencies.

To my massive irritation, calling nix-shell on a nix file that isn’t in $PWD fails:

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN/lol]$ ../uncursed.nix 
evaluating file '/nix/store/ppgcg2ns9sqq5dqzm05s6x8xddmb2q2l-nix-2.3.10/share/nix/corepkgs/derivation.nix'
error: getting status of '/run/user/1000/tmp.6T3BRBy0aN/default.nix': No such file or directory

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN/lol]$ cat ../uncursed.nix 
#! /usr/bin/env nix-shell
#! nix-shell -v
{pkgs ? (import (import ./nix/sources.nix).nixpkgs {}) }: {
  devShell = pkgs.mkShell {
    buildInputs = with pkgs; [ git ];
    };
  }

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN/lol]$ nix-shell ../uncursed.nix 
evaluating file '/nix/store/ppgcg2ns9sqq5dqzm05s6x8xddmb2q2l-nix-2.3.10/share/nix/corepkgs/derivation.nix'
error: getting status of '/run/user/1000/tmp.6T3BRBy0aN/default.nix': No such file or directory

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN/lol]$ cd ..

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN]$ nix-shell uncursed.nix 
evaluating file '/nix/store/ppgcg2ns9sqq5dqzm05s6x8xddmb2q2l-nix-2.3.10/share/nix/corepkgs/derivation.nix'
error: getting status of '/run/user/1000/tmp.6T3BRBy0aN/default.nix': No such file or directory

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN]$ ls
cursed.nix  lol  nix  uncursed.nix

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN]$ mv uncursed.nix shell.nix

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN]$ nix-shell shell.nix 
evaluating file '/nix/store/ppgcg2ns9sqq5dqzm05s6x8xddmb2q2l-nix-2.3.10/share/nix/corepkgs/derivation.nix'
evaluating file '/run/user/1000/tmp.6T3BRBy0aN/nix/sources.nix'
evaluating file '/nix/store/rd5ha76r9bia75p3b8znxkll84qsxdp6-source/default.nix'
evaluating file '/nix/store/rd5ha76r9bia75p3b8znxkll84qsxdp6-source/lib/minver.nix'
...

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN]$ cd lol/

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN/lol]$ cat ../cursed.nix 
#! /usr/bin/env nix-shell
#! nix-shell -i "bash -c 'set -x; fr=$(realpath \"$1\"); dir=$(dirname \"$fr\"); fname=$(basename \"$fr\"); shift; pushd \"$dir\"; nix-shell -v \"$fname\" \"$@\";' -- " -v -p ""
# The shebang does work just maybe not in the way you/I think?
{pkgs ? (import (import ./nix/sources.nix).nixpkgs {}) }: {
  devShell = pkgs.mkShell {
    buildInputs = with pkgs; [ git ];
    };
  }

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN/lol]$ ../cursed.nix 
evaluating file '/nix/store/ppgcg2ns9sqq5dqzm05s6x8xddmb2q2l-nix-2.3.10/share/nix/corepkgs/derivation.nix'
evaluating file '/nix/store/b44y8fqa9523hcs3krlq4qarsw6nqgxy-nixos-21.05pre275822.916ee862e87/nixos/default.nix'
evaluating file '/nix/store/b44y8fqa9523hcs3krlq4qarsw6nqgxy-nixos-21.05pre275822.916ee862e87/nixos/lib/minver.nix'
...

evaluating file '/nix/store/b44y8fqa9523hcs3krlq4qarsw6nqgxy-nixos-21.05pre275822.916ee862e87/nixos/pkgs/development/libraries/ncurses/default.nix'
+ f ../cursed.nix                                                                                                                                                                                                                             
++ realpath ../cursed.nix                                                                                              
+ fr=/run/user/1000/tmp.6T3BRBy0aN/cursed.nix                                                                          
++ dirname /run/user/1000/tmp.6T3BRBy0aN/cursed.nix                                                                    
+ dir=/run/user/1000/tmp.6T3BRBy0aN
++ basename /run/user/1000/tmp.6T3BRBy0aN/cursed.nix
+ fname=cursed.nix
+ shift
+ pushd /run/user/1000/tmp.6T3BRBy0aN
/run/user/1000/tmp.6T3BRBy0aN /run/user/1000/tmp.6T3BRBy0aN/lol
+ nix-shell -v cursed.nix
evaluating file '/nix/store/ppgcg2ns9sqq5dqzm05s6x8xddmb2q2l-nix-2.3.10/share/nix/corepkgs/derivation.nix'
...

I probably messed up and misdiagnosed something and broke something else and misunderstood a third thing, but here goes:

https://github.com/NixOS/nix/blob/8803753666023882515404177b08f3f8bdad52a0/src/nix-build/nix-build.cc#L250 means we can’t have nice things because is searches for shell.nix or default.nix in $PWD or however the heck lstat() works (because pathExists() calls lstat()).

To start remedying our relative path problem, we see we can use -p with a dummy value to trigger “stop searching in my current directory kthx” mode.

Since the pathExists() checks are after the exec()s, we can pull off the rest of our chain.

Next we need to actually interpret our nix file somehow.

Since the exec() uses a simple string substitution, since shebang mode is enabled when the nested nix is called with our executable shebanged file as its first argument, we can call arbitrary code through the -i interpreter argument (thanks @grahamc ), which is only enabled in shebang mode (see heuristic).

To execute arbitrary code but be able to consume our arguments as well, we use a bash expression:

"bash -c '<arbitrary code here>; nix-shell -v \"$filename\" \"$@\"  ' -- "

To prevent the shebang being interpreted again we pass -v as a noop-ish item to push the path out of the first-argument position then we pass the filename and remaining arguments. The -- at the end means the arguments the parent nix-shell passes at the end of the interpreter string are all arguments to the script/-c we pass to bash.

An alternative approach @sternenseemann and @lukegb recognized involves naming the file such that it contains the substring nix-shell (“Why did my files suddenly stop working when I started naming them with nix-shell?”). This however results in the shebang not being processed.

If there is a will there is a way.
Und ich will nicht upstream patchen for things that should work.

3 Likes

To summarize, you can use this very nice shebang set if you want to ./ your nix expressions:

#! /usr/bin/env nix-shell
#! nix-shell -i "bash -c 'set -x; fr=$(realpath \"$1\"); dir=$(dirname \"$fr\"); fname=$(basename \"$fr\"); shift; pushd \"$dir\"; nix-shell -v \"$fname\" \"$@\";' -- " -v -p ""

Improvements welcome.

It would be nice to get rid of the mandatory -v.

Someone is going to tell me I missed something obvious and this was all a horrible waste of time.

1 Like

To further illustrate my confusion, it’s entirely sufficient to just pass the realpath, which yields this much shorter, nicer expression:

#! nix-shell -i "bash -c 'fr=$(realpath \"$1\"); shift; nix-shell -v \"$fr\" \"$@\";' -- " -v -p ""

Apparently it’s sufficient to deactivate the shebang heuristic:

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN/lol]$ nix-shell ../cursed2.nix 
error: getting status of '/run/user/1000/tmp.6T3BRBy0aN/default.nix': No such file or directory

[nix-shell:/run/user/1000/tmp.6T3BRBy0aN/lol]$ nix-shell -v ../cursed2.nix 
evaluating file '/nix/store/ppgcg2ns9sqq5dqzm05s6x8xddmb2q2l-nix-2.3.10/share/nix/corepkgs/derivation.nix'
evaluating file '/run/user/1000/tmp.6T3BRBy0aN/nix/sources.nix'
evaluating file '/nix/store/rd5ha76r9bia75p3b8znxkll84qsxdp6-source/default.nix'
evaluating file '/nix/store/rd5ha76r9bia75p3b8znxkll84qsxdp6-source/lib/minver.nix'
evaluating file '/nix/store/rd5ha76r9bia75p3b8znxkll84qsxdp6-source/pkgs/top-level/impure.nix'

So, this is sufficient:

#! nix-shell -i "nix-shell -v" -p ""

Have your shebang and eat it too.

I should sleep on this. I definitely misdiagnosed some of it. The checks / path changes only happen if left is empty, so preumably the activation of the shebang heuristic messes with that.

Ok…

#! nix-shell -i "nix-shell" -p ""

might also be enough?

Either I’m shooting myself in the foot here with something or all that matters is passing -p and the first time I tried this, something else was wrong.

A fun think I realized/discovered is technically you can pass arguments, so combined with the bash trick you could even do extra argument parsing?

As it is, something like ./cursed.nix -A whatever seems to work just fine.

None of this particularly surpising, just you know, not exactly supported out of the box.

Not a full example, but here you can see me using it with a module, combined with getting nix to build a dependency needed in the shebang line:

#! /usr/bin/env nix-shell
#! nix-shell -i "bash -c '$(nix-build $EXTRA_NIX_ARGS -A config.shebang --no-out-link \"$1\")/bin/extra-container ${CNT_MODE:-create --start} \"$@\"; sudo cp /etc/systemd-mutable/system/container@nest.service /run/systemd/system/' --" -p""
# e.g. EXTRA_NIX_ARGS="-v --show-trace" ./nest.nix --build-args -v
# CNT_MODE="destroy nest" ./nest.nix  
{lib ? (import (import ../nix/sources.nix {}).nixpkgs {}).lib, ...}: {
  options.shebang = lib.mkOption {
    type = lib.types.anything;
    };

  config = {
    # This could be turned into a script and most of the shebang moved here, to clean things up a bit..
    shebang = with (import (import ../nix/sources.nix).nixpkgs) {}; import ../lib/extra-container.nix { inherit pkgs; };

...

Another interesting things to do is to pass things via -I, e.g. pulling a tarball off github (with the usual caveats involved with unpinned code).

Interestingly, maintainers/scripts/update.nix already wants to be executed using nix-shell, but doesn’t have a shebang, most likely because whoever wrote it gave up quicker than you.

Bonus one line she bang (depends on GNU env): #!/usr/bin/env -S nix-shell -v

2 Likes