Published in Articles on by Michiel van Oosterhout ~ 8 min read
Now that we've configured the prompt in Bash and Windows Command Processor, it's time to do the same for Windows PowerShell. Because of its module system, we will not attempt to implement the Commando extension system for PowerShell. Instead we'll implement a quick solution now, and revisit it later, when we've set up our system for PowerShell module development.
The Windows PowerShell prompt
The prompt in Windows PowerShell is rendered by the built-in prompt
function. By default this function is implemented like this:
"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) ";
So the function just returns a string, and the function is called for every prompt, so dynamic prompts with contextual information are possible. The prompt
function can simply be redefined in your current session to change the default prompt: Function prompt { "PS> " }
.

prompt
function in Windows PowerShell
Of course we'd prefer to do this only once, rather than everytime we start a new session. Just like Windows Command Processor has the HKCU:\Software\Microsoft\Command Processor\AutoRun
registry value, and Bash has the .bashrc
and .bash_profile
files, Windows PowerShell has the $PROFILE
automatic variable for one-time configuration.
The Windows PowerShell profile system
The $PROFILE
variable has 4 properties, each containing the path to a file that can be used to configure Windows PowerShell, for all users or the current user, and for all hosts or the current host. We will configure a custom prompt for the current user and for all hosts, so we're interested in the value of $PROFILE.CurrentUserAllHosts
, which is $HOME\Documents\WindowsPowerShell\profile.ps1
.
Configuring Windows PowerShell via profile.ps1
For Bash we kept the .bashrc
and .bash_profile
files very simple, just sourcing all $USERPROFILE\.config\bash\interactive.d\*.bash
and .$USERPROFILE\.config\bash\login.d\*.bash
files respectively (and sourcing .bashrc
from .bash_profile
). This made it easy to add configuration without breaking existing configuration, just by adding a Bash script to one of those directories.
For now, we can do something similar in the Windows PowerShell profile script: source all PowerShell files in a specific directory. The proper directory would be %USERPROFILE%\Documents\WindowsPowerShell\Scripts
, which is where PowerShellGet script packages are installed by default.
Not all scripts in this directory will be suitable to source from profile.ps1
, so let's limit ourselves to only those scripts with the profile
tag. The Windows PowerShell script below creates the current user, all hosts profile script:
# Ensure the profile directory exists
$path = $PROFILE.CurrentUserAllHosts
$_ = New-Item (Split-Path $path) -Force -ItemType Directory
# Declare the profile script contents
$script = @'
foreach ($script in (Get-InstalledScript))
{
if ($script.Tags.Contains("profile"))
{
$path = Join-Path -Path $script.InstalledLocation -ChildPath "$($script.Name).ps1"
. $path
}
}
'@
# Create the profile script
Set-Content -Path $path -Value $script
With this script in place, any installed script tagged profile
will automatically be sourced at the start of every Windows PowerShell session.
Publishing a profile script
What is left now is to create and install a script that will create a custom prompt. For now we will publish this script to a temporary local package source, because it's not yet ready to be published to a public repository. The Windows PowerShell script below first takes care of this part, after which we can focus on the script itself.
# Get the temporary directory
$temp = [IO.Path]::GetTempPath()
# Create a temporary package source directory
$packageSourcePath = Join-Path $temp ([IO.Path]::GetRandomFileName())
$_ = New-Item $packageSourcePath -Force -ItemType Directory
# Register a temporary package source
Register-PackageSource -Trusted -Provider PowerShellGet -Name Temp -Location $packageSourcePath
# Create a temporary directory
$scriptPath = Join-Path $temp ([IO.Path]::GetRandomFileName())
$_ = New-Item $scriptPath -Force -ItemType Directory
# Declare the placeholder script contents
$script = @'
<#PSScriptInfo
.VERSION 1.0.0
.GUID 88e8b433-2aa2-45fb-8e0e-0e1d696809d6
.AUTHOR Michiel van Oosterhout
.TAGS profile
#>
<#
.DESCRIPTION
Exports a custom prompt function
#>
'@
# Create the script
$scriptPath = Join-Path -Path $scriptPath -ChildPath "prompt.ps1"
Set-Content -Path $scriptPath -Value $script
# Publish a script package
Publish-Script -Path $scriptPath -Repository Temp
# Install the script package
Install-Script -Force -Name prompt -Repository Temp
# Unregister the temporary package source
Unregister-PackageSource -Source Temp
# Delete the temporary directories
Remove-Item -Path $scriptPath -Recurse -Force
Remove-Item -Path $packageSourcePath -Recurse -Force
You might be prompted to download and install NuGet.exe
from https://aka.ms/psget-nugetexe
, which is a requirement. It will be installed to %LOCALAPPDATA%\Microsoft\Windows\PowerShell\PowerShellGet
by default.
Configuring a custom prompt
Now we can append our actual script to the installed prompt script. The script will contain a function to determine the current directory, a function to determine the current Git branch, tag, or commit, a function to create colored output, and finally the prompt function which will check if $Env:PS_PROMPT
is set, and if so, it will use it to set the PowerShell prompt.
Getting the current directory
The first section of the prompt script defines a function to get the shortened current working directory.
# Get the prompt script
$path = (Get-InstalledScript -Name prompt).InstalledLocation
$path = Join-Path -Path $path -ChildPath "prompt.ps1"
# Declare the script contents
$script = @'
function Get-PromptDir
{
$location = $executionContext.SessionState.Path.CurrentLocation
return $location.ToString().Replace($Env:USERPROFILE, "~")
}
'@
# Append the script contents
Add-Content -Path $path -Value $script
This function can be used to set Env:PROMPT_DIR
before configuring the prompt.
Getting the current Git branch, tag, or commit
The second section of the prompt script defines a function to get the current Git branch, tag, or commit.
# Get the prompt script
$path = (Get-InstalledScript -Name prompt).InstalledLocation
$path = Join-Path -Path $path -ChildPath "prompt.ps1"
# Declare the script contents
$script = @'
function Get-GitAt
{
# Check that current directory is in a Git repository
git rev-parse --is-inside-work-tree 2>&1 | Out-Null
if ($?)
{
# Get current branch
$result = $(git branch --show-current)
# Get first tag instead
if (-not $result)
{
$result = $(git --no-pager tag --points-at HEAD)
if ($result -is [array])
{
$result = "[$($result[0])]"
}
elseif ($result -is [string])
{
$result = "[$result]"
}
}
# Get commit ID instead
if (-not $result)
{
$result = $(git rev-parse --short HEAD)
}
return $result
}
}
'@
# Append the script contents
Add-Content -Path $path -Value $script
This function can be used to set Env:PROMPT_GIT_AT
before configuring the prompt.
Setting the color of output
The third section of the prompt script defines a function to set the output color using a two-character color specification, such as bW
for a blue background and a bright white foreground.
# Get the prompt script
$path = (Get-InstalledScript -Name prompt).InstalledLocation
$path = Join-Path -Path $path -ChildPath "prompt.ps1"
# Declare the script contents
$script = @'
function Set-Color
{
param(
[string]$Specification
)
# Colors (normal and bright)
$colors = [HashTable]::new()
$colors["k"] = 0
$colors["r"] = 1
$colors["g"] = 2
$colors["y"] = 3
$colors["b"] = 4
$colors["m"] = 5
$colors["c"] = 6
$colors["w"] = 7
$colors["K"] = 60
$colors["R"] = 61
$colors["G"] = 62
$colors["Y"] = 63
$colors["B"] = 64
$colors["M"] = 65
$colors["C"] = 66
$colors["W"] = 67
$colors["0"] = 9
# Start the SGR terminal sequence
$sequence = "$([char]0x1B)["
# Parse background color
$bg = $Specification.Substring(0, 1)
if ($bg -eq "-")
{
# Parameters for RGB color
$sequence += "48;2;$TERM_BACKGROUND_COLOR"
}
elseif ($bg -eq "+")
{
# Parameters for RGB color
$sequence += "48;2;$TERM_FOREGROUND_COLOR"
}
else
{
# Parameter for palette-based background color
$sequence += $colors[$bg] + 40
}
# Separate the parameters for background color and foreground color
$sequence += ";"
# Parse foreground color
$fg = $Specification.Substring(1, 1)
if ($fg -eq "-")
{
# Parameters for RGB color
$sequence += "38;2;$TERM_BACKGROUND_COLOR"
}
elseif ($fg -eq "+")
{
# Parameters for RGB color
$sequence += "38;2;$TERM_FOREGROUND_COLOR"
}
else
{
# Parameter for palette-based background color
$sequence += $colors[$fg] + 30
}
# Finish the SGR terminal sequence
$sequence += "m"
# Return the SGR terminal sequence
return $sequence
}
'@
# Append the script contents
Add-Content -Path $path -Value $script
This function can be used to create colored output like so: "$(Set-Color 'bW')TEST
rn$(Set-Color 'rW')TEST
rn$(Set-Color '00')TEST"
All that is left now is a function to set the prompt, but we'll add that in the next article.
Conclusion
As with Bash, Windows PowerShell provides some natural extension points, both for initialization and for customizing its prompt. The profile script makes it possible to add utility functions to every session, but at the cost of (significantly) slowing down Windows PowerShell startup.