2P by GN⁺ | ★ favorite | 댓글 1개
  • ~/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 설치 도구 때문에 그런 경험을 했음
    • 좋음. miseaqua에도 유용한 도구 설치 개념이 있는 것 같음
  • 재미있는 셔뱅 얘기가 나온 김에, 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를 실행하고, envscryer-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 스크립트 대체재로 완벽해질 듯함
  • 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 쪽이 더 복잡해 보임
  • 작은 작업에는 mise(mise-en-place)와 함께 이 요령을 쓸 수 있음: https://mise.jdx.dev/tasks/toml-tasks.html#shell-shebang
    toolsuv = 'latest'를 두고, 작업의 run 블록 안에서 #!/usr/bin/env -S uv run --scriptdependencies = ["requests<3", "rich"]를 선언한 뒤 Python 코드를 넣는 방식임

  • 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처럼 쓸 수는 있지만, 대상 파일 기준 상대 프로젝트 스위치가 있으면 반복을 줄일 수 있어 좋겠음

  • uvuvx명령 실행기로 쓰면서 몇 가지 함정은 겪었지만, 잘 동작할 때가 대부분이고 그럴 때는 시간을 크게 아껴 줌
    Python 개발자이자 호기심 많은 사람으로서 홈 디렉터리에 얕은 클론과 일회성 테스트 디렉터리가 널려 있는데, 이런 관리 부담을 덜어 주는 점이 정말 유용함
    이 아이디어는 훔쳐 쓰고 싶음. .py가 아닌 별도 확장자를 쓰는지, 아니면 .py 파일에 실행 비트가 켜져 있고 셔뱅이 있는 것만으로 시스템 Python이 아니라 스크립트로 바로 실행할 수 있다는 신호로 삼는지 궁금함

    • 지난 1년 동안 uv를 추천할 만한지 보려고 직접 쓰면서 함정 목록을 모아 왔는데, 의외로 많지 않았음
      0개는 아니지만 꽤 열심히 찾아보고 있음. 어떤 함정을 겪었는지 알 수 있으면 목록에 추가하고 싶음
    • 이해한 바로는 .py에 실행 비트와 기사에 나온 셔뱅 줄을 붙여 쓰는 방식임
    • 어떤 함정이 있었는지 정말 궁금함
      Pipenv에서는 특정 의존성이 플랫폼별로 다른 휠을 가질 때 문제가 있었고, Poetry에서는 잘 동작했음
      Pipenv의 잠금 파일은 잠근 플랫폼의 의존성만 포함했지만, Poetry는 모든 플랫폼 변형을 잠금 파일에 추가함
      uv가 이 경우 어떻게 동작하는지에 대한 문서는 아직 못 찾았음
  • 핵심은 이렇음
    최근 공개된 PEP가 Python 스크립트가 시작 주석에서 의존성을 선언하는 방식을 정의하고, uv는 그 의존성을 스캔해 충족시킨 뒤 해당 모듈들을 가져올 수 있는 상태로 스크립트를 실행하는 Python 스크립트 실행기이자 패키지 관리자임
    Python 스크립트 첫 줄 주석인 셔뱅에서 python 대신 uv로 파일을 호출하게 하면, 스크립트를 “실행”할 때 이 효과를 자동으로 얻게 됨
    결과적으로 설정이 필요한 의존성을 가진 Python 스크립트도 그냥 실행됨

    • 단, $PATHuv가 설치되어 있어야 함
      그리고 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처럼 추가하면 됨