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> " }.

Redefining the `prompt` function in Windows PowerShell
Redefining the 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')TESTrn$(Set-Color 'rW')TESTrn$(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.