PowerShell: Read-Host On Steroids – Enhancements To Jeff Hicks Version

A better Read-Host – by Jeff Hicks

Read-Host is very useful but it is too simplistic to write “good” code using it.

Jeff Hicks (Twitter: @JeffHicks, Blog: jdhitsolutions.com/blog/)  who needs no introductions in the PowerShell world (and whose books I read to learn PowerShell), created a better Read-Host. I love his version. Please checkout the nice blog post he did about why Jeff created Read-HostSpecial.

I am working on a continuous integration deployment tool for deploying databases scripts/objects to Oracle/SQL Server (.sql, .rdl, .dtsx, .xmla etc). I have built some wizards (prompt based) that collect a series of inputs. Trying to avoid Read-Host, I found Jeff Hicks Read-HostSpecial and started using it extensively.

I am not going to go over the functionality of Read-HostSpecial as Jeff has already done a great job in his post. Again, check-out Jeff’s great post and code

https://www.petri.com/tip-writing-better-script-powershell-read-host-cmdlet

Some shortcomings of Read-HostSpecial

While it does a lot already, I did find some very minor shortcomings that I wanted to address

  1. Bad inputs killed the program – If you are prompting for a series of inputs (like a wizard) and the user mis-keyed one input by mistake, the error recovery is very hard and the user has to start-over from the beginning. This called for a RepromptOnError switch which issues a gentle warning and then allows the user to input a valid value upon encountering validation errors.
  2. I needed a couple of more canned validations like ValidateFolder and ValidateFile.
  3. Too bad, there is no Write-HostSpecial – I wanted Read-HostSpecial to display some pretty text and not wait for input (like Write-Host) using the same nomenclature for fonts/look/feel/usability as Read-HostSpecial. So, I needed a NoWait switch.

The code with the enhancements

The new code with the enhancements is not too difficult to follow.

#Original version by Jeff Hicks [Twitter: @JeffHicks] [Blog: jdhitsolutions.com/blog/]
#Source: https://www.petri.com/tip-writing-better-script-powershell-read-host-cmdlet
#
#Enhancements by - Jana Sattainathan [Twitter: @SQLJana] [Blog: sqljana.wordpress.com]
#       1) Added RepromptOnError so that a 10 step wizard may not die of a single bad value!
#       2) Added switch Nowait to display and quit without waiting (just like write-host)
#       3) Added ValidateFolder and ValidateFile

Function Global:Read-HostSpecial
{
    [cmdletbinding(DefaultParameterSetName="_All")]
    Param(
        [Parameter(Position = 0,Mandatory,HelpMessage = "Enter prompt text.")]
        [Alias("message")]
        [ValidateNotNullorEmpty()]
        [string]$Prompt,

        [Parameter(Mandatory=$false, HelpMessage = "If value entered is invalid, reprompts until the value is valid")]
        [switch]$RepromptOnError = $true,

        [Parameter(ParameterSetName = "NoWait", HelpMessage = "Behavior is identical to Write-Host when True")]
        [Parameter(Mandatory=$false)]
        [switch]$Nowait = $False,

        [Alias("foregroundcolor","fg")]
        [consolecolor]$PromptColor,
        [string]$Title,

        [Parameter(ParameterSetName = "SecureString")]
        [switch]$AsSecureString,

        [Parameter(ParameterSetName = "NotNull")]
        [switch]$ValidateNotNull,

        [Parameter(ParameterSetName = "Range")]
        [ValidateNotNullorEmpty()]
        [int[]]$ValidateRange,

        [Parameter(ParameterSetName = "Pattern")]
        [ValidateNotNullorEmpty()]
        [regex]$ValidatePattern,

        [Parameter(ParameterSetName = "Set")]
        [ValidateNotNullorEmpty()]
        [string[]]$ValidateSet,

        [Parameter(ParameterSetName = "Folder")]
        [ValidateNotNullorEmpty()]
        [switch]$ValidateFolder,

        [Parameter(ParameterSetName = "File")]
        [ValidateNotNullorEmpty()]
        [switch]$ValidateFile
    )

    Write-Verbose "Starting: $($MyInvocation.Mycommand)"
    Write-Verbose "Parameter set = $($PSCmdlet.ParameterSetName)"
    Write-Verbose "Bound parameters $($PSBoundParameters | Out-String)"

    #combine the Title (if specified) and prompt
    $Text = @"
$(if ($Title) {
    "$Title`n$("-" * $Title.Length)"
})
$Prompt
"@ 

    if ($Nowait -eq $false) { $Text = "$Text : "}

    #create a hashtable of parameters to splat to Write-Host
    $paramHash = @{
        NoNewLine = $True
        Object = $Text
    }    

    if ($PromptColor) {
        $paramHash.Add("Foregroundcolor",$PromptColor)
    }

    while ($true)
    {
        #display the prompt
        Write-Host @paramhash

        #get the value
        if ($AsSecureString) {
            $r = $host.ui.ReadLineAsSecureString()
        }
        else {
            #If $Nowait is $true, just write out and exit
            if ($Nowait -eq $true)
            {
                break
            }
            else  #($Nowait -eq $false)
            {
                #read console input
                $r = $host.ui.ReadLine()
            }
        }

        #assume the input is valid unless proved otherwise
        $Valid = $True

        if ($Nowait -eq $false)
        {
            #run validation if necessary
            if ($ValidateNotNull) {
                Write-Verbose "Validating for null or empty"
                if($r.length -eq 0 -OR $r -notmatch "\S" -OR $r -eq $Null) {
                    $Valid = $False
                    $err = "Validation test for not null or empty failed."
                }
            }
            elseif ($ValidatePattern) {
                Write-Verbose "Validating for pattern $($validatepattern.ToString())"
                If ($r -notmatch $ValidatePattern) {
                    $Valid = $False
                    $err = "Validation test for the specified pattern failed."
                }
            }
            elseif ($ValidateRange) {
                Write-Verbose "Validating for range $($ValidateRange[0])..$($ValidateRange[1]) "
                if ( -NOT ([int]$r -ge $ValidateRange[0] -AND [int]$r -le $ValidateRange[1])) {
                    $Valid = $False
                    $err = "Validation test for the specified range ($($ValidateRange[0])..$($ValidateRange[1])) failed."
                }
                else {
                     #convert to an integer
                    [int]$r = $r
                }
            }
            elseif ($ValidateSet) {
                Write-Verbose "Validating for set $($validateset -join ",")"
                if ($ValidateSet -notcontains $r) {
                    $Valid = $False
                    $err = "Validation test for set $($validateset -join ",") failed."
                }
            }
            elseif ($ValidateFolder) {
                Write-Verbose "Validating for folder"
                if (-not (Test-Path -LiteralPath $r -PathType Container)) {
                    $Valid = $False
                    $err = "Validation test for folder failed."
                }
            }
            elseif ($ValidateFile) {
                Write-Verbose "Validating for file"
                if (-not (Test-Path -LiteralPath $r -PathType Leaf)) {
                    $Valid = $False
                    $err = "Validation test for file failed."
                }
            }
            If ($Valid) {
                Write-Verbose "Writing result to the pipeline"
                #any necessary validation passed
                $r

                break
            }
            else
            {
                if ($RepromptOnError -eq $false)
                {
                    Write-Error $err
                    break
                }
                else
                {
                    Write-Warning $err
                    Write-Warning "**** Please enter a valid value ****"
                }
            }
        }
    }

    Write-Verbose "Ending: $($MyInvocation.Mycommand)"

} #end function

#define an alias
#Set-Alias -Name rhs -Value Read-HostSpecial

I will show examples for the enhancements.

RepromptOnError – Example: With RepromptOnError:$false

With RepromptOnError:$false (default behavior before addition of this switch), any mistakes and hence validation failures will result in an error.

PS C:\> Read-HostSpecial -Prompt ‘Enter input folder’ -ValidateFolder -RepromptOnError: $false
Enter input folder : c:\winfows
Read-HostSpecial : Validation test for folder failed.
At line:1 char:1
+ Read-HostSpecial -Prompt ‘Enter input folder’ -ValidateFolder -RepromptOnError: …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Read-HostSpecial

PS C:\>

RepromptOnError – Example: With RepromptOnError:$true

With RepromptOnError:$true (default behavior now with the addition of this switch), any mistakes and hence validation failures will result in a warning following by a reprompt for the value.

PS C:\> Read-HostSpecial -Prompt ‘Enter input folder’ -ValidateFolder -RepromptOnError
Enter input folder : c:\Winfows\

WARNING: Validation test for folder failed.
WARNING: **** Please enter a valid value ****

Enter input folder : c:\windows
c:\windows
PS C:\>                

The above example also shows the new ValidateFolder switch. ValidateFile is very similar.

I will skip the example for NoWait as it is pretty self-explanatory.

I use Out-Gridview and Read-HostSpecial for inputs

As I said, I use this in a wizard as shown in the screen-shot below, but feel free to use it as you deem fit.

I use a combination of Out-GridView (with -OutputMode: Single or Multiple) to collect input from a collection of items. Yes, Out-GridView can be used to collect input. The screenshots show how I use them to drive input data collection.

Out-GridView
Out-GridView in action – collecting input

…and here is Read-HostSpecial in action!

Read-HostSpecial
Read-HostSpecial in action in a Wizard

Once the above wizard collects the inputs it requires to automate a DB release deployment, it then kicks off the deploy process. While the users can input them as parameters in a PowerShell function call, a wizard wrapper makes the learning curve a lot better and provides additional validations to the input data (not to mention the better user experience). Power users who are intimately familiar with the application can skip the wizard and get straight to the meat of the functionality.

Conclusion:

Again, a big Thanks goes to Jeff for creating the very useful Read-HostSpecial that makes the user-experience a lot better with fewer lines of code!

Advertisements

One thought on “PowerShell: Read-Host On Steroids – Enhancements To Jeff Hicks Version

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s