2 điểm bởi GN⁺ 2025-01-29 | 1 bình luận | Chia sẻ qua WhatsApp
  • Khi đặt các script tự động hóa trong ~/bin/ để chạy, có thể dùng uv cùng metadata script nội tuyến để quản lý script Python gọn nhẹ hơn
  • Nếu ~/bin đã có trong PATH, có thể đặt shebang#!/usr/bin/env -S uv run --script để chạy tên file như một lệnh
  • Cấu hình này khiến shell chạy uv run --script trước, rồi truyền file script hiện tại làm đối số
  • uv sẽ bỏ qua dòng shebang, sau đó thực thi phần còn lại như một file Python thông thường
  • Sau khi cấp quyền thực thi bằng chmod a+x {filname}, có thể dùng nó như một script dòng lệnh Python đơn giản với khả năng tự động xử lý phụ thuộc

Chạy uv bằng shebang

  • Tạo và dùng nhiều script trong thư mục ~/bin/ để tự động hóa công việc
  • Nếu ~/bin nằm trong PATH, có thể chạy trực tiếp script chỉ bằng tên file
  • Shebang dùng trong trường hợp này như sau
#!/usr/bin/env -S uv run --script

Luồng thực thi và thiết lập quyền

  • Shebang trên sẽ chạy uv run --script từ dòng lệnh và truyền file script làm đối số
  • uv sẽ bỏ qua shebang rồi thực thi phần còn lại của file như mã Python thông thường
  • Trước khi chạy, cần cấp quyền thực thi cho script
chmod a+x {filname}
  • Sau khi cấu hình xong, có thể chạy ngay một script dòng lệnh Python đơn giản có khả năng tự động xử lý phụ thuộc

1 bình luận

 
GN⁺ 2025-01-29
Các ý kiến trên Hacker News
  • Hôm nay tôi mới biết đến env -S, và ngay khi thấy shebang trong bài viết, tôi đã nghĩ “trên Linux chắc không chạy đâu, vì shebang chỉ có thể truyền một đối số”
    Ví dụ, khi chạy foo.py bắt đầu bằng #!/usr/bin/env -S uv run --script, hệ điều hành thực ra sẽ truyền toàn bộ dòng shebang như một đối số, kiểu /usr/bin/env '-S uv run --script' foo.py
    Nhưng tùy chọn -S của env lại tách chuỗi đó thành nhiều đối số, nên nó hoạt động. Rất hay và hữu ích

    • Trên macOS, hành vi này không giống vậy nên khá khó chịu: https://unix.stackexchange.com/a/774145
    • Tôi nhớ đến cách GNU Guile xử lý giới hạn một đối số bằng shebang nhiều dòng
      Đặt #!/usr/bin/guile \ rồi dòng tiếp theo là -e main -s, và đóng bằng !# thì nó sẽ thành kiểu /usr/bin/guile -e main -s filename
      Tôi tò mò vì sao họ lại phải làm như vậy. Có lẽ env -S là tính năng mới được thêm gần đây, hoặc không dùng được trên mọi nền tảng mà họ quan tâm
      https://www.gnu.org/software/guile/manual/html_node/The-Meta-Switch.html
    • Cách này dùng khá hợp với tusk, một Go CLI giống make nhưng dùng YAML để cấu hình
      Đặt #!/usr/bin/env -s go run github.com/rliebz/tusk@latest -f làm shebang, rồi dùng gosh, một Go shell, làm trình thông dịch kiểu interpreter: go run mvdan.cc/sh/v3/cmd/gosh@latest -s
      Như vậy, chỉ cần có Go được cài đặt là sẽ có một CLI chạy được trên mọi kiến trúc
    • Nếu wrapper phối hợp, cũng có thể đưa thêm nhiều thông tin vào các dòng theo sau
      Ví dụ nix-shell cho phép đặt #!/usr/bin/env nix-shell, rồi dòng tiếp theo là #!nix-shell --pure -i runghc ./default.nix để chỉ định dependency và đối số, sau đó có thể viết tiếp mã Haskell
    • env -S lẽ ra ngay từ đầu không nên cần thiết
      Quy tắc tách theo khoảng trắng kỳ lạ của dòng shebang là một lỗi cũ đã hóa thành khuyết điểm không thể sửa của Unix
      Mỗi lần dùng những mẹo kiểu này, tôi lại nghĩ rằng nếu hồi thập niên 1980 ai đó bỏ ra chỉ 30 phút thì đã tránh được nhiều năm bực bội về sau. Dòng shebang đáng lẽ phải được tách như /bin/sh ngay từ đầu
  • Không phải shebang, nhưng gần đây tôi đang dùng uv như một công cụ cài đặt
    Vì việc đóng gói và phân phối các công cụ Python CLI có dependency phức tạp khá khó, tôi đã tận dụng nó theo hai cách
    Một cách là sao chép script cài đặt curl | sh rồi thêm uv tool install --python python3.12 my-tool ở cuối để người dùng có thể cài CLI bằng một dòng lệnh; cách còn lại là tạo một gói “trình cài đặt” nhỏ trên PyPI chỉ phụ thuộc vào uv, để nó chạy uv tool install
    Cả hai cách đều cài công cụ CLI, Python 3.12 và toàn bộ dependency Python vào một môi trường cô lập. Người dùng không cần quản lý môi trường ảo, và với cách curl | sh thì thậm chí không cần cài sẵn Python
    Từ đó, số issue trên GitHub do người dùng làm hỏng các dependency phức tạp đã giảm đi rất nhiều
    Chi tiết và liên kết mã nguồn: https://aider.chat/2025/01/15/uv.html

    • Tôi chưa kiểm tra bên trong nó hoạt động thế nào, nhưng nếu cài cả công cụ CLI lẫn Python 3.12, có lẽ hệ thống của người dùng cuối cần có compiler, kernel header, các header thư viện như openssl-dev, gzip, ffi, và phải biên dịch Python chăng
      Ít nhất khi dùng ASDF-VM, tôi từng gặp trải nghiệm như vậy vì công cụ cài Python khác bên trong nó
    • Hay đấy. Có vẻ miseaqua cũng có khái niệm cài đặt công cụ hữu ích
  • Nhân lúc đang có câu chuyện thú vị về shebang, tôi đã làm thứ này để chạy các file Prolog .pl từ shell
    /*usr/bin/env scryer-prolog "$0" "$@" ; exit #*/
    Nếu là một ngôn ngữ script có chú thích /**/ nhưng không hỗ trợ trực tiếp chú thích # hay shebang #! thì nhìn chung có lẽ sẽ chạy được
    Bình luận gốc: https://github.com/mthom/scryer-prolog/issues/2170#issuecomment-1821713993
    File này vừa là một file shell hợp lệ, vừa là một file Prolog hợp lệ. Khi chạy bằng shell, dòng đầu tiên tìm glob pattern /*usr/bin/env và chạy /usr/bin/env, rồi env chạy scryer-prolog "test.pl". Về phía Prolog, dòng đầu tiên là chú thích /* ... */ nên bị bỏ qua
    Sau đó shell chạy exit sau dấu ; để không diễn giải phần mã Prolog còn lại như shell, còn # khiến shell bỏ qua phần kết thúc chú thích Prolog */, tránh in lỗi
    Có lẽ đây vừa là thứ hay nhất vừa là thứ tệ nhất tôi từng tạo ra

    • Cũng có cách tương tự để làm cho mã nguồn Go có thể chạy được: https://unix.stackexchange.com/questions/162531/shebang-starting-with
      Thêm vào đó, chương trình C cũng có thể được làm cho chạy được bằng cách dùng dòng đầu dạng //usr/bin/env sh -c .... Khi chạy, nó build bằng make rồi chạy binary kết quả, nên nếu sửa source rồi chạy lại thì output đã thay đổi sẽ hiện ra ngay
    • Không phải tôi tự nghĩ ra, nhưng TCL cũng dùng kiểu này
      Sau #!/bin/sh, có thể đặt dòng # the next line restarts using tcl \ rồi cho chạy exec tclsh "$0" "$@"
      Cách này tận dụng việc trong TCL ngay cả dòng chú thích cũng có thể nối dòng
    • Cái này thật sự ấn tượng. Nếu chỉnh một chút phần chuyển hướng nhập/xuất của Scryer thì có vẻ sẽ hoàn hảo như một phương án thay thế bash script
  • Bài Lazy self-installing Python scripts with uv của Trey Hunner có nội dung chi tiết hơn kèm ví dụ
    https://treyhunner.com/2024/12/lazy-self-installing-python-scripts-with-uv/

    • Trong các ví dụ, “script in ra 80 số 0” cũng có thể làm bằng shell với printf %.1s 0{1..80} $'\n' hoặc for i in {1..80}; do echo -n 0; done; echo
      Ví dụ ffmpeg thì tôi cũng không rõ khác gì so với 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
      Thành thật mà nói phía Python trông còn phức tạp hơn
  • Với các tác vụ nhỏ, có thể dùng mẹo này cùng mise(mise-en-place): https://mise.jdx.dev/tasks/toml-tasks.html#shell-shebang
    Cách làm là đặt uv = 'latest' trong tools, rồi trong block run của tác vụ khai báo #!/usr/bin/env -S uv run --script cùng dependencies = ["requests<3", "rich"], sau đó đưa mã Python vào

  • 16 ngày trước cũng đã có bài liên quan và thảo luận: Uv's killer feature is making ad-hoc environments easy https://news.ycombinator.com/item?id=42676432 (502 điểm, 417 bình luận)

  • Đây là một mẹo đáng ghi nhớ
    Gần đây tôi đã đổi alias Python thành uv run python và thấy khá dễ chịu. Không cần quản lý .venv v.v., cũng không phải bận tâm chuyện cài đặt hệ thống hay cài đặt global/user; trên Debian thì đặc biệt hữu ích
    Khi chạy REPL trong một project hay môi trường, cũng có thể gọi mà không cần activate, nên bớt phải suy nghĩ hơn
    Tuy nhiên, khi gọi trực tiếp .py bằng uv, nhược điểm là đường dẫn file project/môi trường được xác định theo thư mục làm việc hiện tại, chứ không theo vị trí của file .py
    Có switch tường minh --project nên có thể dùng kiểu uv run --project /script.py, nhưng nếu có một switch project tương đối theo file đích thì sẽ tốt hơn vì giảm được sự lặp lại

  • Khi dùng uv hay uvx làm trình chạy lệnh, tôi đã gặp vài cạm bẫy, nhưng phần lớn thời gian nó hoạt động tốt, và những lúc đó tiết kiệm được rất nhiều thời gian
    Là một lập trình viên Python và cũng là người hay tò mò, thư mục home của tôi đầy các bản clone nông và thư mục thử nghiệm dùng một lần; việc nó giúp giảm gánh nặng quản lý như vậy thật sự rất hữu ích
    Tôi muốn “mượn” ý tưởng này để dùng. Tôi tò mò không biết họ dùng một phần mở rộng riêng không phải .py, hay chỉ dựa vào việc file .py có bật bit thực thi và có shebang để coi đó là tín hiệu rằng file có thể chạy trực tiếp như một script, thay vì qua Python của hệ thống

    • Trong năm qua, để xem có nên khuyên dùng uv hay không, tôi đã tự dùng và tổng hợp một danh sách cạm bẫy, nhưng bất ngờ là không nhiều
      Không phải là 0, nhưng tôi vẫn đang khá tích cực tìm thêm. Nếu biết bạn đã gặp cạm bẫy nào thì tôi muốn thêm vào danh sách
    • Theo tôi hiểu thì cách làm là dùng .py kèm bit thực thi và dòng shebang như trong bài viết
    • Tôi thật sự tò mò là đã có những cạm bẫy nào
      Với Pipenv, từng có vấn đề khi một dependency cụ thể có wheel khác nhau theo từng nền tảng, còn Poetry thì hoạt động tốt
      File khóa của Pipenv chỉ bao gồm dependency của nền tảng đã khóa, trong khi Poetry thêm mọi biến thể nền tảng vào file khóa
      Tôi vẫn chưa tìm thấy tài liệu nói uv xử lý trường hợp này như thế nào
  • Ý chính là như sau
    Một PEP được công bố gần đây định nghĩa cách script Python khai báo dependency trong phần chú thích mở đầu, và uv là một trình chạy script Python kiêm trình quản lý gói: nó quét các dependency đó, đáp ứng chúng, rồi chạy script trong trạng thái có thể import các module tương ứng
    Nếu ở shebang, tức dòng chú thích đầu tiên của script Python, ta gọi file bằng uv thay vì python, thì khi “chạy” script, hiệu ứng này sẽ tự động có được
    Kết quả là ngay cả các script Python có dependency cần cấu hình cũng cứ thế chạy được

    • Tuy nhiên, uv phải được cài đặt trong $PATH
      Và dù uv sẽ cài dependency, việc cách này có tự động cài luôn cả trình thông dịch Python hay không thì chưa rõ
    • Rốt cuộc là phải cài uv vào hệ thống và thêm các dòng dependency vào đầu mọi script thì chúng mới “cứ thế chạy”
      Cá nhân tôi không thấy cách này tiết kiệm thời gian hơn so với cách cũ là tạo môi trường rồi gọi nó
      Tôi cũng không rõ uv có thể nạp môi trường trong phiên tương tác hay chỉ được dùng để chạy script
  • Nếu bạn thích TypeScript thì cũng có thể dùng cách tương tự với Deno. Bun có lẽ cũng được, nhưng tôi chưa tự thử
    Viết như #! /usr/bin/env -S deno run, và nếu cần cờ quyền thì thêm như #! /usr/bin/env -S deno run --allow-env --allow-read --allow-net