Over the years, I've found myself writing many small programs to help with my work. Most of these are in either C# or Python, depending on the task at hand. Because Windows doesn't natively support shebangs (the `#!` line at the top of a script), running these programs can be a bit cumbersome:

- **C#:** At a minimum, you need a compiled DLL for your code, and you must remember to run `dotnet` every time. This implies you've already built the code, and you also have to keep track of the DLL path, update the DLL when changes occur, and so on.
- **Python:** There are good reasons to avoid permanently adding Python to your `PATH`, so environment management becomes an issue—plus you still have to remember where Python is installed.

Fortunately, there are ways around these hassles:

- In C#, you can use [dotnet-script](https://github.com/dotnet-script/dotnet-script), which allows you to run C# files as scripts without compiling them first.
- In Python, tools like [uv](https://github.com/astral-sh/uv) or [pipx](https://github.com/pypa/pipx) let you run Python scripts without a global install. They also support [inline script metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/#inline-script-metadata) to automatically manage virtual environments for you!

All of this is great, but it still doesn't solve the problem of having to run `dotnet script`, `uv`, `pipx`, or any other command each time you want to invoke a script—not to mention remembering the full paths.

I've found that adding the following snippet to my `profile.ps1` helps a lot:

```powershell
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
```

These two functions add any `.py` scripts from `~/.dev/python` and `.csx` scripts from `~/.dev/dotnet` as global functions in your PowerShell session. That way, you can call them just like any other command, and they'll automatically handle the arguments you pass along.

Here's what a Python script might look like:

```python
#!/usr/bin/env python
# /// script
# dependencies = ["dep1", "dep2"]
# ///

if __name__ == '__main__':
    print('Hello world!')
```

And here's an example in C#:

```csharp
#!/usr/bin/env dotnet-script
#r "nuget: NugetPkg1, 1.0.0"
#nullable enable

Console.WriteLine("Hello world!");
```

Because these scripts are exposed as native PowerShell functions, you also get PowerShell tab autocompletion on the names, and any other features you'd expect from normal commands.