Skip to content(if available)orjump to list(if available)

Using uv as your shebang line

Using uv as your shebang line

138 comments

·January 28, 2025

rav

Oh wow, today I learned about env -S - when I saw the shebang line in the article, I immediately thought "that doesn't work on Linux, shebang lines can only pass a single argument". Basically, running foo.py starting with

    #!/usr/bin/env -S uv run --script
causes the OS run really run env with only two arguments, namely the shebang line as one argument and the script's filename as the second argument, i.e.:

    /usr/bin/env '-S uv run --script' foo.py
However, the -S flag of env causes env to split everything back into separate arguments! Very cool, very useful.

sangeeth96

It's frustrating this is not the same behavior on macOS: https://unix.stackexchange.com/a/774145

rav

It seems to me that macOS has env -S as well, but the shebang parsing is different. The result is that shebang lines using env -S are portable if they don't contain any quotes or other characters. The reason is that, running env -S 'echo a b c' has the same behavior as running env -S 'echo' 'a' 'b' 'c' - so simple command lines like the one with uv are still portable, regardless of whether the OS splits on space (macOS) or not (Linux).

fjarlq

This is true. For example, the following shebang/uv header works on both macOS and Linux:

  #!/usr/bin/env -S uv --quiet run --script
  # /// script
  # requires-python = ">=3.13"
  # dependencies = [
  #     "python-dateutil",
  # ]
  # ///
  #
  # [python script that needs dateutil]

sangeeth96

True, this should be fine for straightforward stuff but extremely annoying as soon as you have for eg, quoted strings with whitespace in it which is where it breaks. Have to keep that difference in mind when writing scripts.

The link I posted in my original reply has a good explanation of this behavior. I was the one who asked the question there.

dredmorbius

And that on Android env doesn't live in /bin/.

silverwind

`brew install coreutils` and update your `PATH`.

sangeeth96

I'm aware of this package for getting other utilities but:

1. I'm worried about this conflicting/causing other programs to fail if I set it on PATH. 2. This probably doesn't fix the shebang parsing issue I mentioned since it's an OS thing. Let me know if that's not the case.

drzaiusx11

You can also brew install the gnu tools package and have both side by side for compatibility (gnu tools are prefixed with 'g', gls, gcat, etc

I have a script that toggles the prefix on or off via bash aliases for when I need to run Linux bash scripts on a mac.

4ad

The PATH is irrelevant, this is about how the kernel parses the shebang. It starts exactly /usr/bin/env with two arguments, not some other env binary you might have in your PATH.

sudahtigabulan

Reminds me of how GNU Guile handles the one argument limitation - with "multi-line" shebang[1].

  #!/usr/bin/guile \
  -e main -s
  !#
turns into

  /usr/bin/guile -e main -s filename
Wonder why they bothered.

Probably env -S is a recent addition. Or not available on all platforms they cared about.

[1]: https://www.gnu.org/software/guile/manual/html_node/The-Meta...

RHSeeger

Somewhat unrelated, I guess, but we used to use a split line shebang for tcl like the following

    #!/bin/sh
    # A Tcl comment, whose contents don't matter \
    exec tclsh "$0" "$@"
- The first line runs the shell

- The second line is treated like a commend by the shell (and Tcl)

- The third line is executed by the shell to run Tcl with all the command line args. But then Tcl treats it as part of the second line (a comment).

Edit: Doing a bit of web searching (it's been a while since I last had the option to program in Tcl), this was also used to work around line length limitations in shebang. And also it let you exec Tcl from your path, rather than hard code it.

moondev

I like using this with tusk, which is a golang cli a bit like make, but it uses yaml for the config. The shebang is

      #!/usr/bin/env -s go run github.com/rliebz/tusk@latest -f
Then use gosh a golang shell for the interpreter

      interpreter: go run mvdan.cc/sh/v3/cmd/gosh@latest -s
This makes it a cli can run anywhere on any architecture with golang installed

viraptor

If the wrapper itself cooperates, you can also embed more information in the following lines. nix-shell for example allows installing dependencies and any parameters with:

    #!/usr/bin/env nix-shell
    #!nix-shell --pure -i runghc ./default.nix
    ... Any Haskell code follows

MayeulC

Yeah, it is very useful and allows environment variables, so you can do

   /usr/bin/env -S myvar=${somevar} ${someprefix}/bin/myprogram
However, as another commenter wrote, support is not universal (looks present in RH8 but not RH7 for instance). Also, the max length of a shebang is usually limited to about 127 characters.

So sometimes you have to resort to other tricks, such as polyglot scripts:

   /usr/bin/sh
   """exec" python --whatever "$@"
   Well this is still a Python docstring
   """
   print("hello")

Or classically in Tcl:

   #! /usr/bin/sh
   # Tcl can use \ to continue comments, but not sh \
   exec tclsh "$@" # still a comment in Tcl
   puts "hello"
Such things are not usually needed, until they are, and they make for fun head-scratching moment. I would personally recommend against them if they can be avoided, as they are relatively fragile.

I'll leave the self-compiling C language script "shebang" as an exercise to the reader ;)

quotemstr

env -S should never have been necessary. The strange whitespace splitting rules of the shebang line is an old bug that has matured into an unfixable wart marring the face of Unix forever. Every time I have to use tricks like the above, I'm reminded that half an hour of work in the 1980s would have saved years of annoyance later. Shebang lines should have always split like /bin/sh.

oneshtein

Send your patches to Linux and BSD kernel mailing lists.

quotemstr

It cannot be fixed now. It would break thing.

IshKebab

Yeah unfortunately support for that is kind of spotty, so don't do this in any scripts you want to work everywhere.

gkfasdfasdf

It would be nice if uv had something like uvx but for scripts...uvs maybe? Then you could put it as a single arg to env and it would work everywhere.

mingus88

Yeah, my first reaction was cool, what’s uv

Oh, yet another python dependency tool. I have used a handful of them, and they keep coming

I guess no work is important enough until it gets a super fast CLI written in the language du jour and installed by piping curl into sh

kernelbugs

I believe parent comment was about `env -S` not being portable rather than `uv` being portable.

I'll say, I am as pessimistic as the next person about new ways to do X just to be hip. But as someone who works on many different Python projects day to day (from fully fledged services, to a set of lambdas with shared internal libraries, to CI scripts, to local tooling needing to run on developer laptops) - I've found uv to be particularly free of many sharp edges of other solutions (poetry, pipenv, pyenv, etc).

I think the fact that the uv tool itself is not written in Python actually solves a number of real problems around bootstrapping and dependency management for the tool that is meant to be a dependency manager.

alt187

It's, er, "funny" how people used to make fun of `curl | sh` because of how lame it was, and now you have it everywhere because Rust decided that this should be the install.

baq

uv is the tool, finally. We've been waiting for two decades and it really does basically everything right, no ifs or buts. You can scratch off the 'yet another' part.

epcoa

uv is not just a dependency tool. uv deals well with packages and dependency management (well), venvs, runtimes, and tools. It replaces all the other tools and works better in just about every way.

IshKebab

> Oh, yet another python dependency tool. I have used a handful of them, and they keep coming

Yeah that's my opinion of all the other Python dependency tools, but uv is the real deal. It's fast, well designed, it's actually a drop-in replacement for pip and it actually works.

> I guess no work is important enough until it gets a super fast CLI written in the language du jour and installed by piping curl into sh

Yeah it's pretty nice that it's written in Rust so it's fast and reliable, and piping curl into sh makes it easy to install. Huge upgrade compared to the rest of Python tooling which is slow, janky and hard to install. Seriously the official way to install a recent Python on Linux is to build it from source!

It's a shame curl | bash is the best option we have but it does at least work reliably. Maybe one day the Linux community will come up with something better.

nsteel

What we have now, a load of different people developing a load of new (better!) tools, is surely what the PyPA had in mind when they developed their tooling standards. This is all going to plan. We've gotten new features and huge speedups far quicker this way.

I don't like changing out and learning new tools constantly, but if this is the cost of the recent rapid tooling improvements then it's a great price.

And best of all, it's entirely optional. You can even install it other ways. What exactly was your point here?

anotherpaulg

Not using shebang, but I've recently been using uv as an "installer". It's hard to package and distribute python CLI tools with complex dependencies. I used uv in a couple of novel ways:

1. I copied their `curl | sh` install script and added a `uv tool install --python python3.12 my-tool` line at the end. So users can now install my CLI tool with a nice curl-pipe-sh one-liner.

2. I made a tiny pypi "installer" package that has "uv" as its only dependency. All it does is `uv tool install` my CLI tool.

Both methods install the CLI tool, python3.12 and all python dependencies in their own isolated environment. Users don't need to manage python virtual envs. They don't need to even have python installed for the curl-pipe-sh method.

I now get far fewer GitHub issues from users who have somehow mangled the complex dependencies of my tool.

I wrote it up with more detail and links to code:

https://aider.chat/2025/01/15/uv.html

CoolCold

I have not checked how that works under the hood, but

> Both methods install the CLI tool, python3.12

Won't that require some compiler (GCC?), kernel headers, openssl-dev/gzip/ffi/other_lib headers to be present on end user system and then compiling Python?

At least that's my experience with ASDF-VM, which uses someother Python-setup toolkit under the hood.

null

[deleted]

oulipo

Nice! I think `mise` and `aqua` also have a notion of tool install which can be useful

infogulch

Speaking of funny shebangs, I came up with this to shell execute prolog .pl files, but it should work for any scripting language that has /**/-comments but doesn't support #-comments or #! shebangs specifically:

    /*usr/bin/env scryer-prolog "$0" "$@" ; exit #*/
From my original comment https://github.com/mthom/scryer-prolog/issues/2170#issuecomm... :

> The way this works is that this test.pl is both a valid shell file and prolog file. When executing as shell the first line finds and executes /usr/bin/env after searching for the glob pattern /*usr/bin/env. env then executes scryer-prolog "test.pl" which runs the prolog file as a module and halts; of course prolog ignores the first line as a comment /* ... */. Then the shell continues and executes the next command after ; which is exit which stops execution so the rest of the file (which is not valid shell) is ignored. The # makes the shell evaluator ignore prolog comment closer */ to prevent it from printing an error.

This may be the best and worst thing I have ever created.

emmelaich

Similarly, make go source executable : https://unix.stackexchange.com/questions/162531/shebang-star...

also, my contribution, make C programs executable:

    $ cat cute-use-of-slash-slash.c
    //usr/bin/env sh -c 'p=$(expr '"_$0"' : "_\(.*\)\.[^.]*"); make $p > /dev/null && $p'; exit

    #include <stdio.h>

    int main()
    {
        puts("hello world");
    }
    $ chmod  +x cute-use-of-slash-slash.c
    $ ./cute-use-of-slash-slash.c
    hello world
    $ $(pwd)/cute-use-of-slash-slash.c
    hello world
    $ ../cc/cute-use-of-slash-slash.c
    hello world
    $ sed -i -e s/hello/bye/ cute-use-of-slash-slash.c
    $ ./cute-use-of-slash-slash.c
    bye world
    $

mzs

I didn't think of this but I use it:

  % ./hworld.tcl  
  Hello, world.
  % cat hworld.tcl 
  #!/bin/sh
  # the next line restarts using tcl \
  exec tclsh "$0" "$@"
  puts "Hello, world."
  % 
Turns-out that in TCL you can line continue a comment :)

upghost

Oh my god this is amazing. We need to fix some of the IO redirect in Scryer and then this will be a perfect replacement for bash scripts <3

You rock!

nikkindev

“Lazy self-installing Python scripts with uv”[1] article from Trey Hunner has more details with examples

[1] https://treyhunner.com/2024/12/lazy-self-installing-python-s...

godelski

  > For example, here’s a script I use to print out 80 zeroes (or a specific number of zeroes) 
Also...

  # Print 80 0's and flush 
  printf %.1s 0{1..80} $'\n'
  # Alternatively
  for i in {1..80}; do echo -n 0; done; echo
For the ffmpeg example is this any different from

  ffmpeg -i in.mp4 -c:v copy -filter:a volumedetect -pass 1 -f null /dev/null &&\
  ffmpeg -i in.mp4 -c:v copy -filter:a "loudnorm" -pass 2 out.mp4
The python seems more complicated tbh

EdwardDiego

Not in any way relevant to an example of leveraging Python's new inline metadata PEP via uv.

hv42

You can use this trick with mise (mise-en-place) for small tasks: https://mise.jdx.dev/tasks/toml-tasks.html#shell-shebang

  [tools]
  uv = 'latest'

  [tasks.python_uv_task]
  run = """
  #!/usr/bin/env -S uv run --script
  # /// script
  # dependencies = ["requests<3", "rich"]
  # ///

  import requests
  # your code here 
  """

mkl

Related article and discussion from 16 days ago: Uv's killer feature is making ad-hoc environments easy https://news.ycombinator.com/item?id=42676432 (502 points, 417 comments)

babel_

Now that's a trick I should remember!

I recently switched over my python aliases to `uv run python` and it's been really quite pleasant, without needing to manage `.venv`s and the rest. No fuss about system installs for python or anything else either, which resolves the old global/user install problem (and is a boon on Debian). Also means you can invoke the REPL within a project/environment without any `activate`, which saves needing to think about it.

Only downside for calling a .py directly with uv is the cwd relative pathing to project/environment files, rather than relative to the .py file itself. There is an explicit switch for it, `--project`, which at least is not much of an ask (`uv run --project <path> <path>/script.py`), though a target relative project switch would be appreciated, to at least avoid the repetition.

threecheese

I e experienced a few “gotchas” using uv (or uvx) as a command runner, but when it works (which is most of the time) it’s a big time saver. As a Python dev and curious person my homedir is littered with shallow clones and one-off directories for testing some package or another, and not having to manage this overhead is really useful.

OP, you have a great idea and I’m stealing it. Do you use some non-.py suffix, or is seeing the exec bit at on a .py file enough of a signal for you to know that you can just run the file as a script (and it won’t use the system Python)?

przemub

From what I understand, he uses .py with exec bit and the shebang line as in the article.

EdwardDiego

Really curious as to your gotchas.

One thing that hit me in Pipenv, but worked well in Poetry, is when a given dep has different wheels for different platforms.

The pipenv lock lockfile would only include the deps for the platform you locked it on.

Poetry adds all platform variants to the lockfile.

And haven't found any documentation around uv's behaviour in this regard.

BiteCode_dev

I've been collecting gotchas from using uv myself over the last year to assess whether to recommend it or not and I found surprisingly few. Not zero, but I'm looking pretty hard.

Would love to hear what gotchas to find so I can add that to the list.

achierius

Is the list public? I'm trying to figure out whether it's worth taking the plunge.

BiteCode_dev

I'll write an article on bitecode.dev at the one year mark (about end of feb) about the various situations I tested it in with clients and the results.

I will include a section about the things that it was not good for and the problems with it. Spoiler, there were not that many and they fixed them astonishingly well (even one this week!). But obviously there were some, no software is perfect although they worked hard to not make it too opinionated for the first taste.

I can ping you when it comes out.

dcre

For the TypeScript enjoyers: you can do the same with Deno (and I'm sure Bun too, though I haven't done it).

    #! /usr/bin/env -S deno run
You can add permission flags like this:

    #! /usr/bin/env -S deno run --allow-env --allow-read --allow-net

IshKebab

I love Deno and I use this, but I really wish they would fix this dumb bug:

https://github.com/denoland/deno/issues/16466

sebmellen

I really wish they would allow you to connect to "unsafe" TLS endpoints.

https://github.com/denoland/deno/issues/6427#issuecomment-65...

tempaccount420

Jesus, both of these issues are sad. This is probably why Bun is winning

dcre

That's interesting! I think I've run into it but it has rarely been a problem.

krick

Wow, this is super obvious (so obvious that it wouldn't occur to me to write a post to brag about it), yet it somehow didn't occur to me. Very cool.

Not so long ago I started pip-installing my scripts to keep things tidy. It seems now I can regress back to chaos again...

nomel

My favorite way to pass small scripts to non software colleagues:

    try:
       import package
    except ImportError:
       import pip
       print("Installing package...")
       pip.main(['install', 'package']
       import package
:D

traverseda

You can also do the same thing with the nix package manager and pretty much any language or dependency. Like if you wanted a portable bash-script with imagemagick

    #! /usr/bin/env nix-shell
    #! nix-shell -i bash -p bash imagemagick
Handy for passing quick scripts around. Nothing is quite as portable as a single file like this.

IshKebab

> Nothing is quite as portable as a single file like this.

Well... a file that doesn't require Nix is probably a fair bit more portable!

traverseda

Sure, an appimage, the UV example above

recursive_fruit

Oh nice, that's really cool. I'm seeing uv pop up in more and more places. Python really needed this tooling upgrade. Also, I'll make a mental note of -S, awesome.

If you want to do the same with javascript or typescript, you can just use bun, it will auto-install[1] the packages included in there.

    #!/usr/bin/env bun
[1]: https://bun.sh/docs/runtime/autoimport