2 คะแนน โดย GN⁺ 2025-01-29 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • หากใช้ uv ร่วมกับ metadata ของสคริปต์แบบ inline ในขั้นตอนที่วางสคริปต์อัตโนมัติไว้ใน ~/bin/ แล้วเรียกใช้งาน จะช่วยให้จัดการสคริปต์ Python ได้เบาขึ้น
  • หาก ~/bin อยู่ใน PATH สามารถกำหนด shebang เป็น #!/usr/bin/env -S uv run --script เพื่อเรียกใช้ไฟล์ตามชื่อเหมือนเป็นคำสั่งได้
  • การตั้งค่านี้ทำให้ shell เรียก uv run --script ก่อน แล้วส่งไฟล์สคริปต์ปัจจุบันเป็นอาร์กิวเมนต์
  • uv จะละเว้นบรรทัด shebang แล้วเรียกใช้เนื้อหาที่เหลือเหมือนเป็น ไฟล์ Python ปกติ
  • เมื่อให้สิทธิ์รันด้วย chmod a+x {filname} แล้ว ก็สามารถใช้เป็นสคริปต์บรรทัดคำสั่ง Python แบบง่ายที่จัดการ dependencies ให้อัตโนมัติได้

การรัน uv ด้วย shebang

  • ใช้ไดเรกทอรี ~/bin/ เพื่อสร้างและใช้งานสคริปต์หลายตัวสำหรับงานอัตโนมัติ
  • หาก ~/bin ถูกเพิ่มไว้ใน PATH แล้ว จะสามารถรันสคริปต์ได้โดยตรงด้วยชื่อไฟล์เท่านั้น
  • shebang ที่ใช้ในกรณีนี้มีดังนี้
#!/usr/bin/env -S uv run --script

ลำดับการรันและการตั้งค่าสิทธิ์

  • shebang ด้านบนจะเรียก uv run --script บน command line แล้วส่งไฟล์สคริปต์เป็นอาร์กิวเมนต์
  • uv จะละเว้น shebang แล้วรันส่วนที่เหลือของไฟล์เป็นโค้ด Python ปกติ
  • ก่อนรัน ต้องให้สิทธิ์ execute กับสคริปต์ก่อน
chmod a+x {filname}
  • เมื่อตั้งค่าเสร็จแล้ว ก็สามารถรันสคริปต์บรรทัดคำสั่ง Python แบบง่ายที่จัดการ dependencies ให้อัตโนมัติได้ทันที

1 ความคิดเห็น

 
GN⁺ 2025-01-29
ความคิดเห็นจาก Hacker News
  • วันนี้เพิ่งรู้จัก env -S เป็นครั้งแรก และพอเห็น shebang ในบทความก็คิดทันทีว่า “บน Linux ไม่น่าจะใช้ได้ เพราะ shebang ส่งอาร์กิวเมนต์ได้แค่ตัวเดียว”
    เช่น ถ้ารัน foo.py ที่ขึ้นต้นด้วย #!/usr/bin/env -S uv run --script ระบบปฏิบัติการจะส่งทั้งบรรทัด shebang เป็นอาร์กิวเมนต์เดียวจริง ๆ เหมือน /usr/bin/env '-S uv run --script' foo.py
    แต่ -S ของ env จะแยกสตริงนั้นกลับออกเป็นหลายอาร์กิวเมนต์ จึงทำงานได้ เจ๋งและมีประโยชน์มาก

    • บน macOS พฤติกรรมนี้ไม่เหมือนกัน เลยน่าหงุดหงิด: https://unix.stackexchange.com/a/774145
    • นึกถึงวิธีที่ GNU Guile จัดการข้อจำกัดอาร์กิวเมนต์เดียวด้วย shebang หลายบรรทัด
      ถ้าวาง #!/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
    • วิธีนี้เหมาะมากเมื่อใช้ร่วมกับ tusk ซึ่งเป็น Go CLI ที่คล้ายกับ make แต่ใช้ YAML สำหรับการตั้งค่า
      ตั้ง shebang เป็น #!/usr/bin/env -s go run github.com/rliebz/tusk@latest -f แล้วใช้อินเทอร์พรีเตอร์เป็น gosh ซึ่งเป็น Go shell เช่น interpreter: go run mvdan.cc/sh/v3/cmd/gosh@latest -s
      แบบนี้แค่มี Go ติดตั้งอยู่ ก็จะได้ CLI ที่รันได้บนทุกสถาปัตยกรรม
    • ถ้า wrapper ให้ความร่วมมือ ก็สามารถใส่ข้อมูลเพิ่มเติมในบรรทัดถัด ๆ ไปได้ด้วย
      เช่น nix-shell ใช้ #!/usr/bin/env nix-shell แล้วใส่ #!nix-shell --pure -i runghc ./default.nix ในบรรทัดถัดไปเพื่อระบุ dependency และอาร์กิวเมนต์ จากนั้นจึงเขียนโค้ด Haskell ต่อได้
    • env -S ไม่ควรจำเป็นต้องมีตั้งแต่แรก
      กฎการแยกช่องว่างแปลก ๆ ของบรรทัด shebang เป็นบั๊กเก่าที่กลายเป็นรอยตำหนิของ Unix ซึ่งแก้ไม่ได้แล้ว
      ทุกครั้งที่ต้องใช้ทริกแบบข้างต้น ก็อดคิดไม่ได้ว่าถ้าในยุค 1980 ใช้เวลาแค่ 30 นาที ก็คงเลี่ยงความหงุดหงิดอีกหลายปีหลังจากนั้นได้ บรรทัด shebang ควรถูกแยกอาร์กิวเมนต์แบบ /bin/sh มาตั้งแต่แรก
  • ไม่ใช่ shebang แต่ช่วงหลังใช้ uv เป็นเครื่องมือติดตั้ง
    เพราะการแพ็กเกจและแจกจ่ายเครื่องมือ Python CLI ที่มี dependency ซับซ้อนทำได้ยาก เลยนำไปใช้สองวิธี
    วิธีหนึ่งคือคัดลอกสคริปต์ติดตั้งแบบ curl | sh แล้วเพิ่ม uv tool install --python python3.12 my-tool ไว้ท้ายสุด เพื่อให้ผู้ใช้ติดตั้ง CLI ได้ด้วยคำสั่งบรรทัดเดียว อีกวิธีคือทำแพ็กเกจ “ตัวติดตั้ง” ขนาดเล็กบน PyPI ที่มี dependency แค่ uv แล้วให้มันรัน uv tool install
    ทั้งสองวิธีจะติดตั้งเครื่องมือ CLI, Python 3.12 และ dependency ของ Python ทั้งหมดไว้ในสภาพแวดล้อมแยก ผู้ใช้ไม่ต้องจัดการ virtual environment และในแบบ curl | sh ก็ไม่จำเป็นต้องมี Python ติดตั้งไว้ด้วย
    หลังจากนั้น issue บน GitHub ที่เกิดจากผู้ใช้ทำ dependency ซับซ้อนพังลดลงไปมาก
    รายละเอียดเพิ่มเติมและลิงก์โค้ด: https://aider.chat/2025/01/15/uv.html

    • ยังไม่ได้ตรวจว่าข้างในทำงานอย่างไร แต่ถ้าติดตั้งทั้งเครื่องมือ CLI และ Python 3.12 ด้วย ก็น่าจะต้องมีคอมไพเลอร์, kernel headers, header ของไลบรารีอย่าง openssl-dev, gzip, ffi ในระบบของผู้ใช้ปลายทาง และต้องคอมไพล์ Python เองไม่ใช่หรือ
      อย่างน้อยตอนใช้ ASDF-VM ผมเคยเจอแบบนั้นเพราะเครื่องมือติดตั้ง Python ตัวอื่นที่อยู่ข้างใน
    • ดีเลย ดูเหมือน mise และ aqua ก็มีแนวคิด การติดตั้งเครื่องมือ ที่มีประโยชน์เช่นกัน
  • ไหน ๆ ก็มีเรื่อง shebang ที่น่าสนใจแล้ว ผมเลยทำสิ่งนี้ขึ้นมาเพื่อรันไฟล์ Prolog .pl จากเชลล์
    /*usr/bin/env scryer-prolog "$0" "$@" ; exit #*/
    น่าจะใช้ได้กับภาษาสคริปต์ส่วนใหญ่ที่มีคอมเมนต์ /**/ แต่ไม่รองรับคอมเมนต์ # หรือ shebang #! โดยตรง
    คอมเมนต์ต้นฉบับ: https://github.com/mthom/scryer-prolog/issues/2170#issuecomment-1821713993
    ไฟล์นี้เป็นทั้งไฟล์เชลล์ที่ถูกต้อง และเป็นไฟล์ Prolog ที่ถูกต้องในเวลาเดียวกัน ถ้ารันด้วยเชลล์ บรรทัดแรกจะหา /usr/bin/env ด้วย glob pattern /*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 ... ได้เช่นกัน ตอนรันจะ build ด้วย make แล้วรันไบนารีผลลัพธ์ ดังนั้นถ้าแก้ซอร์สแล้วรันใหม่ ก็จะเห็นเอาต์พุตที่เปลี่ยนไปทันที
    • ไม่ได้คิดขึ้นเองโดยตรง แต่ใน TCL ก็ใช้แนวทางแบบนี้
      สามารถใส่ #!/bin/sh แล้วบรรทัดถัดไปใส่ # the next line restarts using tcl \ เพื่อให้รัน exec tclsh "$0" "$@" ได้
      ใช้ประโยชน์จากจุดที่ใน TCL บรรทัดคอมเมนต์ก็สามารถต่อบรรทัดได้
    • อันนี้สุดยอดจริง ๆ ถ้าปรับเรื่อง I/O redirection ของ Scryer อีกนิด น่าจะสมบูรณ์แบบในฐานะ ตัวแทน bash script ได้เลย
  • บทความ Lazy self-installing Python scripts with uv ของ Trey Hunner มีรายละเอียดมากกว่านี้พร้อมตัวอย่าง
    https://treyhunner.com/2024/12/lazy-self-installing-python-scripts-with-uv/

    • ในตัวอย่าง “สคริปต์ที่พิมพ์เลข 0 จำนวน 80 ตัว” นั้น ในเชลล์ก็ทำได้ด้วย 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
    วิธีคือใส่ uv = 'latest' ไว้ใน tools แล้วในบล็อก run ของงาน ประกาศ #!/usr/bin/env -S uv run --script กับ dependencies = ["requests<3", "rich"] จากนั้นใส่โค้ด Python ลงไป

  • เมื่อ 16 วันก่อนก็มีบทความและการถกเถียงที่เกี่ยวข้อง: Uv's killer feature is making ad-hoc environments easy https://news.ycombinator.com/item?id=42676432 (502 คะแนน, 417 คอมเมนต์)

  • เป็นทริกที่ควรจำไว้
    ช่วงหลังผมเปลี่ยน alias ของ Python เป็น uv run python แล้วค่อนข้างสบาย ไม่ต้องจัดการ .venv อะไรพวกนั้น และไม่ต้องกังวลเรื่องการติดตั้งในระบบหรือการติดตั้งแบบ global/user โดยเฉพาะบน Debian นี่ช่วยได้มาก
    เวลาเปิด REPL ในโปรเจกต์หรือ environment ก็เรียกได้โดยไม่ต้อง activate ทำให้มีเรื่องให้คิดน้อยลง
    แต่ข้อเสียคือเวลารัน .py โดยตรงด้วย uv เส้นทางไฟล์โปรเจกต์/environment จะอิงจาก current working directory ไม่ใช่อิงจากตำแหน่งของไฟล์ .py
    มีสวิตช์แบบระบุชัดเจน --project จึงใช้ได้แบบ uv run --project /script.py แต่ถ้ามีสวิตช์โปรเจกต์แบบ relative โดยอิงจากไฟล์เป้าหมาย ก็น่าจะดีเพราะช่วยลดการเขียนซ้ำได้

  • แม้จะเคยเจอกับดักอยู่บ้างตอนใช้ uv หรือ uvx เป็น ตัวรันคำสั่ง แต่ส่วนใหญ่ก็ทำงานได้ดี และเวลาที่เป็นแบบนั้นก็ช่วยประหยัดเวลาได้มาก
    ในฐานะนักพัฒนา Python และคนที่ชอบลองโน่นลองนี่ ในโฮมไดเรกทอรีของผมมี shallow clone กับไดเรกทอรีทดสอบแบบใช้ครั้งเดียวกระจัดกระจายเต็มไปหมด จุดที่ช่วยลดภาระในการจัดการแบบนี้จึงมีประโยชน์มากจริง ๆ
    ไอเดียนี้อยากเอาไปใช้ตามเลย สงสัยว่าใช้ส่วนขยายไฟล์แยกต่างหากที่ไม่ใช่ .py หรือว่าใช้แค่ไฟล์ .py ที่เปิดบิต executable และมี shebang เป็นสัญญาณว่ารันได้โดยตรงในฐานะสคริปต์ ไม่ใช่ผ่าน system Python

    • ตลอดปีที่ผ่านมา ผมลองใช้ uv เองเพื่อดูว่าควรแนะนำไหม และรวบรวม รายการกับดัก ไว้ ซึ่งน้อยกว่าที่คิด
      ไม่ใช่ว่าไม่มีเลย แต่ผมก็กำลังพยายามหาอย่างจริงจังอยู่ ถ้ารู้ว่าเจอกับดักอะไรบ้าง ผมอยากเพิ่มลงในรายการ
    • ตามที่ผมเข้าใจ คือใช้วิธีใส่ executable bit ให้ .py แล้วเพิ่ม บรรทัด shebang ตามที่บทความพูดถึง
    • อยากรู้จริง ๆ ว่ามีกับดักอะไรบ้าง
      ใน Pipenv เคยมีปัญหาเมื่อ dependency บางตัวมี wheel ต่างกันตามแพลตฟอร์ม ส่วน Poetry ทำงานได้ดี
      ไฟล์ lock ของ Pipenv ใส่เฉพาะ dependency ของแพลตฟอร์มที่ lock ไว้ แต่ Poetry เพิ่ม variant ของทุกแพลตฟอร์มลงในไฟล์ lock
      ยังหาเอกสารไม่เจอว่า uv ทำงานอย่างไรในกรณีนี้
  • สาระสำคัญคือแบบนี้
    PEP ที่เพิ่งเผยแพร่เมื่อไม่นานมานี้กำหนดวิธีให้สคริปต์ Python ประกาศ dependency ในคอมเมนต์ตอนต้นไฟล์ และ uv เป็นทั้งตัวรันสคริปต์ Python และตัวจัดการแพ็กเกจที่สแกน dependency เหล่านั้น จัดการให้ครบ แล้วรันสคริปต์ในสภาพที่สามารถ import โมดูลเหล่านั้นได้
    ถ้าให้บรรทัด shebang ซึ่งเป็นคอมเมนต์บรรทัดแรกของสคริปต์ Python เรียกไฟล์ด้วย uv แทน python ก็จะได้ผลนี้โดยอัตโนมัติเมื่อ “รัน” สคริปต์
    ผลลัพธ์คือแม้แต่สคริปต์ Python ที่มี dependency ซึ่งต้องตั้งค่าก็สามารถรันได้เลย

    • แต่ต้องมี uv ติดตั้งอยู่ ใน $PATH
      และแม้ uv จะติดตั้ง dependency ให้ แต่ยังไม่ชัดเจนว่าวิธีนี้จะติดตั้ง Python interpreter ให้โดยอัตโนมัติด้วยหรือไม่
    • สุดท้ายก็คือ หลังจากติดตั้ง uv ในระบบและใส่บรรทัด dependency ไว้หน้าสคริปต์ทุกตัวแล้ว จึงจะ “รันได้เลย”
      โดยส่วนตัวไม่เห็นว่ามันประหยัดเวลากว่าวิธีเดิมที่สร้าง environment แล้วเรียกใช้งาน
      ยังไม่ค่อยแน่ใจด้วยว่า uv สามารถโหลด environment สำหรับ session แบบโต้ตอบได้หรือเปล่า หรือใช้เฉพาะสำหรับการรันสคริปต์เท่านั้น
  • ถ้าชอบ TypeScript ก็ใช้วิธีเดียวกันกับ Deno ได้เหมือนกัน Bun ก็น่าจะได้ แต่ยังไม่ได้ลองเอง
    ใช้แบบ #! /usr/bin/env -S deno run และถ้าต้องมี flag สิทธิ์ ก็เพิ่มแบบ #! /usr/bin/env -S deno run --allow-env --allow-read --allow-net ได้

    • ผมชอบ Deno และใช้วิธีนี้อยู่ แต่หวังว่าจะช่วยแก้บั๊กงี่เง่านี้ที: https://github.com/denoland/deno/issues/16466