Shell Scripts: env-shebang with Arguments
What you need to pass arguments to an interpreter found by 'env'.
The Problem
There is an old annoyance that, if you use env
in a bang path to search the script interpreter in the shell's path, you cannot pass any arguments to it. Instead, all the text after the call to env
is passed as one single argument, and env
tries to find this as the executable to invoke, which fails of course when arguments are present.
env
is not the culprit here, but the very definition of how a bang path works (quoted from the bash
manpage):
If the program is a file beginning with
#!
, the remainder of the first line specifies an interpreter for the program. The shell executes the specified interpreter on operating systems that do not handle this executable format themselves. The arguments to the interpreter consist of a single optional argument following the interpreter name on the first line… (emphasis mine)
So what env gets to see in its argv
array when you write something like #! /usr/bin/env python3 -I -S
is ['/usr/bin/env', 'python3 -I -S']
. And there is no python3 -I -S
anywhere to be found that could interpret your script. 😞
The Solution
The env
command in coreutils 8.30 solves this (i.e. Debian Buster only so far, Ubuntu Bionic still has 8.28). The relevant change is introducing a split option (-S
), designed to handle that special case of getting all arguments mushed together into one.
In the example below, we want to pass the -I -S
options to Python on startup. They increase security of a script, by reducing the possible ways an attacker can insert their malicious code into your runtime environment, as you can see from the help text:
-I : isolate Python from the user's environment (implies -E and -s)
-E : ignore PYTHON* environment variables (such as PYTHONPATH)
-s : don't add user site directory to sys.path; also PYTHONNOUSERSITE
-S : don't imply 'import site' on initialization
You can try the following yourself using docker run --rm -it --entrypoint /bin/bash python:3-slim-buster
:
$ cat >isolated <<'.'
#!/usr/bin/env -S python3 -I -S
import sys
print('\n'.join(sys.path))
.
$ chmod +x isolated
$ ./isolated
/usr/local/lib/python38.zip
/usr/local/lib/python3.8
/usr/local/lib/python3.8/lib-dynload
Normally, the Python path would include both the current working directory (/
in this case) as well as site packages (/usr/local/lib/python3.8/site-packages
).
However, we prevented their inclusion as a source of unanticipated code – and you can be a happy cat again. 😻