powershell

PowerShell

Installation#

powershell
winget install --id Microsoft.PowerShell
powershell
winget install --id Starship.Starship
powershell
winget install gerardog.gsudo

Usage#

powershell
Set-PSReadlineKeyHandler -Key Tab -Function MenuComplete
powershell
# la — matching the Unix convention: long format + hidden entries.# It has to be a function — a PowerShell alias cannot carry a fixed argument# (-Force here). And aliases outrank functions, so the existing Set-Alias la# has to go firstRemove-Item Alias:la -Force -ErrorAction Ignore
function la { Get-ChildItem -Force @args }

-Force is the counterpart to Unix -A: it makes la list hidden and system entries.

No ll is defined — PowerShell never had one, and cross-platform alignment only covers the command actually typed.

powershell
function i {    param (        [string]$DirectoryName    )
    Set-Location -Path "$HOME\i\$DirectoryName"}

Git no longer goes through the posh-git / git-aliases modules — the functions are defined by hand instead, see the git page.

Speeding Up Startup#

Nearly all of the profile's cost is spawning subprocesses. Measured medians of pwsh -Command "exit":

Time
pwsh -NoProfile (baseline)151 ms
Before630 ms
After524 ms

starship gets launched twice#

starship init powershell prints exactly one line:

powershell
Invoke-Expression (& 'C:\Program Files\starship\bin\starship.exe' init powershell --print-full-init | Out-String)

So running it launches starship a second time. Ask for the full script directly and cache it to a file, which removes that whole round trip:

powershell
$__cacheDir = "$HOME\.cache\pwsh"if (-not (Test-Path $__cacheDir)) { New-Item -ItemType Directory $__cacheDir -Force | Out-Null }
$__f = "$__cacheDir\starship.ps1"$__src = (Get-Command starship -ErrorAction SilentlyContinue).Sourceif ($__src -and ((-not (Test-Path $__f)) -or (Get-Item $__src).LastWriteTime -gt (Get-Item $__f).LastWriteTime)) {    starship init powershell --print-full-init | Out-String | Set-Content $__f -Encoding utf8}if (Test-Path $__f) { . $__f }

Regeneration keys off the binary's LastWriteTime, so a winget upgrade refreshes the cache on its own — no manual clearing.

The dot-source has to sit at the top level of the profile. Wrap this in a function and . $__f only applies inside that function's scope: the prompt never reaches global scope, and the symptom is "the cache ran but the prompt didn't change".

mise's completion script is cached the same way, but it must come after mise activate — the completions shell out to usage at runtime, and usage is itself a mise-managed tool that is not on PATH before activation, so every new shell would print usage CLI not found. See the mise page.

The part that cannot be reduced#

The single Set-PSReadlineKeyHandler line costs about 183 ms, which is really the first load of the PSReadLine module, not the key binding. An interactive session loads that module anyway, so moving or deferring the line just pushes the cost to the first keystroke — it does not feel faster.

mise activate cannot be cached either: it has to run every time to resolve versions for the current session and install the directory-change hook.

powershell-profile#