Everybody has their own idea of what the perfect prompt in PowerShell consists of. My prompt is pretty boring, but it’s got one pretty unique feature – my prompt function doubles as a useful command!

You see, a lot of times I wind up in deep directories of solution and project folders in Visual Studio and I wind up with a prompt that barely fits on one line, or even worse, wraps. In this cases, I often only care about having a vague idea of where I am.

# sometimes I only want a little information about where I am.
C:\Users\Josh Einstein\Documents\Visual Studio\Projects> Prompt -Minimize

# sometimes I don't care at all
PS Projects> Prompt -Hide

# if I need to bring it back to the way it was, I can just do this.
PS> Prompt -Show
C:\Users\Josh Einstein\Documents\Visual Studio\Projects>

This has really come in handy for me and it’s something you could easily do with a separate script or function in your profile. But I figured, hey the prompt function is already there right? What’s it gonna hurt if I add a few parameters to it?

##############################################################################
#.SYNOPSIS
# The all-important Prompt function called by PowerShell in order to display
# the prompt, with an added twist using the built-in parameters.
#
#.DESCRIPTION
# Customizing the prompt is common in PowerShell, but this function adds some
# parameters to the function that enable some dynamic behavior. These
# parameters will not affect the way PowerShell calls the function, but they
# allow you to minimize or hide the prompt by passing switches.
# 
#.PARAMETER Minimize
# Shrinks the prompt so that it shows only the leaf portion of the current
# provider path. Restore it using the -Show parameter.
#
#.PARAMETER Hide
# Sets the prompt to an ultra-compact mode, displaying only an indicator,
# and no information about the current location.
#
#.PARAMETER Show
# Restores the prompt to it's default mode which displays the full location
# of the current provider path.
#
#.LINK
# about_prompts
#
#.EXAMPLE
# Prompt -Minimize
# Prompt -Hide
# Prompt -Show
##############################################################################
function Prompt {

    [CmdletBinding(DefaultParameterSetName='Prompt')]
    param ( 

        [Parameter(ParameterSetName='Minimize')]
        [Switch] $Minimize,

        [Parameter(ParameterSetName='Hide')]
        [Switch] $Hide,

        [Parameter(ParameterSetName='Show')]
        [Switch] $Show

    )

    switch ( $PSCmdlet.ParameterSetName ) {
        Minimize { $Global:PromptVisibility = 'Minimized' }
        Hide     { $Global:PromptVisibility = 'Hidden' }
        Show     { $Global:PromptVisibility = 'Default' }
        Prompt   {
            switch ( $PromptVisibility ) {
                Minimized { "PS $(Split-Path $PWD -Leaf)> " }
                Hidden    { "PS> " }
                Default   { "$PWD> " }
            }
        }
    }

}

One of the things I really like about PowerShell is how through the use of script blocks and locally scoped functions, you can create interesting script using perfectly valid, albeit odd looking PowerShell script. I had first done something like this without script blocks using the pipeline which was pretty ugly. It was mostly just a way of quickly building up objects using functions that wrapped New-Object and Add-Member.

New-PSObject |
    Field FirstName 'Josh' |
    Field LastName 'Einstein'

That was pretty handy but as a C# guy it still looked ugly. I got the idea from Bruce Payette’s book, PowerShell in Action to use script blocks and locally-scoped functions so that I didn’t have to pollute the runspace with a bunch of functions that were only valid within the context of another function.

New-PSObject {

    Field FirstName 'Josh'
    Field LastName 'Einstein'

    Property FullName { "$($this.FirstName) $($this.LastName)" }

    Method SayHello {
        param($to)
        "$($this.FirstName) says 'Hello' to $to!"
    }

}

How does it work? Well, like most things in PowerShell, functions have scope. So a function defined within another function is only valid during the execution of the outer function. This makes it possible to trample on existing functions and cmdlets too.

In Bruce Payette’s book he calls them "little languages".

This technique allows for some really clean and concise syntax similar to how some DSL’s are being used in .NET.

# Global functions used by New-PSObject
function Field([String]$Name,$Value,[String]$Format) {
    @{ MemberType='NoteProperty'; Name=$Name; Value=$Value; Format=$Format }
}
function Property([String]$Name,[ScriptBlock]$GetDefinition,[ScriptBlock]$SetDefinition) {
    @{ MemberType='ScriptProperty'; Name=$Name; Value=$GetDefinition; SecondValue=$SetDefinition }
}
function Method([String]$Name,[ScriptBlock]$MethodDefinition) {
    @{ MemberType='ScriptMethod'; Name=$Name; Value=$MethodDefinition }
}

##############################################################################
#.SYNOPSIS
# Creates a PSObject and initializes it with fields, properties, and methods
# using a ScriptBlock written in a mini-language.
#
#.DESCRIPTION
# The ScriptBlock passed to the Definition parameter can contain commands
# that are private to this function, such as Field, Property, and Method
# which define members on the new PSObject.
#
#.PARAMETER Definition
# The ScriptBlock that builds up the PSObject by using special commands such
# as Field, Property, and Method. See the example for more information.
#
#.PARAMETER InputObject
# A HashTable that contains key/value pairs that will be turned into
# NoteProperties on the new PSObject.
#
#.EXAMPLE
# New-PSObject {
#     Field FirstName 'Josh'
#     Field LastName 'Einstein'
#     Property FullName { "$($this.FirstName) $($this.LastName)" }
#     Method GetFullName { "$($this.FirstName) $($this.LastName)" } 
# }
#
#.EXAMPLE
# @{FirstName='Josh';LastName='Einstein'} | New-PSObject
##############################################################################
function New-PSObject {

    [CmdletBinding(DefaultParameterSetName='Definition')]
    param ( 

        [Parameter(ParameterSetName='Definition', Position=1)]
        [ScriptBlock]$Definition,
        
        [Parameter(ParameterSetName='Hashtable', Mandatory=$true, ValueFromPipeline=$true)]
        [HashTable]$InputObject
        
    )

    begin { }
    
    process {

        $Object = New-Object PSObject
        
        if ( $InputObject ) {        
            foreach ( $k in $InputObject.Keys ) {
                Add-Member -InputObject $Object -MemberType NoteProperty -Name $k -Value $InputObject[$k]
            }
        }
        elseif ( $Definition ) {

            $Members = &$Definition
            
            foreach ( $Member in $Members ) {
                if ( $Member.Format ) { $Member.Value = [String]::Format([String]$Member.Format,[String]$Member.Value) }
                if ( $Member.ContainsKey('SecondValue') ) { 
                    Add-Member -InputObject $Object `
                                -MemberType $Member.MemberType `
                                -Name $Member.Name `
                                -Value $Member.Value `
                                -SecondValue $Member.SecondValue
                }
                else {
                    Add-Member -InputObject $Object `
                                -MemberType $Member.MemberType `
                                -Name $Member.Name `
                                -Value $Member.Value
                }
            }

        }
        
        $Object
        
    }
    
    end { }

}