Shell Scripts: env-shebang with Arguments

jhermann

Jürgen Hermann

Posted on March 10, 2020

Shell Scripts: env-shebang with Arguments

How 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. 😻

This post originally appeared on my personal blog.

💖 💪 🙅 🚩
jhermann
Jürgen Hermann

Posted on March 10, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related