Figure 1: Jamian · CC BY 3.0 Deed (link)

Figure 1: Jamian · CC BY 3.0 Deed (link)

A frequent quip of the unix-beard is shebangs cannot contain multiple command-line arguments. Let’s break it down and see where this assumption no longer holds true.

§What is a Shebang?

The shebang is the line at the beginning scripts such as Python and Shell scripts that instructs the OS how to execute the script. Looks something like #!/bin/sh or #!/usr/bin/env python. This line specifies how to interpret the file. This progam specified on that line is also known as an interpreter. Without the line, the OS wouldn’t know what to do. By default, shebang-less scripts are executed with the same program as the parent process. One might experience this if saving a Perl script without a shebang, then executing it. Your shell will likely try to run it as a shell script.

This is why a shebang is essential to most scripts. Without one, there is no guarentee it will run with the correct interpreter besides manipulating the parent process to choose a specified interpreter.

Wikipedia has an excellent page on the shebang. The execve(2) manpage (try running man 2 execve) explains the shebang in further detail.

§An example… in Perl?

Consider this short Perl script. It prints the first line and last line, or just the first line, or no lines. This script uses perl -n to wrap the body of the program in while(<>) { ... } loop. In short, perl -n reads each line, then executes the body. Since I need the -n argument on the command line, I tried this first:

#!/usr/bin/env perl -n
# Why won't this work :-o
print if ++$lines == 1;
print if eof() && $lines > 1;

If I save it and run it, I get the following error output:

$ /tmp/  < /etc/passwd
/usr/bin/env: ‘perl -n’: No such file or directory
/usr/bin/env: use -[v]S to pass options in shebang lines

GNU coreutils env documentation offers insight:

The -S/--split-string option enables use of multiple arguments on the first line of scripts (the shebang line, ‘#!’). … Most operating systems (e.g. GNU/Linux, BSDs) treat all text after the first space as a single argument. When using env in a script it is thus not possible to specify multiple arguments.

Alright, ok. TIL about this -S flag, neat! If I change the shebang line to #!/usr/bin/env -Sperl -n, it works:

$ /tmp/  < /etc/passwd
root:x:0:0:System administrator:/root:/run/current-system/sw/bin/bash
nobody:x:65534:65534:Unprivileged account (don't use!):/var/empty:/run/current-system/sw/bin/nologin

Viola! Passing arguments in the shebang without any funny hacks!

If I add the suggested -v flag (#!/usr/bin/env -vSperl -n), env prints helpful debug information before executing the script:

split -S:  ‘perl -n’
 into:    ‘perl’
     &    ‘-n’
executing: perl
   arg[0]= ‘perl’
   arg[1]= ‘-n’
   arg[2]= ‘/tmp/’

It’s all documented in the coreutils documentation available online or via info coreutils env in your terminal.

§Where can I use this feature?

Any modern OS that ships env from coreutils including the majority of popular desktop/server Linux distros. If you’re a Mac user you’re also in luck, thanks to FreeBSD’s downstream env implementation supporting -S too.

POSIX env does not offer this -S feature. OpenBSD, NetBSD, Busybox, Toybox are also without -S. Busybox ships on some Linux distros such as Alpine. My first step when using these hosts is to install software to reduce the friction of productivity including coreutils. Coreutils is my portability strategy for env -S.

§Can’t use env -S or need POSIX compliance?

If one cannot use env -S, I believe the time-tested approach is to write a wrapper program for each tool that needs specified arguments on the shebang line. With the above example, I could put this script in my $PATH and name it perlloop:

exec perl -n "$@"

Next, I can update the shebang from the above example to #!/usr/bin/env perlloop. Then it’ll work the same

§Why not just use #!/usr/bin/perlloop?

Works too if that’s where perlloop lives. You probably don’t care where it lives though. Just want it to find the matching program in your PATH. That is why I prefer using #!/usr/bin/env .... See the coreutils env documentation for additional discussion on the tradeoffs.

§Emacs and Shebangs

My Emacs setup checks for a shebang, and ensures the file is executable on save. Check it out here. I suggest adding a hook to your text editor to ensure your scripts are executable on save. It saves a lot of headache.

I also wrote an experimental feature to auto-matically changes the syntax highlighting and major mode when the shebang changes in the Emacs buffer. The result is not fiddling about with distracting stuff like re-opening the file or using M-: (auto-mode) RET.

Shebangs are pretty cool.