2 puntos por GN⁺ 2025-01-29 | 1 comentarios | Compartir por WhatsApp
  • En un flujo donde colocas scripts de automatización en ~/bin/, usar uv y metadatos de script en línea permite manejar scripts de Python de forma más ligera
  • Si ~/bin está en el PATH, puedes definir el shebang como #!/usr/bin/env -S uv run --script y ejecutar el archivo por su nombre como si fuera un comando
  • Esta configuración hace que el shell ejecute primero uv run --script y le pase el archivo del script actual como argumento
  • uv ignora la línea shebang y luego ejecuta el resto del contenido como un archivo normal de Python
  • Si le das permisos de ejecución con chmod a+x {filname}, puedes usarlo como un script simple de línea de comandos en Python que resuelve dependencias automáticamente

Ejecutar uv como shebang

  • Para automatizar tareas, se pueden crear y usar varios scripts en el directorio ~/bin/
  • Si ~/bin está incluido en el PATH, puedes ejecutar los scripts directamente solo con el nombre del archivo
  • El shebang que se usa en este caso es el siguiente
#!/usr/bin/env -S uv run --script

Flujo de ejecución y configuración de permisos

  • El shebang anterior ejecuta uv run --script desde la línea de comandos y pasa el archivo del script como argumento
  • uv ignora el shebang y luego ejecuta el resto del archivo como código Python normal
  • Antes de ejecutarlo, debes darle permisos de ejecución al script
chmod a+x {filname}
  • Una vez configurado, puedes ejecutar de inmediato un script simple de línea de comandos en Python que maneja dependencias automáticamente

1 comentarios

 
GN⁺ 2025-01-29
Comentarios de Hacker News
  • Hoy me enteré por primera vez de env -S, y en cuanto vi el shebang del artículo pensé: “eso no va a funcionar en Linux, porque el shebang solo puede pasar un argumento”
    Por ejemplo, si ejecutas foo.py que empieza con #!/usr/bin/env -S uv run --script, el sistema operativo en realidad pasa toda la línea shebang como un solo argumento, algo como /usr/bin/env '-S uv run --script' foo.py
    Pero env con -S vuelve a dividir esa cadena en varios argumentos, así que sí funciona. Muy genial y útil

    • En macOS esto no funciona igual, lo cual es frustrante: https://unix.stackexchange.com/a/774145
    • Esto me hizo recordar cómo GNU Guile maneja la limitación de un solo argumento con un shebang de varias líneas
      Pones #!/usr/bin/guile \ y en la siguiente línea -e main -s, luego lo cierras con !#, y termina siendo algo como /usr/bin/guile -e main -s filename
      Me pregunto por qué se tomaron la molestia de hacerlo así. Supongo que env -S se agregó hace relativamente poco, o no estaba disponible en todas las plataformas que les importaban
      https://www.gnu.org/software/guile/manual/html_node/The-Meta-Switch.html
    • Está bueno usar este enfoque junto con tusk, un CLI en Go parecido a make pero que usa YAML para la configuración
      Puedes poner #!/usr/bin/env -s go run github.com/rliebz/tusk@latest -f como shebang, y usar como intérprete interpreter: go run mvdan.cc/sh/v3/cmd/gosh@latest -s, que es el shell de Go llamado gosh
      Así obtienes un CLI ejecutable en cualquier arquitectura con solo tener Go instalado
    • Si el wrapper coopera, también se puede meter más información en las líneas siguientes
      Por ejemplo, nix-shell usa #!/usr/bin/env nix-shell y en la línea siguiente #!nix-shell --pure -i runghc ./default.nix para indicar dependencias y argumentos, y después puedes seguir con código Haskell
    • env -S en realidad nunca debería haber sido necesario
      Las extrañas reglas de separación por espacios en la línea shebang son una vieja falla que terminó convertida en una imperfección intocable de Unix
      Cada vez que veo estos trucos pienso que con solo 30 minutos de trabajo en los años 80 se podrían haber evitado muchos años de frustración. La línea shebang debió dividirse desde el principio como /bin/sh
  • No es sobre shebangs, pero últimamente he estado usando uv como herramienta de instalación
    Como empaquetar y distribuir herramientas CLI de Python con dependencias complejas es difícil, lo he usado de dos maneras
    Una es copiar un script de instalación curl | sh y agregar al final uv tool install --python python3.12 my-tool, para que el usuario instale el CLI con una sola línea; la otra es crear un pequeño paquete “instalador” en PyPI que solo tenga uv como dependencia y que luego ejecute uv tool install
    En ambos casos se instala la herramienta CLI, Python 3.12 y todas las dependencias de Python en un entorno aislado. El usuario no tiene que gestionar entornos virtuales, y en el enfoque de curl | sh ni siquiera necesita tener Python instalado
    Desde entonces han bajado muchísimo los issues en GitHub causados por usuarios que rompen dependencias complejas
    Más detalles y enlaces al código: https://aider.chat/2025/01/15/uv.html

    • No he revisado cómo funciona internamente, pero si además de la herramienta CLI también instala Python 3.12, me imagino que en el sistema del usuario final harían falta un compilador, headers del kernel y headers de bibliotecas como openssl-dev, gzip y ffi, además de compilar Python
      Al menos esa fue mi experiencia al usar ASDF-VM, por cómo funcionan por dentro otras herramientas de instalación de Python
    • Está bueno. Parece que mise y aqua también tienen un concepto útil de instalación de herramientas
  • Ya que salió el tema de shebangs curiosos, hice esto para ejecutar archivos Prolog .pl desde el shell
    /*usr/bin/env scryer-prolog "$0" "$@" ; exit #*/
    Probablemente funcione en general con lenguajes de scripting que tengan comentarios /**/, pero no soporten directamente comentarios # ni shebang #!
    Comentario original: https://github.com/mthom/scryer-prolog/issues/2170#issuecomment-1821713993
    Este archivo es un archivo de shell válido y al mismo tiempo un archivo Prolog válido. Si se ejecuta como shell, la primera línea busca el patrón glob /*usr/bin/env y ejecuta /usr/bin/env, y env luego ejecuta scryer-prolog "test.pl". Del lado de Prolog, la primera línea se ignora porque es un comentario /* ... */
    Después, el shell ejecuta exit después de ; para no interpretar el resto del código Prolog como shell, y # hace que el shell ignore */, que es el cierre de comentario de Prolog, evitando así la salida de errores
    Puede que sea lo mejor y lo peor que he hecho

    • De forma parecida, también hay una manera de hacer ejecutable código fuente Go: https://unix.stackexchange.com/questions/162531/shebang-starting-with
      Además, los programas en C también se pueden hacer ejecutables usando una primera línea con la forma //usr/bin/env sh -c .... Al ejecutarlo, se compila con make y luego se corre el binario resultante, así que si modificas el código fuente y lo vuelves a ejecutar, ves de inmediato la salida actualizada
    • No se me ocurrió a mí, pero en TCL también se usa algo así
      Después de #!/bin/sh, puedes poner en la siguiente línea # the next line restarts using tcl \ y hacer que ejecute exec tclsh "$0" "$@"
      Aprovecha que en TCL hasta las líneas de comentario permiten continuación de línea
    • Esto de verdad es impresionante. Si se ajusta un poco la redirección de entrada/salida de Scryer, parece que podría convertirse en un reemplazo perfecto para los scripts de bash
  • El artículo de Trey Hunner, Lazy self-installing Python scripts with uv, tiene más detalles y ejemplos
    https://treyhunner.com/2024/12/lazy-self-installing-python-scripts-with-uv/

    • En los ejemplos, el “script que imprime 80 ceros” también se puede hacer en shell con printf %.1s 0{1..80} $'\n' o for i in {1..80}; do echo -n 0; done; echo
      Tampoco me queda muy claro en qué se diferencia el ejemplo de ffmpeg de 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
      Sinceramente, la versión en Python se ve más compleja
  • Para tareas pequeñas, este truco también se puede usar junto con mise (mise-en-place): https://mise.jdx.dev/tasks/toml-tasks.html#shell-shebang
    La idea es poner uv = 'latest' en tools, y dentro del bloque run de la tarea declarar #!/usr/bin/env -S uv run --script y dependencies = ["requests<3", "rich"], y luego escribir el código Python

  • Hace 16 días también hubo un artículo relacionado y discusión: Uv's killer feature is making ad-hoc environments easy https://news.ycombinator.com/item?id=42676432 (502 puntos, 417 comentarios)

  • Es un truco que vale la pena recordar
    Hace poco cambié mi alias de Python a uv run python y resulta bastante cómodo. No hace falta gestionar .venv ni preocuparse por instalaciones del sistema o instalaciones globales/de usuario, y en Debian ayuda especialmente
    Incluso al ejecutar un REPL dentro de un proyecto o entorno, se puede invocar sin activate, así que hay menos cosas en qué pensar
    Eso sí, cuando llamas un .py directamente con uv, las rutas de archivos del proyecto/entorno se resuelven según el directorio de trabajo actual, no según la ubicación del archivo .py, y eso es una desventaja
    Existe la opción explícita --project, así que se puede usar como uv run --project /script.py, pero estaría bueno que hubiera una opción de proyecto relativa al archivo objetivo para reducir repeticiones

  • He usado uv o uvx como ejecutor de comandos y me he topado con algunas trampas, pero la mayoría de las veces funciona bien, y cuando funciona ahorra muchísimo tiempo
    Como desarrollador de Python y persona curiosa, tengo el directorio home lleno de clones superficiales y directorios de pruebas de un solo uso, así que es realmente útil que reduzca esa carga de mantenimiento
    Quiero robarme esta idea. Me pregunto si usan una extensión aparte en vez de .py, o si el hecho de que un archivo .py tenga el bit de ejecución activado y un shebang sirve como señal para ejecutarlo directamente como script en vez de con el Python del sistema

    • Durante el último año he estado usándolo para ver si valía la pena recomendar uv, y he ido juntando una lista de trampas; sorprendentemente, no han sido muchas
      No son cero, pero las he estado buscando bastante en serio. Si puedes contar qué trampas encontraste, me gustaría agregarlas a la lista
    • Según entendí, la idea es usar .py con el bit de ejecución y la línea shebang que aparece en el artículo
    • De verdad me da curiosidad saber qué trampas hubo
      Con Pipenv tuve problemas cuando cierta dependencia tenía ruedas distintas según la plataforma, mientras que con Poetry funcionó bien
      El archivo de bloqueo de Pipenv solo incluía las dependencias de la plataforma donde se bloqueó, pero Poetry agregaba al lockfile todas las variantes de plataforma
      Todavía no he encontrado documentación sobre cómo se comporta uv en ese caso
  • La idea central es esta
    Un PEP publicado recientemente define una forma de que los scripts de Python declaren dependencias en comentarios del encabezado, y uv es un ejecutor de scripts de Python y administrador de paquetes que escanea esas dependencias, las satisface y luego ejecuta el script con esos módulos disponibles para importar
    Si en el shebang, que es el comentario de la primera línea del script de Python, haces que el archivo invoque uv en lugar de python, obtienes ese efecto automáticamente al “ejecutar” el script
    En consecuencia, hasta los scripts de Python con dependencias que requieren configuración se pueden ejecutar sin más

    • Eso sí, uv debe estar instalado en $PATH
      Y aunque uv instalará las dependencias, no está claro si con este método también instala automáticamente el intérprete de Python
    • En última instancia, solo “se ejecuta sin más” después de instalar uv en el sistema y poner líneas de dependencias al inicio de todos los scripts
      Personalmente, no me parece que eso ahorre tiempo frente a la forma antigua de crear el entorno e invocarlo
      Tampoco tengo claro si uv puede cargar el entorno solo en sesiones interactivas o si se usa únicamente para ejecutar scripts
  • Si te gusta TypeScript, también puedes usar este mismo enfoque con Deno. Supongo que con Bun también se puede, pero no lo he probado
    Se escribe como #! /usr/bin/env -S deno run, y si necesitas flags de permisos puedes agregarlas como #! /usr/bin/env -S deno run --allow-env --allow-read --allow-net