A lo largo de los años me encontré escribiendo muchos programas pequeños para ayudarme con mi trabajo. La mayoría están en C# o Python, según la tarea que tenga entre manos. Como Windows no soporta shebangs de forma nativa (la línea #! al comienzo de un script), ejecutar estos programas puede resultar bastante incómodo:
- C#: Como mínimo, necesitás un DLL compilado de tu código y tenés que acordarte de ejecutar
dotnetcada vez. Eso implica que ya construiste el código, que tenés que rastrear la ruta del DLL, actualizarlo cuando haya cambios, y así sucesivamente. - Python: Hay buenas razones para evitar agregar Python de forma permanente a tu
PATH, así que la gestión de entornos se convierte en un problema —además de que igual tenés que recordar dónde está instalado Python.
Por suerte, existen formas de esquivar estos problemas:
- En C#, podés usar dotnet-script , que te permite ejecutar archivos C# como scripts sin compilarlos primero.
- En Python, herramientas como uv o pipx te permiten ejecutar scripts de Python sin una instalación global. ¡También soportan metadatos de script en línea para gestionar entornos virtuales de forma automática por vos!
Todo esto está muy bien, pero sigue sin resolver el problema de tener que ejecutar dotnet script, uv, pipx u otro comando cada vez que querés invocar un script —sin mencionar lo de recordar las rutas completas.
Descubrí que agregar el siguiente fragmento a mi profile.ps1 ayuda bastante:
function Add-PythonScripts {
Get-ChildItem -Path (Resolve-Path "~/.dev/python/*.py") | ForEach-Object {
$scriptName = $_.BaseName
$fullPath = $_.FullName
Set-Item -Path "function:global:$scriptName" -Value {
if (Get-Command "uv" -ErrorAction SilentlyContinue) {
& uv run "$fullPath" @Args
}
elseif (Get-Command "pipx" -ErrorAction SilentlyContinue) {
& pipx run "$fullPath" @Args
}
elseif (Get-Command "python" -ErrorAction SilentlyContinue) {
& python "$fullPath" @Args
}
}.GetNewClosure()
}
}
Add-PythonScripts
function Add-DotnetScripts {
Get-ChildItem -Path (Resolve-Path "~/.dev/dotnet/*.csx") | ForEach-Object {
$scriptName = $_.BaseName
$fullPath = $_.FullName
Set-Item -Path "function:global:$scriptName" -Value {
& dotnet script "$fullPath" -- @Args
}.GetNewClosure()
}
}
Add-DotnetScripts
Estas dos funciones agregan cualquier script .py de ~/.dev/python y cualquier script .csx de ~/.dev/dotnet como funciones globales en tu sesión de PowerShell. De ese modo, podés llamarlos igual que cualquier otro comando, y ellos se encargan automáticamente de los argumentos que les pasás.
Así podría verse un script de Python:
#!/usr/bin/env python
# /// script
# dependencies = ["dep1", "dep2"]
# ///
if __name__ == '__main__':
print('Hello world!')
Y acá va un ejemplo en C#:
#!/usr/bin/env dotnet-script
#r "nuget: NugetPkg1, 1.0.0"
#nullable enable
Console.WriteLine("Hello world!");
Como estos scripts quedan expuestos como funciones nativas de PowerShell, también obtenés el autocompletado con tabulador de PowerShell sobre los nombres, y cualquier otra funcionalidad que esperarías de comandos normales.