Skip to content

Instantly share code, notes, and snippets.

@cfstras
Created April 14, 2025 15:34
Show Gist options
  • Save cfstras/5c94e39b057d383f61c73779f3e7e9e2 to your computer and use it in GitHub Desktop.
Save cfstras/5c94e39b057d383f61c73779f3e7e9e2 to your computer and use it in GitHub Desktop.

Revisions

  1. cfstras renamed this gist Apr 14, 2025. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. cfstras renamed this gist Apr 14, 2025. 1 changed file with 0 additions and 0 deletions.
  3. cfstras revised this gist Apr 14, 2025. No changes.
  4. cfstras revised this gist Apr 14, 2025. 2 changed files with 0 additions and 0 deletions.
    File renamed without changes.
  5. cfstras revised this gist Apr 14, 2025. 2 changed files with 0 additions and 0 deletions.
    File renamed without changes.
  6. cfstras created this gist Apr 14, 2025.
    41 changes: 41 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,41 @@
    # Universal Python UV Shebang

    You've probably seen the `#!/usr/bin/env -S uv run` pattern popularized by [Simon Willison](https://simonwillison.net/2024/Aug/21/usrbinenv-uv-run/).

    One issue I've had with it:
    When "upgrading" scripts to get their own pyproject.toml, I can't run them from a different directory anymore!

    E.g. this works:
    ```bash
    ./my_script.py
    ```
    This doesn't:
    ```bash
    some_dir/my_script.sh
    ```

    This special, but longer, shebang header allows us to do that.

    ## How?
    It works like this:

    1. The first line is a shebang executing the file as a regular old POSIX script with /bin/sh.
    1. A comment follows - this is ignored by sh (and also python)
    1. Here comes the magic: We start a python block comment.
    - in Bash land, we just gave an empty string (`""`), followed by the start of a string (`"`).
    - to make bash happy, we finish off with `:"`
    - turning that "start of a string" into "please execute `:`"
    - which is an alias for "true"`, or a no-op command returning 0
    1. Now, we just tell bash to run `uv`, but with `--project $(dirname $0)`, so that UV knows where to search for a pyproject file.
    1. We use `exec` to replace the running `sh`, so that signals and return codes are followed
    1. We use `set -eu` to error out if any errors occur before uv takes over
    - unset variables
    - `uv` not being installed, etc.

    ## Limitations

    Tested on
    - macOS
    - Debian (Bookworm)

    Probably doesn't work on Windows, unless you're using WSL or Git-Bash.
    13 changes: 13 additions & 0 deletions _python-uv-project-shebang.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,13 @@
    #!/usr/bin/env sh
    # Special shebang: almost the same as just `uv run`, but always use the script directory to search for pyproject.toml.
    # The following lines are ignored by python, and executed by bash
    # Use `set -x` if you want to see what's happening
    """:"
    set -eu
    exec uv run --project=$(dirname "$0") "$0" "$@"
    """
    ###
    if __name__ == "__main__":
    print("Hello, world!")