18

I have read that if you want to use Bash in a portable way you should use the shebang:

#!/usr/bin/env bash

But now I am wondering: When I want to state explicitly that I do not rely on Bash, but instead, wrote a POSIX compliant script, should I use:

#!/bin/sh

Or is #!/usr/bin/env sh preferable here too?

JepZ
  • 1,159
  • 14
  • 27
  • 1
    Isn't `#!/usr/bin/env sh` counterproductive? If `/usr` is on separate partition, scripts with `#!/bin/sh` can be used even if `/usr` is not mounted, whereas `#!/usr/bin/env sh` would fail, even though `sh` is in `/bin`. I agree that's somewhat rare situation, but still. – el.pescado - нет войне Nov 02 '18 at 11:50
  • Indeed, for the default shell (like sh) that might be a problem, but for other shells (e.g., bash) it seems to be [necessary](https://stackoverflow.com/questions/10376206/what-is-the-preferred-bash-shebang#comment59287349_10376606). – JepZ Nov 02 '18 at 12:04
  • Even with `bash`, it stretches the definition of portable. All it says is "use whatever program named `bash` is first in your path". It doesn't say anything about what that version of `bash` should be (3.2? 4.0? 4.1? 4.2? 4.3? 4.4? 5.0?), or even that the program named `bash` even *is* Bash. Document what needs to run your script; let the user worry about how to specify that. – chepner Nov 02 '18 at 14:11

2 Answers2

20

Formal perspective

The informative section of the POSIX specification for sh: Application Usage states that you cannot rely on the sh executable being installed at /bin/sh.

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

However, instead of suggesting the use of env to use the appropriate PATH, it suggests that shell scripts should be modified at installation time to use the full path to sh:

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

In practice

I mostly write POSIX shell scripts and, in practice, every GNU/Linux system (Red Hat and Debian-based) – and others such as Cygwin and OS X – has a POSIX-compliant sh either installed to /bin/sh or available as a soft or hard link at this path. I’ve never needed to use env to cater for systems where sh does not use this path.

There may be some Unix systems where a POSIX-compliant sh is not available as /bin/sh. The POSIX specification suggests that it might be installed on some systems as /usr/xpg4/bin/sh. As I understand it, this is (was?) true for Solaris systems where /bin/sh is an earlier version of the Bourne shell which predates POSIX. In this case, using env sh would not be guaranteed to help as it could still find the Bourne shell (at /bin/sh) before the POSIX shell at /usr/xpg4/bin/sh.

Summary

If you’re writing POSIX shell scripts for common Unix and Linux operating systems, simply use #!/bin/sh as the shebang.

In rare cases where /bin/sh is a Bourne shell instead of a POSIX-compliant shell, you would have to modify the shebang to use the appropriate full path to the POSIX shell.

In either case, there’s no benefit to using #!/usr/bin/env sh – and would be more likely to fail than simply using #!/bin/sh.

Anthony Geoghegan
  • 11,533
  • 5
  • 49
  • 56
  • 1
    Okay, so your recommendation is to use installation tools (e.g., configure, make) when possible and stick with `#!/bin/sh` otherwise? – JepZ Nov 02 '18 at 11:19
  • 1
    @JepZ I've updated my answer. I hope it's clearer now. – Anthony Geoghegan Nov 02 '18 at 12:03
  • 2
    Wouldn't people who *want* the POSIX semantics on Solaris simply make sure `/usr/xpg4/bin` comes before `/usr` in their `PATH` anyway? – tripleee Nov 02 '18 at 12:09
  • @tripleee Good point. I've never personally used Solaris (only GNU/Linux, Cygwin and OS X) -- but if I had to, that's what I'd do. – Anthony Geoghegan Nov 02 '18 at 12:13
  • @JepZ That's what I would recommend. From the developer, the shebang is a *recommendation*; the *installer* knows where the correct interpreter lives on the local system. – chepner Nov 02 '18 at 12:59
  • Python installation tools will replace any shebang containing `python` (conventionally `#!python` as the minimal example) with whatever installer-specified path is correct and appropriate. (This handles both "non-standard" paths and the difference between Python 2 and Python 3, which may both be installed locally.) – chepner Nov 02 '18 at 13:00
  • It's better to use `#!/usr/bin/env sh`, to make sure that your shell is the same as the one you may invoke as `sh` from within your script. See my answer for a more detailed explanation. – kxmh42 Jun 30 '19 at 14:41
0

I would say #!/usr/bin/env sh is preferable.

Some portable constructs, e.g. the loop over the results of find, require sh to be called explicitly. Consider this script:

#!/bin/sh
# ... some commands here ...
find . -exec sh -c '
  for file do
    # ... commands processing "$file" ...
  done' find-sh {} +

The commands at the beginning will be run by /bin/sh, and the commands processing "$file" will be run by whatever sh that comes first in the PATH, which may behave differently than /bin/sh. This is a potential source of unexpected bugs. The #!/usr/bin/env sh shebang solves this problem, as all the commands will be run by the sh that is first in your PATH.

The only potential disadvantage of the #!/usr/bin/env sh shebang is the fact that /usr might not be mounted at the time of invoking the script. However, this shouldn't occur often in practice. External programs frequently used in portable scripts, such as awk, are also often found in /usr/bin, so it might be difficult to make sure the script runs correctly with /usr unmounted anyway.

If you really want to be portable and not depend on /usr being mounted, you can begin your script as follows, to make sure it is always executed by sh from the PATH, wherever it is:

#!/bin/sh
if test X"$SUBSHELL" != X"1"; then
  SUBSHELL=1
  export SUBSHELL
  exec sh "$0" "$@"
  exit 1
fi

# ... your actual script comes here ...

But it does seem to be a bit of an overkill, so I'd say the #!/usr/bin/env sh shebang is a reasonable compromise.

kxmh42
  • 3,121
  • 1
  • 25
  • 15