26
#! /bin/sh -

Is (or at least was) often the recommended shebang to have a script interpreted by /bin/sh.

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

What's that - for?

35

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 -euf 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 script 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

  • 1
    Does this have any relevance to #!/bin/bash ? – Joe Mar 18 '17 at 8:16
  • 1
    @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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.