Using uv as your shebang line
akrabat.com
akrabat.com
#!/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. #!/usr/bin/env -S uv --quiet run --script
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "python-dateutil",
# ]
# ///
#
# [python script that needs dateutil]The link I posted in my original reply has a good explanation of this behavior. I was the one who asked the question there.
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.
Been doing it for more than a decade and yet to get in trouble. Not one issue. Doing it consistently for my teams as we decrease cognitive load (developing on macs but targeting unix). Others would confirm https://news.ycombinator.com/item?id=17943202
Basically software will either use absolute paths i.e. wants to use your OS version for a dependency like grep, or will use whatever grep is in your $PATH and stick to safe invocations regardless if it's BSD/GNU or if it's version x or y
> Basically software will either use absolute paths
I’ve personally written scripts that break this assumption (that’s a me problem, I guess) so I am quite sure there’s a lot of scripts at the very least that do this.
Nevertheless, you’ve given me something to consider.
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.
#!/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...
#!/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.
#!/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 #!/usr/bin/env nix-shell
#!nix-shell --pure -i runghc ./default.nix
... Any Haskell code follows(Though, this is more general than uv for Python script deps: https://packaging.python.org/en/latest/specifications/inline...)
/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 ;)
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
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.
It's interesting that the language people choose to write systems with (Python) is basically identified as not the best language to write systems to support that language (Python).
To my knowledge, no other mainstream language has tooling predominantly written in another language.
I think Javascript and Python stand out because they are both extremely popular and also not very good languages, especially their tooling. You're obviously going to get a load of people using Javascript and Python saying "why is Webpack/Pip so damn slow? I don't have any choice but to use this language because of the web/AI/my coworkers are idiots, so I may as well improve the tooling".
No, now you have it everywhere because the Linux community completely failed to come up with anything better.
If a developer can't be arsed to package their software, it's not the Linux community's fault
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.
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?
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:
> 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.
/*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.
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
$ % ./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 :)You rock!
[1] https://treyhunner.com/2024/12/lazy-self-installing-python-s...
> 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 [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
"""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.
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)?
Would love to hear what gotchas to find so I can add that to the list.
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.
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.
* A recently-published PEP specificies how Python scripts can declare dependencies in opening comments.
* uv is a Python script-runner/package manager which scans for those dependencies, arranges for them to be met, and runs the script with those dependency modules available for importing
* If, in a Python script, you use the first line comment - the shebang line - to have the file invoked using uv rather than python itself, you'll get that effect automagically when "running" the Python script
The result: Python scripts which require setup of dependencies will now "just run".
Also, uv will install the dependencies, but I am not clear on whether or not it will automatically install a Python interpreter for you using this method.
Imo this saves you no time over just constructing and calling environments the old way. I’m not sure either whether uv lets you load the env only in an interactive session or if its just for script running.
but I think what it saves you is the trouble of setting up a virtual environment the "old" way. So, it's about making things easier, not faster.
#! /usr/bin/env -S deno run
You can add permission flags like this: #! /usr/bin/env -S deno run --allow-env --allow-read --allow-nethttps://github.com/denoland/deno/issues/6427#issuecomment-65...
> "What, you want to connect to an old government website that has an old TLS version? You are a very bad boy. Not allowed!"
Back to Node we go.
Not so long ago I started pip-installing my scripts to keep things tidy. It seems now I can regress back to chaos again...
try:
import package
except ImportError:
import pip
print("Installing package...")
pip.main(['install', 'package']
import package
:D #! /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.Well... a file that doesn't require Nix is probably a fair bit more portable!
"/usr/bin/env -S uv run": https://news.ycombinator.com/item?id=42198256
125 points, 45 comments
I haven’t dove into uv as it just hasn’t been at the top of my list, but I will probably take the plunge soon.
I gave it an initial try thinking it might function as a drop in, but didn’t find any great guides, and just haven’t been back yet. But we’re ~8% into 2025 so it’s time.
I just wrote a script without a .py extension, and it seemed to work fine.
I’ve been having a blast automating some macOS annoyances with small scripts that can use native macOS APIs directly, no more pyobjc.
I also wrote an app to bring the startup folder functionality from Windows to the Mac to help me run these scripts at startup more easily [2]
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 #!/bin/sh
#! -*- perl -*- -p
eval 'exec perl -x -wS $0 ${1+"$@"}'
if 0;
# It's Perl code from this point.
0 - https://perldoc.perl.org/perlrunOf course I have plenty of separate environments set up, but that's because I'm developing things I hope to share with others, so I need to keep close track of their specific dependencies (so I can specify them in package metadata). But even this stuff would generally work with a wide range of versions for those dependencies. I'd imagine this is equally true of small, one-off, personal automation scripts.
But, sure, if you have this problem, that's a great way to use the shebang line. (You can do similarly with Pipx, and I expect you'll be able to with Paper as well, although I might push that sort of thing into an optional add-on.)
If my weird one-off ad-hoc script needs numpy I can bake the exact tested version into an inline script dependency and run it with "uv run" and never have to think about it ever again.
If I don't do that, there's a good chance that in a year I'll upgrade numpy for some other project and now my ad-hoc script will break.
(Numpy is actually a great example here, lots of code I try to run turns out to need numpy 1.0 because it hasn't yet been upgraded to work with the new 2.0 - here's a recent example https://simonwillison.net/2024/Dec/24/modernbert/ )
Competing languages like Java don't need or even have the concept of isolated environments. It's an illogical concept. You have a class path and that is about it.
> Competing languages like Java don't need or even have the concept of isolated environments
The nice thing about scripts is that you can quickly add new capabilities when you need them. They are always under development. To get the same thing in Java, you need a project directory with a build file, which gives you an isolated build environment.
Right; my point is that I'd expect one-off scripts like this to keep relying on the same few standard dependencies which would already have been set up in an environment somewhere.
>You have a class path and that is about it.
A Python venv pretty much is just a layer on top of the existing "class path" mechanism (sys.path) to point it at the right place automatically (reproducing the basic folder layout relative to a symlinked executable, so that sys.path ends up with the right paths and so that there's a dedicated folder for installing the dependencies). While it's ugly and not recommended, you can work with the PYTHONPATH environment variable, or even modify sys.path at runtime.
$ python -m venv --without-pip test-venv && du -sh test-venv
56K test-venvPhysically, that can just be a symlink to the system python (or another version that is still potentially shared with multiple venvs.)
https://bundler.io/guides/bundler_in_a_single_file_ruby_scri...
Though I think the implementation could be improved upon
function transcribe() {
# get the active conda environment
active_env=$(conda info | grep "active environment" | awk '{print $4}')
if [[ "$active_env" != "speechrec-env" ]]; then
conda activate speechrec-env
fi
python /users/vunderba/dev/transcriber/transcribe.py "$@"
}
It's handy because if it'll always point to the most current version of my script.Besides, not everyone uses conda, and it would be quite a stretch to say it "might as well" be a built-in compared to, well, the actual built in, pip! Plus, uv works quite nicely as "just a pip replacement", which is how I started with it, so it aligns quite well to the actual built-in paradigm of the language.
I love having little custom command line apps in my ~/bin directory. I used to, a very long time ago, use Gambit Scheme for this, then moved on to Common Lisp command line apps. More recently I have been using Python for ~/bin scripts but since I use venv for each individual project on my laptop, having a global Python setup with dependencies for ~/bin is annoying. Using uv seems like a great idea, which I will try today: use uv for all command line apps and venv for all other Python coding projects.
- speed, as others have said (5 minutes → 70 seconds for our docker build)
- much easier (and faster) virtual environment management than poetry, should you want a venv
- manages the python version, including new GIL-less variants if you want to try them
- script dependencies (include requirements as a comment in a script and it will install and use them)
I highly recommend it over all conceivable alternatives.
I read the same sentiment about poetry a few years ago. Turns out tools that do one thing well are better in the long run.
it's the same for uv/pip/poetry except uv is so much better than the alternatives there isn't any contest. pip is the safe default (which doesn't even work out of the box on debian derivatives, which is half the issue) and uv is... just default.
I did eventually figure out I could just do `corepack pnpm setup` then install packages globally with pnpm.
https://akrabat.com/defining-python-dependencies-at-the-top-...
#!/usr/bin/env python3
?