46
#! /bin/sh -

Is (or at least was) often the recommended shebang to have a script interpreted by /bin/sh (or #! /bin/bash - for bash, #! /bin/ksh - for ksh, etc).

Why not just #! /bin/sh or #!/bin/sh?

What's that - for?

1 Answers1

59

That's for a similar reason as why you need to write:

rm -- *.txt

And not

rm *.txt

Unless you can guarantee none of the .txt files in the current directory have a name that starts with -.

In:

rm <arg>

<arg> is considered an option if it starts with - or a file to remove otherwise. In

rm -- <arg>

<arg> is always considered as a file to remove regardless of whether it starts with - or not.

That's the same for sh.

When one executes a script that starts with

#! /bin/sh

Typically with:

execve("path/to/the-script", ["the-script", "arg"], [environ])

The system transforms it to:

execve("/bin/sh", ["/bin/sh", "path/to/the-script", "arg"], [environ])

The path/to/the-script is not usually something that is under the control of the script author. The author can't predict where copies of the script will be stored nor under what name. In particular, they can't guarantee that the path/to/the-script with which it is called won't start with - (or + which is also a problem with sh). That's why we need the - here to mark the end of options.

For instance, on my system zcat (like most other scripts actually) is one example of a script that didn't follow that recommendation:

$ head -n1 /bin/zcat
#!/bin/sh
$ mkdir +
$ ln -s /bin/zcat +/
$ +/zcat
/bin/sh: +/: invalid option
[...]

Now you may ask why #! /bin/sh - and not #! /bin/sh --?

While #! /bin/sh -- would work with POSIX shells, the #! /bin/sh - is more portable; in particular to ancient versions of sh. sh treating - as and end-of-option predates getopt() and the general use of -- to mark the end of options by a long time. The way the Bourne shell (from the late 70s) parsed its arguments, only the first argument was considered for options if it started with -. All the characters after - would be treated as option names; if there was no character after the -, there was no options. That stuck and all later Bourne-like shells recognise - as a way to mark the end of options.

In the Bourne shell (but not in modern Bourne-like shells¹), #! /bin/sh -eu would also work around the problem as only the first argument was considered for options.

Now, one could say that we're being pedantic here, and it's also why I wrote the need in italic above:

  1. nobody in their right mind would call a script with something starting with - or + or put them in a directory whose name starts with - or +.
  2. even if they did, first one would argue they can only blame themselves, but also, when you invoke a script, more often than not, that's from a shell or from execvp()/execlp()-type functions. And in that case, generally you invoke them either as the-script for it to be looked up in $PATH in which case the path argument to the execve() system call will typically start with / (not - nor +) or as ./the-script if you want the-script in the current directory to be run (and then the path starts with ./, not - nor + either).

Now beside the theoretical correctness issue, there's another reason why #! /bin/sh - became recommended as good practice. And that goes back to a time where several systems still supported setuid scripts.

If you have a script that contains:

#! /bin/sh
/bin/echo "I'm running as root"

And that script was setuid root (like with -r-sr-xr-x root bin permissions), on those systems, when executed by an ordinary user, the

execve("/bin/sh", ["/bin/sh", "path/to/the-script"], [environ])

would be done as root!

If the user created a symlink /tmp/-i -> path/to/the-script and executed it as -i, then it would start an interactive shell (/bin/sh -i) as root.

The - would work around that (it would not work around the race-condition issue, or the fact that some sh implementations like some ksh88-based ones would lookup script arguments without / in $PATH though).

Nowadays, hardly any system support setuid scripts any longer, and some of those that still do (usually not by default), end up doing a execve("/bin/sh", ["/bin/sh", "/dev/fd/<n>", arg]) (where <n> is a file descriptor open for reading on the script) which works around both this issue and the race condition.

Note that you get similar issues with most interpreters, not only Bourne-like shells. Non-Bourne-like shells generally don't support - as the end-of-option marker, but generally support -- instead (at least for modern versions).

Also

#! /usr/bin/awk -f
#! /usr/bin/sed -f

Don't have the issue as the next argument is considered as an argument to the -f option in any case, but it still doesn't work if path/to/script is - (in which case, with most sed/awk implementations, sed/awk don't read the code from the - file, but from stdin instead).

Also note that on most systems, one can't use:

#! /usr/bin/env sh -
#! /usr/bin/perl -w --

As on most systems the shebang mechanism allows only one argument after the interpreter path.

As to whether to use #! /bin/sh - vs #!/bin/sh -, that's just a matter of taste. I prefer the former as it makes the interpreter path more visible and makes mouse selection easier. There's a legend that says that the space was needed in some ancient Unix versions but AFAIK, that has never been verified.

A very good reference about the Unix shebang can be found at https://www.in-ulm.de/~mascheck/various/shebang


¹ Though with zsh, you can do #! /bin/zsh -eu- where the extra - there also marks the end of options.

  • 1
    Does this have any relevance to #!/bin/bash ? – Joe Mar 18 '17 at 08:16
  • 2
    @Joe, yes of course, bash accepts options like every other shell. Being a Bourne-like and POSIX shell, bash accepts both #! /bin/bash - and #! /bin/bash --. – Stéphane Chazelas Mar 19 '17 at 22:03
  • You have 158 posts containing #! /bin/sh (https://unix.stackexchange.com/search?q=user%3A22565+code%3A%22%23%21+%2Fbin%2Fsh%22) and 152 containing #! /bin/sh - (https://unix.stackexchange.com/search?q=user%3A22565+code%3A%22%23%21+%2Fbin%2Fsh+-%22). There are 6 posts that may be not using the-. – QuartzCristal Sep 07 '22 at 14:41
  • @QuartzCristal, that would not be critical if they did, but if you find some, please let me know and I'll fix them (I already fixed a handful earlier). It could be some like what is the meaning of this shell script function where the code is quoted from the question. And there are the #! /usr/bin/env bash ones which we can't do anything about portably as most systems don't allow more than on argument for the interpreter. – Stéphane Chazelas Sep 07 '22 at 14:46
  • No, of course that is not critical in most cases. I just though that you would like to know. – QuartzCristal Sep 07 '22 at 16:25
  • In context of the question, do you regard inability to use #! /usr/bin/env sh - as a valid point against the use of env in the shebang? – Christopher Mar 12 '24 at 11:23
  • @Christopher, it's a valid point, but in most contexts (maybe not for sh where these days you can expect a POSIX /bin/sh as much as you can expect a /usr/bin/env), when balanced against the possible benefits of using it (or drawback of not using it or using more clunky alternatives), it might have little weight. – Stéphane Chazelas Mar 12 '24 at 12:47