Modern PowerShell setup
Everyday A fast, well-equipped PowerShell 7 for daily work. Tested on PowerShell 7.6 with PSReadLine 2.4.5.Source of truth
Docs: What's new in PowerShell 7.6
Support: 7.6 is the LTS release, supported until November 2028
Windows still ships PowerShell 5.1 as powershell. The current one is a separate install, run as pwsh, and has its own profile.
Install
winget install --id Microsoft.PowerShell --exact --source winget
winget install --id JanDeDobbeleer.OhMyPosh --exact --source winget
winget install --id ajeetdsouza.zoxide --exact --source winget
winget install --id junegunn.fzf --exact --source winget
winget install --id BurntSushi.ripgrep.MSVC --exact --source winget
winget install --id sharkdp.fd --exact --source winget
winget install --id sharkdp.bat --exact --source wingetOpen a new window so the tools are on your PATH. Then, in pwsh:
Install-PSResource -Repository PSGallery -Scope CurrentUser -TrustRepository -Name PSReadLine, CompletionPredictor, Microsoft.WinGet.CommandNotFound, Terminal-Icons, PSScriptAnalyzer, PesterCheck the names carefully. Lookalike packages exist on the Gallery, such as PSReadLinee.
Nerd Font
Prompt icons show as boxes without one.
oh-my-posh font install CascadiaCodeThis installs per user, so no admin is needed. In Windows Terminal, select CaskaydiaCove NF. The name CaskaydiaCove Nerd Font doesn't match, and Terminal falls back to another font.
Profile
Edit it with code $PROFILE. The file lives at Documents\PowerShell\Microsoft.PowerShell_profile.ps1.
Exit early when there's no console
PSReadLine errors when input or output is redirected. Put this first:
if ([Console]::IsInputRedirected -or [Console]::IsOutputRedirected -or -not [Environment]::UserInteractive) { return }Load slow modules after the prompt appears
Terminal-Icons alone took over 200 ms to import in testing. This opens the shell first, then loads them:
$null = Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -MaxTriggerCount 1 -Action {
foreach ($m in 'Terminal-Icons', 'CompletionPredictor', 'Microsoft.WinGet.CommandNotFound') {
Import-Module $m -Global -ErrorAction SilentlyContinue
}
}Better suggestions
Set-PSReadLineOption -PredictionSource HistoryAndPlugin
Set-PSReadLineOption -PredictionViewStyle ListViewCompletionPredictor adds tab-completion results to your suggestions. CommandNotFound suggests the winget package when a command is missing.
Keep secrets out of history
A custom history handler replaces PSReadLine's built-in filter, so call it yourself. A filter that only checks token= lets --token abc reach your history file.
Set-PSReadLineOption -AddToHistoryHandler {
param($line)
if ($line -match '(?i)(password|passwd|token|apikey|api_key|secret)\s*[=:]') {
return [Microsoft.PowerShell.AddToHistoryOption]::SkipAdding
}
[Microsoft.PowerShell.PSConsoleReadLine]::GetDefaultAddToHistoryOption($line)
}| Line | Result |
|---|---|
git status | Saved to file |
tool --token abc | Memory only |
tool -Password abc | Memory only |
export API_KEY=abc | Not saved |
Prompt and smarter cd
oh-my-posh init pwsh --config "$PSScriptRoot\themes\my-theme.omp.json" | Invoke-Expressionzoxide goes last in the profile:
Invoke-Expression (& { (zoxide init powershell | Out-String) })Then use z project and zi. Its first run can be slow, and caching the output in a file saves time on later starts.
Fuzzy finding
$env:FZF_DEFAULT_COMMAND = 'fd --type f --hidden --exclude .git --exclude node_modules'
# Find a file and open it
function fe { $f = fzf --preview 'bat --color=always --style=numbers --line-range=:200 {}'; if ($f) { code $f } }Bind fzf to Ctrl+R for history search with Set-PSReadLineKeyHandler. Windows Terminal takes some keys first, such as Ctrl+Shift+T for New Tab, so a binding on those never fires.
PowerShell, CMD and Bash
PowerShell launches almost anything, but it doesn't understand all CMD or Bash syntax.
| Command type | Works? | Use instead |
|---|---|---|
Any .exe, .bat or .cmd | Yes | |
&& and || | PowerShell 7 only | |
mklink, assoc, ftype | Indirectly | cmd /c mklink ... |
dir /s, del /q | Not as typed | Get-ChildItem -Recurse, or cmd /c dir /s |
%VAR% | No | $env:VAR |
export X=1 | No | $env:X = 1 |
rm -rf | No | rm -r -fo |
grep | No | rg |
| Bash scripts | No | Git Bash or WSL |
| Admin commands | Only when elevated | sudo, or an admin window |
What's new
In 7.6:
- Dozens of tab completion improvements.
- Missing-command suggestions (
PSFeedbackProvider) are now standard. - Output can be redirected straight into a variable (
PSRedirectToVariable).
In 7.7 (prerelease, pwsh-preview):
- New
New-TemporaryDirectorycmdlet. New-Guidcreates v7 time-sortable UUIDs by default, which can affect scripts expecting v4.
Write better scripts
Invoke-ScriptAnalyzer -Path . -Recurse
Invoke-Pester -Path . -Output DetailedInstall the PowerShell extension for VS Code to get both as you type.