Skip to content

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 ​

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

Open a new window so the tools are on your PATH. Then, in pwsh:

powershell
Install-PSResource -Repository PSGallery -Scope CurrentUser -TrustRepository -Name PSReadLine, CompletionPredictor, Microsoft.WinGet.CommandNotFound, Terminal-Icons, PSScriptAnalyzer, Pester

Check the names carefully. Lookalike packages exist on the Gallery, such as PSReadLinee.

Nerd Font ​

Prompt icons show as boxes without one.

powershell
oh-my-posh font install CascadiaCode

This 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:

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

powershell
$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 ​

powershell
Set-PSReadLineOption -PredictionSource HistoryAndPlugin
Set-PSReadLineOption -PredictionViewStyle ListView

CompletionPredictor 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.

powershell
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)
}
LineResult
git statusSaved to file
tool --token abcMemory only
tool -Password abcMemory only
export API_KEY=abcNot saved

Prompt and smarter cd ​

powershell
oh-my-posh init pwsh --config "$PSScriptRoot\themes\my-theme.omp.json" | Invoke-Expression

zoxide goes last in the profile:

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

powershell
$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 typeWorks?Use instead
Any .exe, .bat or .cmdYes
&& and ||PowerShell 7 only
mklink, assoc, ftypeIndirectlycmd /c mklink ...
dir /s, del /qNot as typedGet-ChildItem -Recurse, or cmd /c dir /s
%VAR%No$env:VAR
export X=1No$env:X = 1
rm -rfNorm -r -fo
grepNorg
Bash scriptsNoGit Bash or WSL
Admin commandsOnly when elevatedsudo, 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-TemporaryDirectory cmdlet.
  • New-Guid creates v7 time-sortable UUIDs by default, which can affect scripts expecting v4.

Write better scripts ​

powershell
Invoke-ScriptAnalyzer -Path . -Recurse
Invoke-Pester -Path . -Output Detailed

Install the PowerShell extension for VS Code to get both as you type.