Arvids Blog

Thoughts on programming and more

Single file scripts using `uv`

uv is a package manager for Python that’s been gaining some traction for the past year or so.

As already blogged/written about many times, uv can be used to write single-file Python scripts without having to worry about virtual environments.

However, they all suggest using #!/usr/bin/env -S as a shebang to invoke the script with uv run --script like this:

#!/usr/bin/env -S uv run --script

Unfortunately, this depends on a reasonably new version of /usr/bin/env. The -S flag was introduced in version 8.30 (published 2018-07-01) of the GNU coreutils. For many of us on enterprise Linux distributions (e.g., RHEL 9, or Amazon Linux 2) this version is too new.

But fear not, there are ways to achieve the same end results without having to update to an unsupported version of GNU coreutils.

tl;dr: Use this instead:

#!/bin/bash
"""exec" uv run --script "$0" "$@"
# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "requests",
# ]
# ///
"""

This is a Polyglot script, a script or program that can be interpreted by more than a single language. In this case, the script is valid Bash and Python. Let’s break it down how this works line by line:

#!/bin/bash: The standard shebang line for bash. The script is being interpreted by bash from here on.

"": An empty string. Strings that follow each other ("" "") are concatenated in bash.

"exec" uv run --script "$0" "$@": This calls the exec bash builtin which replaces the entire program image with the one that follows, in our case we invoke uv with the arguments run --script "$0" "$@". The last two arguments mean we forward the path to the script ($0) and all arguments ($@) to UV. The call to exec ends the execution within bash; therefore, all lines that follow do not have to be valid bash anymore.

Now our little script is not interpreted as bash anymore, but run as Python. We start from the top again:

#!/bin/bash: This is not a shebang anymore. To Python, this is just a comment.

""": It’s not a string anymore, in Python, this is interpreted as a multiline comment; therefore, Python will skip over the entire remaining of the file. The comments with /// script are interpreted according to Pythons inline script metadata specifications.

And with this, we have a script that’s both valid bash and python which can be used instead of relying on new versions of /usr/bin/env.