The Tao of the Shell: The Prompt

Been trying to find time to get back to my WinForms using Classes series but work and home life have me slammed. So I thought I’d kick of a series of disjointed but hopefully fun and interesting snippets. Lots of these will just be my own interpretations of things that probably exist out there already these are just my own mainly QOL things I’ve put together.

Before you can get the right answer you must FIRST prompt the right question.

Sure I might have swapped Asked for Prompt but it’s MY poorly conceived segue so I’m sticking to it. Anyways the Prompt, good old PS: C:\> nice, unassuming, elegant, simp…SWEET MERCIFUL BALLMER what is THAT?!?!

PS C:\Users\Administrator\AppData\Roaming\Mozilla\Firefox\Profiles\ac8x904e.default-release\storage\default\moz-extension+++808770c0-f3e6-4021-844a-4d95c4217ba2^userContextId=4294967295\idb\3647222921wleabcEoxlt-eengsairo.files> 

Right there! Pretty sure that (or something similar to that) was where something snapped in my brain. Too many times having to jump into a powershell shell from some ungodly long unc path or local path riddled with guids and version numbers leaving tiny bits of space after the > to type out my commands. Well NO MORE!!

So let’s talk about what we’re going do to fix this. Might as well get the disclaimer out of the way. There are a decent amount of people’s solutions for changing up the powershell prompt. Ranging from the insanely detailed to the oddly specific. This one is not meant to be either of those it’s a practical solution to a recurring problem I run into. Along the way we can learn a few things.

Hopefully our intention is clear from that contrived path example though it DOES accurately represent some of the INSANE UNC, or Registry paths you can run across. But if its not we want to maximize how much we have left to type out our commands after our prompt without sacrificing the information we want to know. We’ve got 2 basic means to that end Condensing, and Wrapping, at the end we’ll actually combine both of them.

First Condensing:

Function prompt {
    # Setup to show when we are debugging
    $Start = If ($PSDebugContext) { "[DBG]" } Else {""}
    # Setup how many > we need
    $End = '>' * ($nestedPromptLevel + 1)

    # When we glue our path back together this will be what we use
    $Joiner = "\"
    # Chop up our path
    $PathParts = $ExecutionContext.SessionState.Path.CurrentLocation -split '\\'

    # Set How Many Child Path parts we want
    $ChildCount = 2
    # Set how many Parent Path parts we want
    $ParentCount = 2
    # If we don't have enough to condense we just return the full path glued together with our Joiner
    If ($PathParts.Count -le $ParentCount + $ChildCount) {
        "$Start{PSC} $($PathParts -join $Joiner)$End"
    }
    # Otherwise we grab the ParentCount of path items from the front and ChildCount from the end
    # leaving .. in the middle
    Else {
        "$Start{PSC} $(
            $PathParts[0..($ParentCount - 1)] -join $Joiner)$Joiner..$Joiner$(
                $PathParts[($PathParts.Count - $ChildCount)..($PathParts.Count)] -join $Joiner)$End"
    }
}

First we initialize some variables for later (Check if we’re debugging, setup our joiner,our condensing limiters, and our >’s), and chop up our path

# Setup to show when we are debugging
$Start = If ($PSDebugContext) { "[DBG]" } Else {""}
# Setup how many > we need
$End = '>' * ($nestedPromptLevel + 1)

# When we glue our path back together this will be what we use
$Joiner = "\"
# Chop up our path
$PathParts = $ExecutionContext.SessionState.Path.CurrentLocation -split '\\'

# Set How Many Child Path parts we want
$ChildCount = 2
# Set how many Parent Path parts we want
$ParentCount = 2

Next up we have to check if the total of our ParentCount and ChildCount limiters are greater than the total count of our path parts. If so we just join our path parts with our Joiner and return our newly formed prompt

# If we don't have enough to condense we just return the full path glued together with our Joiner
If ($PathParts.Count -le $ParentCount + $ChildCount) {
    "$Start{PSC} $($PathParts -join $Joiner)$End"
}

Or if we have MORE path parts in which case we use the array indexers to grab a ParentCount’s worth of path parts from the front, a ChildCount’s worth from the back, join them all with the delimiter and return our alternative Prompt.

# Otherwise we grab the ParentCount of path items from the front and ChildCount from the end
# leaving .. in the middle
Else {
    "$Start{PSC} $(
        $PathParts[0..($ParentCount - 1)] -join $Joiner)$Joiner..$Joiner$(
            $PathParts[($PathParts.Count - $ChildCount)..($PathParts.Count)] -join $Joiner)$End"
}

That’s it. Now our prompt from before is a more manageable

{PSC} C:\Users..\idb\3647222921wleabcEoxlt-eengsairo.files>

Second Wrapping:

Our other option is wrapping. This one is a lot simpler than Condensing we’re basically just swapping our path delimiter

Function prompt {
    # Setup to show when we are debugging
    $Start = If ($PSDebugContext) { "[DBG]" } Else {""}
    # Setup how many > we need
    $End = '>' * ($nestedPromptLevel + 1)

    # When we glue our path back together this will be what we use
    $Joiner = "`r`n"
    # Chop up our path
    $PathParts = $ExecutionContext.SessionState.Path.CurrentLocation -split '\\'

    "$Start{PSML}$Joiner$($PathParts -join $Joiner)$Joiner$End"
}

We start out almost exactly like Condensing setting up our starting variables the only difference is our Joiner is now

"`r`n"
# Setup to show when we are debugging
$Start = If ($PSDebugContext) { "[DBG]" } Else {""}
# Setup how many > we need
$End = '>' * ($nestedPromptLevel + 1)

# When we glue our path back together this will be what we use
$Joiner = "`r`n"
# Chop up our path
$PathParts = $ExecutionContext.SessionState.Path.CurrentLocation -split '\\'

Then its a single line to put it all back together. We do add a final Joiner at the end so our >’s end up alone on a line

"$Start{PSML}$Joiner$($PathParts -join $Joiner)$Joiner$End"

So now our same prompt looks like this

{PSML}
C:
Users
Administrator
AppData
Roaming
Mozilla
Firefox
Profiles
ac8x904e.default-release
storage
default
moz-extension+++808770c0-f3e6-4021-844a-4d95c4217ba2^userContextId=4294967295
idb
3647222921wleabcEoxlt-eengsairo.files
>

Yeah its a LOT of lines BUT the point was to give us the MAXIMUM line space to type out our commands and JUST > is pretty darn close.

But what if we want the best of both worlds? well

Third Part By Our Powers Combined…

This one will look uncannily familiar to Condensed. As a matter of fact there is only 3 things different. More on that later.

Function prompt {
    # Setup to show when we are debugging
    $Start = If ($PSDebugContext) { "[DBG]" } Else {""}
    # Setup how many > we need
    $End = '>' * ($nestedPromptLevel + 1)

    # When we glue our path back together this will be what we use
    $Joiner = "`r`n"
    # Chop up our path
    $PathParts = $ExecutionContext.SessionState.Path.CurrentLocation -split '\\'

    # Set How Many Child Path parts we want
    $ChildCount = 2
    # Set how many Parent Path parts we want
    $ParentCount = 2
    # If we don't have enough to condense we just return the full path glued together with our Joiner
    If ($PathParts.Count -le $ParentCount + $ChildCount) {
        "$Start{PSMLC}`r`n$($PathParts -join $Joiner)`r`n$End"
    }
    # Otherwise we grab the ParentCount of path items from the front and ChildCount from the end
    # leaving .. in the middle
    Else {
        "$Start{PSMLC}`r`n$(
            $PathParts[0..($ParentCount - 1)] -join $Joiner)$Joiner..$Joiner$(
                $PathParts[($PathParts.Count - $ChildCount)..($PathParts.Count)] -join $Joiner)`r`n$End"
    }
}

We start of pretty much the same as condensed just swapping out our Joiner’s "\" for

"`r`n"
# Setup to show when we are debugging
$Start = If ($PSDebugContext) { "[DBG]" } Else {""}
# Setup how many > we need
$End = '>' * ($nestedPromptLevel + 1)

# When we glue our path back together this will be what we use
$Joiner = "`r`n"
# Chop up our path
$PathParts = $ExecutionContext.SessionState.Path.CurrentLocation -split '\\'

# Set How Many Child Path parts we want
$ChildCount = 2
# Set how many Parent Path parts we want
$ParentCount = 2

Next up is our same check if we’re condensing more than we started with

# If we don't have enough to condense we just return the full path glued together with our Joiner
If ($PathParts.Count -le $ParentCount + $ChildCount) {
    "$Start{PSMLC}`r`n$($PathParts -join $Joiner)`r`n$End"
}

Followed by our same alternative when we have more to condense

# Otherwise we grab the ParentCount of path items from the front and ChildCount from the end
# leaving .. in the middle
Else {
    "$Start{PSMLC}`r`n$(
        $PathParts[0..($ParentCount - 1)] -join $Joiner)$Joiner..$Joiner$(
            $PathParts[($PathParts.Count - $ChildCount)..($PathParts.Count)] -join $Joiner)`r`n$End"
}

Now our same prompt looks like this

{PSMLC}
C:
Users
..
idb
3647222921wleabcEoxlt-eengsairo.files
>

A good balance between the two. We’ve still got a little cleanup work to do though. Time to Reset back to the original (or as close as we can get) Prompt

Fourth Part the Great Reset

Function prompt {
    # Setup to show when we are debugging
    $Start = If ($PSDebugContext) { "[DBG]" } Else {""}
    # Setup how many > we need
    $End = '>' * ($nestedPromptLevel + 1)

    # Set us back to as close to the original function
    "$($Start)PS $($executionContext.SessionState.Path.CurrentLocation)$End"
}

Fairly close to what the prompt function looks like out of the box. As a small point of trivia it seems the prompt function is actually regenerated from the shell itself when you are debugging or not debugging something unless you have overridden the function with your own code so you HAVE to include your own check for Debugging even when you reset it back to default.

There we go we’ve taken our Prompt from an unwieldy monster into several tame little things we can work better with but you might be thinking “what if I want 3 Parents and only 1 Child paths?” or “What if I want to switch between these ‘Styles’ of prompts?” and you would do well to think like that. So for our final trick we’re going to setup a Module with a single Cmdlet Set-Prompt and its going to let us change our prompt on the fly with a single line.

Fifth Part I love It When a Prompt Comes Together

As always we’ll start with the WHOLE shebang then talk breakdown

#region Helpers

Enum PromptStyle {
    Condensed
    MultiLine
    MultiLineCondensed
}

# Helper Class for building our prompt functions
Class PromptParts {

    #region The Hidden Stuff

    # holds our single instance of our class
    hidden static [PromptParts] $Instance

    # Gets or creates our single instance of our class
    hidden static [PromptParts] Get() {
        # Do we have an instance? No? Well Create One
        If ($null -eq [PromptParts]::Instance) { [PromptParts]::Instance = [PromptParts]::new() }
        # Return it
        return [PromptParts]::Instance
    }

    # Create our 1st ScriptBlock that all prompts share
    hidden [ScriptBlock] $AllStartRaw = {
        # Setup to show when we are debugging 
        $Start = If ($PSDebugContext) { "[DBG]" } Else {""}
        # Setup how many > we need
        $End = '>' * ($nestedPromptLevel + 1)
    }

    # Create our 2nd ScriptBlock that ALL Styles share
    hidden [ScriptBlock] $AllStylesRaw = {
        # When we glue our path back together this will be what we use
        $Joiner = "$JoinerString"
        # Chop up our path
        $PathParts = $ExecutionContext.SessionState.Path.CurrentLocation -split '\\'
    }

    # Create our 3rd ScriptBlock for both Condensed Styles
    hidden [ScriptBlock] $CondensedRaw = {
        # Set How Many Child Path parts we want
        $ChildCount = $ChildCountValue
        # Set how many Parent Path parts we want
        $ParentCount = $ParentCountValue
        # If we don't have enough to condense we just return the full path glued together with our Joiner
        If ($PathParts.Count -le $ParentCount + $ChildCount) {
            "$Start{$PromptType}$Token$($PathParts -join $Joiner)$EndToken$End"
        }
        # Otherwise we grab the ParentCount of path items from the front and ChildCount from the end 
        # leaving .. in the middle
        Else {
            "$Start{$PromptType}$Token$(
                $PathParts[0..($ParentCount - 1)] -join $Joiner)$Joiner..$Joiner$(
                    $PathParts[($PathParts.Count - $ChildCount)..($PathParts.Count)] -join $Joiner)$EndToken$End"
        }
    }

    # Create our 3rd ScriptBlock for MultiLine
    hidden [ScriptBlock] $MultiLineRaw = {
        "$Start{PSML}$Joiner$($PathParts -join $Joiner)$Joiner$End" 
    }

    # Create our 2nd ScriptBlock for Reset
    hidden [ScriptBlock] $ResetRaw = { 
        # Set us back to as close to the original function
        "$($Start)PS $($executionContext.SessionState.Path.CurrentLocation)$End" 
    }

    #endregion

    #region The Visible Stuff

    # Glues together all the ScriptBlocks to make Reset
    static [string] $Reset = "$([PromptParts]::Get().AllStartRaw)$([PromptParts]::Get().ResetRaw)"

    # Glues together all the ScriptBlocks to make both Styles of Condensed
    static [string] $Condensed = "$([PromptParts]::Get().AllStartRaw)$([PromptParts]::Get().AllStylesRaw)$([PromptParts]::Get().CondensedRaw)"

    # Glues together all the ScriptBlocks to make Multiline
    static [string] $MultiLine = "$([PromptParts]::Get().AllStartRaw)$([PromptParts]::Get().AllStylesRaw)$([PromptParts]::Get().MultiLineRaw)"

    #endregion
}

#endregion

#region Cmdlets

Function Set-Prompt {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,HelpMessage="Prompt Style", ParameterSetName="Style")]
        [PromptStyle] $Style,
        [Parameter(Mandatory=$true,HelpMessage="Reset To default prompt", ParameterSetName="Reset")]
        [Switch] $Reset
    ) 

    DynamicParam {
        # If a user selects either of the Condensed options we want to prompt how much we want to condense
        $DynamicParameters = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        if ($Style -in @([PromptStyle]::Condensed,[PromptStyle]::MultiLineCondensed )) {
            $Attributes = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
            $ParameterAttribute = [System.Management.Automation.ParameterAttribute]::new()
            $ParameterAttribute.ParameterSetName = "Style"
            $Attributes.Add($ParameterAttribute)
            $Attributes.Add([System.Management.Automation.ValidateRangeAttribute]::new(1,10))
            # How Many Parents do We show
            $ParentCount = [System.Management.Automation.RuntimeDefinedParameter]::new("ParentCount",[int],$Attributes)
            $ParentCount.Value = 1
            $DynamicParameters.Add("ParentCount",$ParentCount)                
            # How Many Children do we show
            $ChildCount = [System.Management.Automation.RuntimeDefinedParameter]::new("ChildCount",[int],$Attributes)
            $ChildCount.Value = 1
            $DynamicParameters.Add("ChildCount",$ChildCount)                
        }
        return $DynamicParameters
    }
    
    process {
        # Simple Reset back to the standard prompt       
        If ($Reset) { 
            $Prompt = [PromptParts]::Reset
            Write-Verbose "Reseting Prompt"
        }
        Else {
            switch ( $Style ) {
                { $_ -in @([PromptStyle]::Condensed, [PromptStyle]::MultiLineCondensed) } { 
                    Write-Verbose "Parent Count: $($ParentCount.Value)"
                    Write-Verbose "Child Count: $($ChildCount.Value)"
                    # Set the Condensing Limiters in the function Body we just created
                    $PromptString = [PromptParts]::Condensed.
                        Replace('$ParentCountValue',$ParentCount.Value).
                        Replace('$ChildCountValue',$ChildCount.Value)
                }
                Condensed {
                    $Prompt = $PromptString.
                        Replace('$JoinerString',"\").
                        Replace('$PromptType',"PSC").
                        Replace('$Token'," ").
                        Replace('$EndToken', "")
                }
                MultiLineCondensed {
                    $Prompt = $PromptString.
                        Replace('$JoinerString',"``r``n").
                        Replace('$PromptType',"PSMLC").
                        Replace('$Token',"``r``n").
                        Replace('$EndToken', "``r``n")
                }
                MultiLine { 
                    $Prompt = [PromptParts]::MultiLine.
                        Replace('$JoinerString',"``r``n")
                }
            }
            Write-Verbose "Set Prompt Style to $Style"
        }
        $CompletedPrompt = [ScriptBlock]::Create($Prompt)
        Write-Verbose "Writing new prompt function:`r`n`r`n$CompletedPrompt"
        Set-Content -Path Function:\prompt -Value $CompletedPrompt
    }
}

#endregion

First up we’ve got some helpers we want to setup.

#region Helpers

Enum PromptStyle {
    Condensed
    MultiLine
    MultiLineCondensed
}

# Helper Class for building our prompt functions
Class PromptParts {

    #region The Hidden Stuff

    # holds our single instance of our class
    hidden static [PromptParts] $Instance

    # Gets or creates our single instance of our class
    hidden static [PromptParts] Get() {
        # Do we have an instance? No? Well Create One
        If ($null -eq [PromptParts]::Instance) { [PromptParts]::Instance = [PromptParts]::new() }
        # Return it
        return [PromptParts]::Instance
    }

    # Create our 1st ScriptBlock that all prompts share
    hidden [ScriptBlock] $AllStartRaw = {
        # Setup to show when we are debugging 
        $Start = If ($PSDebugContext) { "[DBG]" } Else {""}
        # Setup how many > we need
        $End = '>' * ($nestedPromptLevel + 1)
    }

    # Create our 2nd ScriptBlock that ALL Styles share
    hidden [ScriptBlock] $AllStylesRaw = {
        # When we glue our path back together this will be what we use
        $Joiner = "$JoinerString"
        # Chop up our path
        $PathParts = $ExecutionContext.SessionState.Path.CurrentLocation -split '\\'
    }

    # Create our 3rd ScriptBlock for both Condensed Styles
    hidden [ScriptBlock] $CondensedRaw = {
        # Set How Many Child Path parts we want
        $ChildCount = $ChildCountValue
        # Set how many Parent Path parts we want
        $ParentCount = $ParentCountValue
        # If we don't have enough to condense we just return the full path glued together with our Joiner
        If ($PathParts.Count -le $ParentCount + $ChildCount) {
            "$Start{$PromptType}$Token$($PathParts -join $Joiner)$EndToken$End"
        }
        # Otherwise we grab the ParentCount of path items from the front and ChildCount from the end 
        # leaving .. in the middle
        Else {
            "$Start{$PromptType}$Token$(
                $PathParts[0..($ParentCount - 1)] -join $Joiner)$Joiner..$Joiner$(
                    $PathParts[($PathParts.Count - $ChildCount)..($PathParts.Count)] -join $Joiner)$EndToken$End"
        }
    }

    # Create our 3rd ScriptBlock for MultiLine
    hidden [ScriptBlock] $MultiLineRaw = {
        "$Start{PSML}$Joiner$($PathParts -join $Joiner)$Joiner$End" 
    }

    # Create our 2nd ScriptBlock for Reset
    hidden [ScriptBlock] $ResetRaw = { 
        # Set us back to as close to the original function
        "$($Start)PS $($executionContext.SessionState.Path.CurrentLocation)$End" 
    }

    #endregion

    #region The Visible Stuff

    # Glues together all the ScriptBlocks to make Reset
    static [string] $Reset = "$([PromptParts]::Get().AllStartRaw)$([PromptParts]::Get().ResetRaw)"

    # Glues together all the ScriptBlocks to make both Styles of Condensed
    static [string] $Condensed = "$([PromptParts]::Get().AllStartRaw)$([PromptParts]::Get().AllStylesRaw)$([PromptParts]::Get().CondensedRaw)"

    # Glues together all the ScriptBlocks to make Multiline
    static [string] $MultiLine = "$([PromptParts]::Get().AllStartRaw)$([PromptParts]::Get().AllStylesRaw)$([PromptParts]::Get().MultiLineRaw)"

    #endregion
}

#endregion

Starting with a simple Enum. I use Enums whenever I can when I’m dealing with Parameter’s that I want a fixed list of options its cleaner than using a validation list and they “just work” with switches, plus I vibe with the sudo strongly typed nature of using them

Enum PromptStyle {
    Condensed
    MultiLine
    MultiLineCondensed
}

Next our other helper is our PromptParts class. I tend to put things that would normally be a script level or global level variable into a class that I can call wherever I want in my module to retrieve values just keeps things cleaner in my opinion

# Helper Class for building our prompt functions
Class PromptParts {

    #region The Hidden Stuff

    # holds our single instance of our class
    hidden static [PromptParts] $Instance

    # Gets or creates our single instance of our class
    hidden static [PromptParts] Get() {
        # Do we have an instance? No? Well Create One
        If ($null -eq [PromptParts]::Instance) { [PromptParts]::Instance = [PromptParts]::new() }
        # Return it
        return [PromptParts]::Instance
    }

    # Create our 1st ScriptBlock that all prompts share
    hidden [ScriptBlock] $AllStartRaw = {
        # Setup to show when we are debugging 
        $Start = If ($PSDebugContext) { "[DBG]" } Else {""}
        # Setup how many > we need
        $End = '>' * ($nestedPromptLevel + 1)
    }

    # Create our 2nd ScriptBlock that ALL Styles share
    hidden [ScriptBlock] $AllStylesRaw = {
        # When we glue our path back together this will be what we use
        $Joiner = "$JoinerString"
        # Chop up our path
        $PathParts = $ExecutionContext.SessionState.Path.CurrentLocation -split '\\'
    }

    # Create our 3rd ScriptBlock for both Condensed Styles
    hidden [ScriptBlock] $CondensedRaw = {
        # Set How Many Child Path parts we want
        $ChildCount = $ChildCountValue
        # Set how many Parent Path parts we want
        $ParentCount = $ParentCountValue
        # If we don't have enough to condense we just return the full path glued together with our Joiner
        If ($PathParts.Count -le $ParentCount + $ChildCount) {
            "$Start{$PromptType}$Token$($PathParts -join $Joiner)$EndToken$End"
        }
        # Otherwise we grab the ParentCount of path items from the front and ChildCount from the end 
        # leaving .. in the middle
        Else {
            "$Start{$PromptType}$Token$(
                $PathParts[0..($ParentCount - 1)] -join $Joiner)$Joiner..$Joiner$(
                    $PathParts[($PathParts.Count - $ChildCount)..($PathParts.Count)] -join $Joiner)$EndToken$End"
        }
    }

    # Create our 3rd ScriptBlock for MultiLine
    hidden [ScriptBlock] $MultiLineRaw = {
        "$Start{PSML}$Joiner$($PathParts -join $Joiner)$Joiner$End" 
    }

    # Create our 2nd ScriptBlock for Reset
    hidden [ScriptBlock] $ResetRaw = { 
        # Set us back to as close to the original function
        "$($Start)PS $($executionContext.SessionState.Path.CurrentLocation)$End" 
    }

    #endregion

    #region The Visible Stuff

    # Glues together all the ScriptBlocks to make Reset
    static [string] $Reset = "$([PromptParts]::Get().AllStartRaw)$([PromptParts]::Get().ResetRaw)"

    # Glues together all the ScriptBlocks to make both Styles of Condensed
    static [string] $Condensed = "$([PromptParts]::Get().AllStartRaw)$([PromptParts]::Get().AllStylesRaw)$([PromptParts]::Get().CondensedRaw)"

    # Glues together all the ScriptBlocks to make Multiline
    static [string] $MultiLine = "$([PromptParts]::Get().AllStartRaw)$([PromptParts]::Get().AllStylesRaw)$([PromptParts]::Get().MultiLineRaw)"

    #endregion
}

This class has 2 basic sections the first is our hidden fields these hold a single instance of our class, and the actual ScriptBlocks that make up our different styles of Prompt function

#region The Hidden Stuff

# holds our single instance of our class
hidden static [PromptParts] $Instance

# Gets or creates our single instance of our class
hidden static [PromptParts] Get() {
    # Do we have an instance? No? Well Create One
    If ($null -eq [PromptParts]::Instance) { [PromptParts]::Instance = [PromptParts]::new() }
    # Return it
    return [PromptParts]::Instance
}

# Create our 1st ScriptBlock that all prompts share
hidden [ScriptBlock] $AllStartRaw = {
    # Setup to show when we are debugging 
    $Start = If ($PSDebugContext) { "[DBG]" } Else {""}
    # Setup how many > we need
    $End = '>' * ($nestedPromptLevel + 1)
}

# Create our 2nd ScriptBlock that ALL Styles share
hidden [ScriptBlock] $AllStylesRaw = {
    # When we glue our path back together this will be what we use
    $Joiner = "$JoinerString"
    # Chop up our path
    $PathParts = $ExecutionContext.SessionState.Path.CurrentLocation -split '\\'
}

# Create our 3rd ScriptBlock for both Condensed Styles
hidden [ScriptBlock] $CondensedRaw = {
    # Set How Many Child Path parts we want
    $ChildCount = $ChildCountValue
    # Set how many Parent Path parts we want
    $ParentCount = $ParentCountValue
    # If we don't have enough to condense we just return the full path glued together with our Joiner
    If ($PathParts.Count -le $ParentCount + $ChildCount) {
        "$Start{$PromptType}$Token$($PathParts -join $Joiner)$EndToken$End"
    }
    # Otherwise we grab the ParentCount of path items from the front and ChildCount from the end 
    # leaving .. in the middle
    Else {
        "$Start{$PromptType}$Token$(
            $PathParts[0..($ParentCount - 1)] -join $Joiner)$Joiner..$Joiner$(
                $PathParts[($PathParts.Count - $ChildCount)..($PathParts.Count)] -join $Joiner)$EndToken$End"
    }
}

# Create our 3rd ScriptBlock for MultiLine
hidden [ScriptBlock] $MultiLineRaw = {
    "$Start{PSML}$Joiner$($PathParts -join $Joiner)$Joiner$End" 
}

# Create our 2nd ScriptBlock for Reset
hidden [ScriptBlock] $ResetRaw = { 
# Set us back to as close to the original function
    "$($Start)PS $($executionContext.SessionState.Path.CurrentLocation)$End" 
}

#endregion

We start off with our static property and method to ensure we only ever have one instance of this class created

# holds our single instance of our class
hidden static [PromptParts] $Instance

# Gets or creates our single instance of our class
hidden static [PromptParts] Get() {
    # Do we have an instance? No? Well Create One
    If ($null -eq [PromptParts]::Instance) { [PromptParts]::Instance = [PromptParts]::new() }
    # Return it
    return [PromptParts]::Instance
}

Next we have our ScriptBlocks that make up our different Prompt functions. We begin with the code they ALL share, followed by the code that all the Styles share. These should look familiar from earlier but with some twists. Notice $Joiner is now set to a string with another variable $JoinerString more on that when we get down to the Cmdlet itself

# Create our 1st ScriptBlock that all prompts share
hidden [ScriptBlock] $AllStartRaw = {
    # Setup to show when we are debugging 
    $Start = If ($PSDebugContext) { "[DBG]" } Else {""}
    # Setup how many > we need
    $End = '>' * ($nestedPromptLevel + 1)
}

# Create our 2nd ScriptBlock that ALL Styles share
hidden [ScriptBlock] $AllStylesRaw = {
    # When we glue our path back together this will be what we use
    $Joiner = "$JoinerString"
    # Chop up our path
    $PathParts = $ExecutionContext.SessionState.Path.CurrentLocation -split '\\'
}

Next up is the meat of the different Styles (and reset) Prompts. Like previously this should also look familiar but again things like $ChildCount and $ParentCount are no longer hard coded values. We even have some other new undeclared variables in here $PromptType, $Token, $EndToken

# Create our 3rd ScriptBlock for both Condensed Styles
hidden [ScriptBlock] $CondensedRaw = {
    # Set How Many Child Path parts we want
    $ChildCount = $ChildCountValue
    # Set how many Parent Path parts we want
    $ParentCount = $ParentCountValue
    # If we don't have enough to condense we just return the full path glued together with our Joiner
    If ($PathParts.Count -le $ParentCount + $ChildCount) {
        "$Start{$PromptType}$Token$($PathParts -join $Joiner)$EndToken$End"
    }
    # Otherwise we grab the ParentCount of path items from the front and ChildCount from the end 
    # leaving .. in the middle
    Else {
        "$Start{$PromptType}$Token$(
            $PathParts[0..($ParentCount - 1)] -join $Joiner)$Joiner..$Joiner$(
                $PathParts[($PathParts.Count - $ChildCount)..($PathParts.Count)] -join $Joiner)$EndToken$End"
    }
}

# Create our 3rd ScriptBlock for MultiLine
hidden [ScriptBlock] $MultiLineRaw = {
    "$Start{PSML}$Joiner$($PathParts -join $Joiner)$Joiner$End" 
}

# Create our 2nd ScriptBlock for Reset
hidden [ScriptBlock] $ResetRaw = { 
# Set us back to as close to the original function
    "$($Start)PS $($executionContext.SessionState.Path.CurrentLocation)$End" 
}

Then we have the actual visible stuff we’ll be using from outside the class. This is pretty straight forward we create string that is all the hidden ScriptBlocks above combined together for each Style and Reset

#region The Visible Stuff

# Glues together all the ScriptBlocks to make Reset
static [string] $Reset = "$([PromptParts]::Get().AllStartRaw)$([PromptParts]::Get().ResetRaw)"

# Glues together all the ScriptBlocks to make both Styles of Condensed
static [string] $Condensed = "$([PromptParts]::Get().AllStartRaw)$([PromptParts]::Get().AllStylesRaw)$([PromptParts]::Get().CondensedRaw)"

# Glues together all the ScriptBlocks to make Multiline
static [string] $MultiLine = "$([PromptParts]::Get().AllStartRaw)$([PromptParts]::Get().AllStylesRaw)$([PromptParts]::Get().MultiLineRaw)"

#endregion

After all that we get down to our actual Cmdlet Set-Prompt

#region Cmdlets

Function Set-Prompt {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,HelpMessage="Prompt Style", ParameterSetName="Style")]
        [PromptStyle] $Style,
        [Parameter(Mandatory=$true,HelpMessage="Reset To default prompt", ParameterSetName="Reset")]
        [Switch] $Reset
    ) 

    DynamicParam {
        # If a user selects either of the Condensed options we want to prompt how much we want to condense
        $DynamicParameters = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        if ($Style -in @([PromptStyle]::Condensed,[PromptStyle]::MultiLineCondensed )) {
            $Attributes = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
            $ParameterAttribute = [System.Management.Automation.ParameterAttribute]::new()
            $ParameterAttribute.ParameterSetName = "Style"
            $Attributes.Add($ParameterAttribute)
            $Attributes.Add([System.Management.Automation.ValidateRangeAttribute]::new(1,10))
            # How Many Parents do We show
            $ParentCount = [System.Management.Automation.RuntimeDefinedParameter]::new("ParentCount",[int],$Attributes)
            $ParentCount.Value = 1
            $DynamicParameters.Add("ParentCount",$ParentCount)                
            # How Many Children do we show
            $ChildCount = [System.Management.Automation.RuntimeDefinedParameter]::new("ChildCount",[int],$Attributes)
            $ChildCount.Value = 1
            $DynamicParameters.Add("ChildCount",$ChildCount)                
        }
        return $DynamicParameters
    }
    
    process {
        # Simple Reset back to the standard prompt       
        If ($Reset) { 
            $Prompt = [PromptParts]::Reset
            Write-Verbose "Reseting Prompt"
        }
        Else {
            switch ( $Style ) {
                { $_ -in @([PromptStyle]::Condensed, [PromptStyle]::MultiLineCondensed) } { 
                    Write-Verbose "Parent Count: $($ParentCount.Value)"
                    Write-Verbose "Child Count: $($ChildCount.Value)"
                    # Set the Condensing Limiters in the function Body we just created
                    $PromptString = [PromptParts]::Condensed.
                        Replace('$ParentCountValue',$ParentCount.Value).
                        Replace('$ChildCountValue',$ChildCount.Value)
                }
                Condensed {
                    $Prompt = $PromptString.
                        Replace('$JoinerString',"\").
                        Replace('$PromptType',"PSC").
                        Replace('$Token'," ").
                        Replace('$EndToken', "")
                }
                MultiLineCondensed {
                    $Prompt = $PromptString.
                        Replace('$JoinerString',"``r``n").
                        Replace('$PromptType',"PSMLC").
                        Replace('$Token',"``r``n").
                        Replace('$EndToken', "``r``n")
                }
                MultiLine { 
                    $Prompt = [PromptParts]::MultiLine.
                        Replace('$JoinerString',"``r``n")
                }
            }
            Write-Verbose "Set Prompt Style to $Style"
        }
        $CompletedPrompt = [ScriptBlock]::Create($Prompt)
        Write-Verbose "Writing new prompt function:`r`n`r`n$CompletedPrompt"
        Set-Content -Path Function:\prompt -Value $CompletedPrompt
    }
}

#endregion

We begin here with our Cmdlet house keeping. We set [CmdletBinding()] to allow us to use things like -Verbose then we get into setting our Parameters we begin with our 2 main parameters each with its own Parameter Set name so you can’t set a style AND Reset at the same time

[CmdletBinding()]
Param(
    [Parameter(Mandatory=$true,HelpMessage="Prompt Style", ParameterSetName="Style")]
    [PromptStyle] $Style,
    [Parameter(Mandatory=$true,HelpMessage="Reset To default prompt", ParameterSetName="Reset")]
    [Switch] $Reset
) 

Finally we have to create 2 Dynamic parameters but only if the Style the user has selected is one that condenses and thus needs a value for $ParentCount and $ChildCount

DynamicParam {
    # If a user selects either of the Condensed options we want to prompt how much we want to condense
    $DynamicParameters = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
    if ($Style -in @([PromptStyle]::Condensed,[PromptStyle]::MultiLineCondensed )) {
        $Attributes = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
        $ParameterAttribute = [System.Management.Automation.ParameterAttribute]::new()
        $ParameterAttribute.ParameterSetName = "Style"
        $Attributes.Add($ParameterAttribute)
        $Attributes.Add([System.Management.Automation.ValidateRangeAttribute]::new(1,10))
        # How Many Parents do We show
        $ParentCount = [System.Management.Automation.RuntimeDefinedParameter]::new("ParentCount",[int],$Attributes)
        $ParentCount.Value = 1
        $DynamicParameters.Add("ParentCount",$ParentCount)                
        # How Many Children do we show
        $ChildCount = [System.Management.Automation.RuntimeDefinedParameter]::new("ChildCount",[int],$Attributes)
        $ChildCount.Value = 1
        $DynamicParameters.Add("ChildCount",$ChildCount)                
    }
    return $DynamicParameters
}

Now we’re finally into functionality of our Cmdlet. We have to use the process block because we have a dynamic parameter

process {
    # Simple Reset back to the standard prompt       
    If ($Reset) { 
        $Prompt = [PromptParts]::Reset
        Write-Verbose "Reseting Prompt"
    }
    Else {
        switch ( $Style ) {
            { $_ -in @([PromptStyle]::Condensed, [PromptStyle]::MultiLineCondensed) } { 
                Write-Verbose "Parent Count: $($ParentCount.Value)"
                Write-Verbose "Child Count: $($ChildCount.Value)"
                # Set the Condensing Limiters in the function Body we just created
                $PromptString = [PromptParts]::Condensed.
                    Replace('$ParentCountValue',$ParentCount.Value).
                    Replace('$ChildCountValue',$ChildCount.Value)
            }
            Condensed {
                $Prompt = $PromptString.
                    Replace('$JoinerString',"\").
                    Replace('$PromptType',"PSC").
                    Replace('$Token'," ").
                    Replace('$EndToken', "")
            }
            MultiLineCondensed {
                $Prompt = $PromptString.
                    Replace('$JoinerString',"``r``n").
                    Replace('$PromptType',"PSMLC").
                    Replace('$Token',"``r``n").
                    Replace('$EndToken', "``r``n")
            }
            MultiLine { 
                $Prompt = [PromptParts]::MultiLine.
                    Replace('$JoinerString',"``r``n")
            }
        }
        Write-Verbose "Set Prompt Style to $Style"
    }
    $CompletedPrompt = [ScriptBlock]::Create($Prompt)
    Write-Verbose "Writing new prompt function:`r`n`r`n$CompletedPrompt"
    Set-Content -Path Function:\prompt -Value $CompletedPrompt
}

We start with the simplest the Reset we just pull the string representation of its ScriptBlock from our helper Class

# Simple Reset back to the standard prompt       
If ($Reset) { 
    $Prompt = [PromptParts]::Reset
    Write-Verbose "Reseting Prompt"
}

Otherwise we dive into our Prompt Styles.

Else {
    switch ( $Style ) {
        { $_ -in @([PromptStyle]::Condensed, [PromptStyle]::MultiLineCondensed) } { 
            Write-Verbose "Parent Count: $($ParentCount.Value)"
            Write-Verbose "Child Count: $($ChildCount.Value)"
            # Set the Condensing Limiters in the function Body we just created
            $PromptString = [PromptParts]::Condensed.
                Replace('$ParentCountValue',$ParentCount.Value).
                Replace('$ChildCountValue',$ChildCount.Value)
        }
        Condensed {
            $Prompt = $PromptString.
                Replace('$JoinerString',"\").
                Replace('$PromptType',"PSC").
                Replace('$Token'," ").
                Replace('$EndToken', "")
        }
        MultiLineCondensed {
            $Prompt = $PromptString.
                Replace('$JoinerString',"``r``n").
                Replace('$PromptType',"PSMLC").
                Replace('$Token',"``r``n").
                Replace('$EndToken', "``r``n")
        }
        MultiLine { 
            $Prompt = [PromptParts]::MultiLine.
                Replace('$JoinerString',"``r``n")
        }
    }
    Write-Verbose "Set Prompt Style to $Style"
}

This Switch statement has some things worth talking about starting with the first Case. This one we’re setting up a case for either of the Condensed Styles here like in Reset above we get our string from our Class but then we run some replacements on the string. Remember I mentioned those undeclared variables up in our Class’s hidden ScriptBlocks? well here is where they get swapped out for the actual values we want starting with our Dynamic ParentCount and ChildCount parameters. What you might also notice is a complete lack of a break at the end of any of these case statements. This is because we are taking advantage of case fall through where if no break is found the code will execute ALL cases that are satisfied when falling through the switch statement.

{ $_ -in @([PromptStyle]::Condensed, [PromptStyle]::MultiLineCondensed) } { 
    Write-Verbose "Parent Count: $($ParentCount.Value)"
    Write-Verbose "Child Count: $($ChildCount.Value)"
    # Set the Condensing Limiters in the function Body we just created
    $PromptString = [PromptParts]::Condensed.
        Replace('$ParentCountValue',$ParentCount.Value).
        Replace('$ChildCountValue',$ChildCount.Value)
}

Meaning we ALSO hit these cases and this is where we finally address the rest of those undeclared variables by swapping out for the values of each Condensed style. here we don’t need break because its only ever going to be one of the Enum

Condensed {
    $Prompt = $PromptString.
        Replace('$JoinerString',"\").
        Replace('$PromptType',"PSC").
        Replace('$Token'," ").
        Replace('$EndToken', "")
}
MultiLineCondensed {
    $Prompt = $PromptString.
        Replace('$JoinerString',"``r``n").
        Replace('$PromptType',"PSMLC").
        Replace('$Token',"``r``n").
        Replace('$EndToken', "``r``n")
}

And our final simpler MultiLine case

MultiLine { 
    $Prompt = [PromptParts]::MultiLine.
        Replace('$JoinerString',"``r``n")
}

Finally we convert our string into a full blown ScriptBlock, and set our prompt function to that ScriptBlock (since PowerShell treats Function: as a drive each function is basically a “File” and you can use the same Set-Content Cmdlet to write to it.

$CompletedPrompt = [ScriptBlock]::Create($Prompt)
Write-Verbose "Writing new prompt function:`r`n`r`n$CompletedPrompt"
Set-Content -Path Function:\prompt -Value $CompletedPrompt

Save that whole file as SetPrompt.psm1 create a folder named SetPrompt under whatever level of module you want (user, system, ise, etc) I put mine right under C:\Windows\System32\WindowsPowerShell\v1.0\Modules since I want this module to be available anywhere I jump into powershell (even works inside VS Code and other shells) and copy our psm1 file to SetPrompt you don’t really need a .psd1 file as this is a DEAD simple module as long as the folder name patches the .psm1 file name it will automatically pull it into the shell as available. Then the next time you fire up a fresh Shell you can jump right into calling

Set-Prompt -Style Condensed -ParentCount 2 -ChildCount 2 -Verbose

No need to import the module, Tab Completion will work just fine since ParentCount and ChildCount have default values of 1 you can also omit either or both

Set-Prompt -Style Condensed -Verbose

To reset just call

Set-Prompt -Reset -Verbose

That’s it nothing glamorous. But I’ve always prized functionality of form and we’ve solved our problem and turned it into an easy Cmdlet we can use to switch between Prompt Styles with minimal effort so yay.

Until the next one in this series where we go hog wild with “drive” letters

Leave a Reply

Your email address will not be published. Required fields are marked *