Shebang locations

hey all,
I’ve installed nixos KDE, and it’s so far wonderful, especially the nix package manager!
My issue is, do any nixos users know how to adjust the shebang environment paths globally so that when I script in bash, the shebang calls it up from the standard bash location:
example:
The standard shebang placement in scripts is

#!/bin/bash

but on my nixos it is

#!/run/current-system/sw/bin/bash

Is there anything I need to put in my configuration.nix to point the bash in the right direction?

Would this be the right approach to placing the following into my configuration.nix

environment.etc.“default.nix” = ‘’
export PATH=“$PATH:/run/current-system/sw/bin”
‘’;

followed by:

sudo nixos-rebuild switch

I need this to be configured back to normal, as I write a lot of scripts and share them.

Thanks in advance

1 Like

The best way to write portable scripts is to use #!/usr/bin/env bash so you can be sure it will work if the person has the program in their PATH.

A limitation of that is when you need to pass parameters to the program in the shebang, because env won’t pass it. #!/usr/bin/env bash -foobar will discard -foobar. This isn’t an issue with bash, but some other runtime (perl for example) may benefit from useful flags :confused:

8 Likes

I also take the opportunity to mention it’s possible to use nix-shell has a shebang to make reproducible scripts :smiley: (but users need to have Nix installed), but the feature is pretty cool!

https://nix.dev/tutorials/first-steps/reproducible-scripts

2 Likes

Annoyingly, that also changes the file type for file. It’s not env-aware.

POSIX in fact reccomends that you change your script’s shebang for every system you install it on:

Applications should note that the standard PATH to the shell cannot be assumed to be either /bin/sh or /usr/bin/sh, and should be determined by interrogation of the PATH returned by getconf PATH , ensuring that the returned pathname is an absolute pathname and not a shell built-in.

For example, to determine the location of the standard sh utility:

command -v sh

On some implementations this might return:

/usr/xpg4/bin/sh

Furthermore, on systems that support executable scripts (the “#!” construct), it is recommended that applications using executable scripts install them using getconf -v to determine the shell pathname and update the “#!” script appropriately as it is being installed (for example, with sed).

Which is contrary to the myth that’s often repeated when this comes up that /bin/sh is fine because sh is mandated to be available according to POSIX. Though /bin/sh will actually work on NixOS.

That means strictly, the “correct” fix is to install the scripts using buildShellApplication, writeShellScriptBin or a stdenv.mkDerivation, as all of those will replace the shebang.

Alternatively you can also symlink /bin/bash into place, of course… Here’s the development discussion on that: Add /bin/bash to avoid unnecessary pain - #3 by bhipple

To explain the argument of “purity” a bit better, the fear is that, if a package executes /bin/bash accidentally at build time, this could result in build failures due to bash version changes. Since nix doesn’t sandbox the build env on all supported platforms this is a real concern. That said, NixOS does sandbox the build env, and if you install nix on other platforms this can’t be prevented anyway, so maybe it’s a moot point nowadays?

1 Like

IIRC patchShebangs will only fix shebangs when an executable with that name is on PATH, yeah? resholve (resholve.{mkDerivation,writeScript,writeScriptBin}) requires explicitly specifying the interpreter. I did it for a few reasons, but one is to nudge the packager to check the upstream instead of letting it do implicit things like pick up ${bash}/bin/sh (it might be picky about what ‘sh’ it gets) or quietly do nothing if the shebang was /bin/zsh and zsh isn’t on PATH.

1 Like

When it comes to making my scripts compatible and portable, I’ve encountered the need to add additional lines at the beginning of each script. This requirement can be quite frustrating, and it makes me wonder about the reasoning behind this decision by the NixOS team.

To test the behavior, I ran the script on my Nobara 37 system, and it executed without any issues. However, since I’m currently away from my NixOS setup, I can’t guarantee that it will behave the same way there. It’s possible that omitting the #!/bin/sh shebang at the beginning could result in errors as the system attempts to move on to the next available interpreter. In fact, when I tried using #!/usr/bin/env python as the shebang on Nobara, the script crashed.

Based on my experience, it appears that using #!/bin/sh as the shebang provides a higher level of compatibility across various linux distributions.

#!/bin/bash
#!/bin/sh
#!/run/current-system/sw/bin/bash
#!/usr/bin/env bash
#!/usr/bin/env python
#!/usr/bin/env sh
#!/usr/bin/python

#*Check if running in NixOS environment
if [ -f /run/current-system/sw/bin/sh ]; then
  echo -e "\033[1;34mRunning in NixOS environment\033[0m"
else
     echo -e "\e[31m   ERR\e[0m: Non NixOs system"
 fi

  echo -e "\033[1;34m   Hello Worldt\033[0m"

I think you’re misunderstanding some things.

An “interpreter” is a specific program that interprets (i.e., reads and parses a file, and statement-by-statement executes some precompiled machine code that is part of the interpreter’s binary - different from natively compiled executables as they directly contain machine code, and don’t need a separate binary to contain machine code) a specific language.

There are lots of different interpreters, and they all implement different languages. The ones you mention here are:

  • /bin/sh is generally a POSIX shell implementation, which can interpret a very specific version of the general family of shell scripting languages, as specified by the POSIX standard.
  • /bin/bash is usually an interpreter for a non-POSIX-compliant shell scripting language.
  • /usr/bin/python is usually an interpreter for the python language, which isn’t a shell scripting language at all and won’t know what to do with a shell script whatsoever.

Generally, interpreters are completely incompatible - one interpreter won’t understand a script written for another interpreter at all. Other interpreters include

  • nodejs
  • php
  • ruby
  • perl
  • awk

The reason this may be confusing is that the shell scripting family of languages (POSIX sh, bash, zsh, ksh, …) are somewhat compatible to different degrees - however, bash has certain extensions on top of POSIX sh that won’t work in any of the others, zsh has extensions that won’t work in any of the others, and they all have subtle incompatibilities with POSIX sh. fish, powershell and nushell on the other hand are completely incompatible with POSIX.

Even between different versions of the same interpreter there may be incompatibilities.

That means these are not equivalent at all:

Even if all the interpreters were equivalent to one another, that won’t work; the way the kernel runs an executable file is to check the first two characters, and if they are #! it will interpret everything until the next newline as the “interpreter”, which it will execute and pass the rest of the file. Your script will therefore always be executed by /bin/bash, which will ignore all the subsequent lines since they are comments to bash.

There’s simply no concept of a “next available interpreter”. You need to get the interpreter right first try.

@Solene 's pragmatic solution is:

#!/usr/bin/env bash

#*Check if running in NixOS environment
if [ -f /run/current-system/sw/bin/sh ]; then
  echo -e "\033[1;34mRunning in NixOS environment\033[0m"
else
     echo -e "\e[31m   ERR\e[0m: Non NixOs system"
 fi

  echo -e "\033[1;34m   Hello Worldt\033[0m"

env is not an interpreter. Instead, it’s a little program that manipulates the environment before it executes another program. If you just give it a single argument, it will look said argument up in $PATH, and execute the first binary with that name it finds, and in this case pass on the rest of the file to that binary.

I.e., that first line means “check where the bash binary is on this system, and run it to interpret this script”.

This is the most portable way of executing bash - it should work on all modern-ish systems, as this behavior of /usr/bin/env has been a standard for a while and is part of the POSIX spec.

Ironically, it’s also completely impure, so I understand why people are concerned about the whole impurity of bash being in /bin thing less and less.

This is kind of true, actually. Since sh generally refers to the POSIX implementation of the shell language, it tends to be executable by most shell scripting interpreters. Furthermore, any Unix-derivative worth its salt tends to implement POSIX, and therefore has an sh somewhere. It is not guaranteed to be in /bin/sh, as this is not part of the spec, but it generally will be.

So using /bin/sh instead of /bin/bash is a pretty safe bet for compatibility, assuming you don’t actually need bash extensions.

That said, if you want to do it 100% correctly you should still modify the shebang on every system you deploy it to (by manually opening the file, and editing the sh location to whatever command -v sh gives you). NixOS has some utility functions available for doing this automatically.


If all of this seems inane, please don’t blame NixOS. It’s the POSIX developer’s fault, but it’s tough going up against standards older than most a lot of (apparently only 35, who’d-a thunk) people on this forum.

35 years ago opening every script you install and editing its first line (or running sed on it) was probably more acceptable.

2 Likes

Wait what?

#!/usr/bin/env bash -x

# foo.sh

echo foo
echo bar
$ ./foo.sh
+ echo foo
foo
+ echo bar
bar

It totally passes parameters. The lines beginning with + are due to the -x argument.

Sorry for asking but are you using ChatGPT to write this?

This is not really POSIX though, if you want your scripts to be portable, you shouldn’t pass parameters.
Here is your script on OpenBSD 7.3

solene@t470 ~> uname -a
OpenBSD t470.perso.pw 7.3 GENERIC.MP#1125 amd64

solene@t470 ~> cat test.sh
#!/usr/bin/env bash -x

echo foo
echo bar

solene@t470 ~> ./test.sh
env: bash -x: No such file or directory

To add to this, GNU’s implementation of env has the -S flag as an attempt to solve this: env invocation (GNU Coreutils 9.4).

This still isn’t actually portable though unless you know env will be GNU compatible.

3 Likes

Very interesting. Thanks for the share.

thanks heaps for the share!

Another angle to this is to set services.envfs.enable = true; in your NixOS config. That mounts a FUSE filesystem on /bin that makes things available in your $PATH.

This is quite convenient if you are not in a position to edit the original program, or want to get an environment that is closer to Ubuntu.

More details at GitHub - Mic92/envfs: Fuse filesystem that returns symlinks to executables based on the PATH of the requesting process.

See also GitHub - Mic92/nix-ld: Run unpatched dynamic binaries on NixOS to avoid having to patch binary programs.

4 Likes

Looks like a cool feature! It sounds like an abomination with regard to correctness, but very useful when you need it :smiley:

1 Like

Bringing back the Ubuntu experience also helps new users quite a bit. Otherwise, they have this huge wall that comes up while doing things that used to be mundane tasks. But yeah, it hurts our pure hearts :slight_smile:

2 Likes

Another angle to this is to set services.envfs.enable = true; in your NixOS config. That mounts a FUSE filesystem on /bin that makes things available in your $PATH.

Thank you kindly! It made NixOS feel a lot more at home for me, even though I fully understand that NixOS is all about a pure functional approach and adherence to POSIX standards.

NixOS doesn’t respect the standard FHS so I wouldn’t say it’s POSIX.

NixOS is about being pure, not really being POSIX.

1 Like

Thank you for elaborating. What I meant to say is their scripting philosophy.

Of course NixOS is POSIX!

Pure Operating System Interface eXperience

POSIX is not POSIX :smiley:

5 Likes