uv를 shebang 라인으로 사용하는 방법
(akrabat.com)~/bin/에 자동화 스크립트를 두고 실행하는 흐름에서 uv와 인라인 스크립트 메타데이터를 쓰면 Python 스크립트를 더 가볍게 다룰 수 있음~/bin이 PATH에 들어 있다면 shebang을#!/usr/bin/env -S uv run --script로 지정해 파일명을 명령처럼 실행할 수 있음- 이 설정은 셸이
uv run --script를 먼저 실행하고, 현재 스크립트 파일을 인자로 넘기도록 만듦 - uv는 shebang 라인을 무시한 뒤 나머지 내용을 일반 Python 파일처럼 실행함
chmod a+x {filname}로 실행 권한을 주면, 의존성을 자동 처리하는 간단한 Python 명령줄 스크립트로 사용할 수 있음
uv를 shebang으로 실행하기
- 작업 자동화를 위해
~/bin/디렉터리에 여러 스크립트를 만들어 사용함 ~/bin이 PATH에 포함되어 있으면 스크립트를 파일명만으로 직접 실행할 수 있음- 이때 사용할 shebang은 다음과 같음
#!/usr/bin/env -S uv run --script
실행 흐름과 권한 설정
- 위 shebang은 명령줄에서
uv run --script를 실행하고, 스크립트 파일을 인자로 전달함 - uv는 shebang을 무시한 뒤 파일의 나머지 부분을 일반 Python 코드로 실행함
- 실행 전에는 스크립트에 실행 권한을 부여해야 함
chmod a+x {filname}
- 설정을 마치면 의존성을 자동으로 처리하는 간단한 Python 명령줄 스크립트를 바로 실행할 수 있음
댓글과 토론
Hacker News 의견들
-
오늘 env -S를 처음 알게 됐고, 기사 속 셔뱅을 보자마자 “Linux에서는 안 될 텐데, 셔뱅은 인자를 하나만 넘길 수 있으니까”라고 생각했음
예를 들어#!/usr/bin/env -S uv run --script로 시작하는foo.py를 실행하면 운영체제는 실제로/usr/bin/env '-S uv run --script' foo.py처럼 셔뱅 줄 전체를 하나의 인자로 넘김
그런데env의-S가 그 문자열을 다시 여러 인자로 쪼개 줘서 동작함. 아주 멋지고 유용함- macOS에서는 이 동작이 같지 않아서 답답함: https://unix.stackexchange.com/a/774145
- GNU Guile이 인자 하나 제한을 여러 줄 셔뱅으로 처리하는 방식이 떠오름
#!/usr/bin/guile \다음 줄에-e main -s를 두고!#로 닫으면/usr/bin/guile -e main -s filename처럼 됨
왜 굳이 이렇게 했는지는 궁금함. 아마env -S가 최근 추가였거나, 관심 있던 모든 플랫폼에서 쓸 수 없었을 듯함
https://www.gnu.org/software/guile/manual/html_node/The-Meta-Switch.html make와 비슷하지만 설정에 YAML을 쓰는 Go CLI인 tusk와 함께 이 방식을 쓰기 좋음
#!/usr/bin/env -s go run github.com/rliebz/tusk@latest -f를 셔뱅으로 두고, 인터프리터는interpreter: go run mvdan.cc/sh/v3/cmd/gosh@latest -s처럼 Go 셸인 gosh를 사용함
이렇게 하면 Go만 설치되어 있으면 어떤 아키텍처에서도 실행 가능한 CLI가 됨- 래퍼가 협조해 주면 뒤따르는 줄에 더 많은 정보를 넣을 수도 있음
예를 들어 nix-shell은#!/usr/bin/env nix-shell다음 줄에#!nix-shell --pure -i runghc ./default.nix를 둬서 의존성과 인자를 지정할 수 있고, 그 뒤에 Haskell 코드를 이어 쓸 수 있음 - env -S는 애초에 필요하지 않았어야 함
셔뱅 줄의 이상한 공백 분리 규칙은 오래된 버그가 고칠 수 없는 Unix의 흠으로 굳어진 것임
위 같은 꼼수를 쓸 때마다 1980년대에 30분만 들였으면 이후 수년간의 짜증을 피할 수 있었겠다는 생각이 듦. 셔뱅 줄은 처음부터/bin/sh처럼 분리됐어야 함
-
셔뱅은 아니지만 최근 uv를 설치 도구처럼 쓰고 있음
복잡한 의존성이 있는 Python CLI 도구를 패키징하고 배포하기가 어려워서 두 가지 방식으로 활용했음
하나는curl | sh설치 스크립트를 복사한 뒤 끝에uv tool install --python python3.12 my-tool을 추가해 사용자가 한 줄로 CLI를 설치하게 하는 방식이고, 다른 하나는uv만 의존성으로 가진 작은 PyPI “설치기” 패키지를 만들어uv tool install을 실행하게 하는 방식임
두 방식 모두 CLI 도구, Python 3.12, 모든 Python 의존성을 격리된 환경에 설치함. 사용자는 가상 환경을 관리할 필요가 없고,curl | sh방식에서는 Python이 설치되어 있을 필요도 없음
그 뒤로 사용자가 복잡한 의존성을 망가뜨려 생기는 GitHub 이슈가 훨씬 줄었음
자세한 내용과 코드 링크: https://aider.chat/2025/01/15/uv.html- 내부가 어떻게 동작하는지는 확인하지 않았지만, CLI 도구와 Python 3.12 설치까지 한다면 최종 사용자 시스템에 컴파일러, 커널 헤더,
openssl-dev,gzip,ffi같은 라이브러리 헤더가 필요하고 Python을 컴파일해야 하지 않나 싶음
적어도 ASDF-VM을 쓸 때는 내부의 다른 Python 설치 도구 때문에 그런 경험을 했음 - 좋음.
mise와aqua에도 유용한 도구 설치 개념이 있는 것 같음
- 내부가 어떻게 동작하는지는 확인하지 않았지만, CLI 도구와 Python 3.12 설치까지 한다면 최종 사용자 시스템에 컴파일러, 커널 헤더,
-
재미있는 셔뱅 얘기가 나온 김에, Prolog
.pl파일을 셸에서 실행하려고 이런 걸 만들었음
/*usr/bin/env scryer-prolog "$0" "$@" ; exit #*/
/**/주석은 있지만#주석이나#!셔뱅을 직접 지원하지 않는 스크립트 언어라면 대체로 동작할 것 같음
원래 코멘트: https://github.com/mthom/scryer-prolog/issues/2170#issuecomment-1821713993
이 파일은 유효한 셸 파일이면서 동시에 유효한 Prolog 파일임. 셸로 실행하면 첫 줄이 글롭 패턴/*usr/bin/env를 찾아/usr/bin/env를 실행하고,env가scryer-prolog "test.pl"을 실행함. Prolog 쪽에서는 첫 줄이/* ... */주석이라 무시됨
이후 셸은;뒤의exit를 실행해 나머지 Prolog 코드를 셸로 해석하지 않게 하고,#는 셸이 Prolog 주석 종료자인*/를 무시하게 해 오류 출력을 막음
내가 만든 것 중 최고이자 최악일지도 모름- 비슷하게 Go 소스를 실행 가능하게 만드는 방법도 있음: https://unix.stackexchange.com/questions/162531/shebang-starting-with
여기에 더해 C 프로그램도//usr/bin/env sh -c ...형태의 첫 줄을 이용해 실행 가능하게 만들 수 있음. 실행 시make로 빌드한 뒤 결과 바이너리를 실행하므로, 소스를 수정하고 다시 실행하면 변경된 출력이 바로 나옴 - 직접 생각해낸 건 아니지만 TCL에서도 이런 식을 씀
#!/bin/sh다음 줄에# the next line restarts using tcl \를 두고exec tclsh "$0" "$@"를 실행하게 만들 수 있음
TCL에서는 주석 줄도 줄 이어쓰기가 가능하다는 점을 이용함 - 이건 정말 대단함. Scryer의 입출력 리다이렉션을 조금 고치면 bash 스크립트 대체재로 완벽해질 듯함
- 비슷하게 Go 소스를 실행 가능하게 만드는 방법도 있음: https://unix.stackexchange.com/questions/162531/shebang-starting-with
-
Trey Hunner의 Lazy self-installing Python scripts with uv 글에 예시와 함께 더 자세한 내용이 있음
https://treyhunner.com/2024/12/lazy-self-installing-python-scripts-with-uv/- 예시 중 “80개의 0을 출력하는 스크립트”는 셸에서도
printf %.1s 0{1..80} $'\n'나for i in {1..80}; do echo -n 0; done; echo로 가능함
ffmpeg 예시도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와 뭐가 다른지 잘 모르겠음
솔직히 Python 쪽이 더 복잡해 보임
- 예시 중 “80개의 0을 출력하는 스크립트”는 셸에서도
-
작은 작업에는 mise(mise-en-place)와 함께 이 요령을 쓸 수 있음: https://mise.jdx.dev/tasks/toml-tasks.html#shell-shebang
tools에uv = 'latest'를 두고, 작업의run블록 안에서#!/usr/bin/env -S uv run --script와dependencies = ["requests<3", "rich"]를 선언한 뒤 Python 코드를 넣는 방식임- uv에 대해 비슷한 예시가 여기에도 나와 있음: https://simonwillison.net/2024/Aug/21/usrbinenv-uv-run/
-
16일 전에도 관련 글과 토론이 있었음: Uv's killer feature is making ad-hoc environments easy https://news.ycombinator.com/item?id=42676432 (502점, 댓글 417개)
-
기억해 둘 만한 요령임
최근 Python 별칭을uv run python으로 바꿨는데 꽤 쾌적함..venv등을 관리할 필요가 없고, 시스템 설치나 전역/사용자 설치 문제도 신경 쓰지 않아도 되며 Debian에서는 특히 도움이 됨
프로젝트나 환경 안에서 REPL을 실행할 때도activate없이 호출할 수 있어 생각할 거리가 줄어듦
다만.py를 uv로 직접 호출할 때는 현재 작업 디렉터리 기준으로 프로젝트/환경 파일 경로가 잡히고,.py파일 위치 기준이 아니라는 점이 단점임
명시적 스위치--project가 있어uv run --project /script.py처럼 쓸 수는 있지만, 대상 파일 기준 상대 프로젝트 스위치가 있으면 반복을 줄일 수 있어 좋겠음 -
uv나uvx를 명령 실행기로 쓰면서 몇 가지 함정은 겪었지만, 잘 동작할 때가 대부분이고 그럴 때는 시간을 크게 아껴 줌
Python 개발자이자 호기심 많은 사람으로서 홈 디렉터리에 얕은 클론과 일회성 테스트 디렉터리가 널려 있는데, 이런 관리 부담을 덜어 주는 점이 정말 유용함
이 아이디어는 훔쳐 쓰고 싶음..py가 아닌 별도 확장자를 쓰는지, 아니면.py파일에 실행 비트가 켜져 있고 셔뱅이 있는 것만으로 시스템 Python이 아니라 스크립트로 바로 실행할 수 있다는 신호로 삼는지 궁금함- 지난 1년 동안 uv를 추천할 만한지 보려고 직접 쓰면서 함정 목록을 모아 왔는데, 의외로 많지 않았음
0개는 아니지만 꽤 열심히 찾아보고 있음. 어떤 함정을 겪었는지 알 수 있으면 목록에 추가하고 싶음 - 이해한 바로는
.py에 실행 비트와 기사에 나온 셔뱅 줄을 붙여 쓰는 방식임 - 어떤 함정이 있었는지 정말 궁금함
Pipenv에서는 특정 의존성이 플랫폼별로 다른 휠을 가질 때 문제가 있었고, Poetry에서는 잘 동작했음
Pipenv의 잠금 파일은 잠근 플랫폼의 의존성만 포함했지만, Poetry는 모든 플랫폼 변형을 잠금 파일에 추가함
uv가 이 경우 어떻게 동작하는지에 대한 문서는 아직 못 찾았음
- 지난 1년 동안 uv를 추천할 만한지 보려고 직접 쓰면서 함정 목록을 모아 왔는데, 의외로 많지 않았음
-
핵심은 이렇음
최근 공개된 PEP가 Python 스크립트가 시작 주석에서 의존성을 선언하는 방식을 정의하고, uv는 그 의존성을 스캔해 충족시킨 뒤 해당 모듈들을 가져올 수 있는 상태로 스크립트를 실행하는 Python 스크립트 실행기이자 패키지 관리자임
Python 스크립트 첫 줄 주석인 셔뱅에서python대신uv로 파일을 호출하게 하면, 스크립트를 “실행”할 때 이 효과를 자동으로 얻게 됨
결과적으로 설정이 필요한 의존성을 가진 Python 스크립트도 그냥 실행됨- 단,
$PATH에 uv가 설치되어 있어야 함
그리고 uv가 의존성은 설치하겠지만, 이 방식으로 Python 인터프리터까지 자동 설치하는지는 명확하지 않음 - 결국 시스템에 uv를 설치하고 모든 스크립트 앞에 의존성 줄을 붙인 뒤에야 “그냥 실행”되는 것임
개인적으로는 예전 방식으로 환경을 만들고 호출하는 것보다 시간이 절약된다고 보이지 않음
uv가 대화형 세션에서만 환경을 불러올 수 있는지, 아니면 스크립트 실행용으로만 쓰이는지도 잘 모르겠음
- 단,
-
TypeScript를 좋아한다면 Deno로도 같은 방식을 쓸 수 있음. Bun도 될 것 같지만 직접 해 보진 않았음
#! /usr/bin/env -S deno run처럼 쓰고, 권한 플래그가 필요하면#! /usr/bin/env -S deno run --allow-env --allow-read --allow-net처럼 추가하면 됨- Deno를 좋아하고 이 방식을 쓰고 있지만, 이 멍청한 버그는 꼭 고쳐 줬으면 함: https://github.com/denoland/deno/issues/16466