26 October 2014

While planning to develop some scripts that would be meant for others to use I had a need to implement a timeout when asking for input (and to go with a default value).  I didn’t think it had a timeout, but I looked at the documentation on read-host anyway and sure enough, no timeout.  I then took to Google to see if there was anything (I wasn’t feeling in a creative mood and just wanted to do it) that someone else had written to do the job.  I was quite surprised that I couldn’t find anything already written.  I found a few scripts that just waited for a keystroke to be entered before continuing on with the script.  Nothing, though, that could use a promtp like read-host and sit at that prompt until someone started entering something.  So I had to create something myself; it proved surprisingly simple.

I decided that using the detection if a key was available from those other scripts was a great way to detect the moment the end user started typing something.  I knew I’d need a loop, then a break from that loop, and most difficult to capture that first key of input and pass it along to the next section of code, display it for the end user, and also capture the rest of what they typed.  I wanted to do it all seamlessly so it looked exactly like using the built in read-host cmdlet.  I quickly sketched up some psuedo code on my whiteboard and then set out to writing the code.

Like I said, it wasn’t too difficult.  The hardest part was figuring out how to make backspacing appear on the screen as the user typed.  I inadvertently stumbled upon a nice feature when choosing to use a character array instead of creating the string itself on the fly.  Reading each key and adding it to the char array also records the backspaces.  When I use the -join operator to turn the char array into a string, it processes the backspaces and the resultant string does not contain the characters that were backspaced.  Had I been building the string on the fly, I would have had to write extra code to account for that.  I was quite happy when I realized I wouldn’t have to.

The one feature not implemented is the ability to use -assecurestring.  Encryption still makes my brain freeze and I didn’t even try to implement it.  I’m sure I could hack my way through it but I usually prefer to understand something a little better before I implement it.  To contradict myself, though, I did write encryption/decryption scripts for protecting your scripts and even importing the function inside it so it can be used straight away in the shell.  Hopefully I’ll get around to posting those soon.

I think the one thing not to change here is the millisecond sleep when waiting for input.  Unless you go lower… the lower it is the more seamless the typing experience for the end user.  Even at 250 there is a slight delay in writing that first character to the screen.. especially for us fast touch-typists.  It’s pretty seamless though and I’m happy with it.

Anyway, enough gabbing, here’s the code.  It’s fully commented so it should explain everything!

Function read-HostTimeout {
###################################################################
##  Description:  Mimics the built-in "read-host" cmdlet but adds an expiration timer for
##  receiving the input.  Does not support -assecurestring
##
##  This script is provided as is and may be freely used and distributed so long as proper
##  credit is maintained.
##
##  Written by: thegeek@thecuriousgeek.org
##  Date Modified:  10-24-14
###################################################################

# Set parameters.  Keeping the prompt mandatory
# just like the original
param(
	[Parameter(Mandatory=$true,Position=1)]
	[string]$prompt,
	
	[Parameter(Mandatory=$false,Position=2)]
	[int]$delayInSeconds
)
	
	# Do the math to convert the delay given into milliseconds
	# and divide by the sleep value so that the correct delay
	# timer value can be set
	$sleep = 250
	$delay = ($delayInSeconds*1000)/$sleep
	$count = 0
	$charArray = New-Object System.Collections.ArrayList
	Write-host -nonewline "$($prompt):  "
	
	# While loop waits for the first key to be pressed for input and
	# then exits.  If the timer expires it returns null
	While ( (!$host.ui.rawui.KeyAvailable) -and ($count -lt $delay) ){
		start-sleep -m $sleep
		$count++
		If ($count -eq $delay) { "`n"; return $null}
	}
	
	# Retrieve the key pressed, add it to the char array that is storing
	# all keys pressed and then write it to the same line as the prompt
	$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyUp").Character
	$charArray.Add($key) | out-null
	Write-host -nonewline $key
	
	# This block is where the script keeps reading for a key.  Every time
	# a key is pressed, it checks if it's a carriage return.  If so, it exits the
	# loop and returns the string.  If not it stores the key pressed and
	# then checks if it's a backspace and does the necessary cursor 
	# moving and blanking out of the backspaced character, then resumes 
	# writing. 
	$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyUp")
	While ($key.virtualKeyCode -ne 13) {
		If ($key.virtualKeycode -eq 8) {
			$charArray.Add($key.Character) | out-null
			Write-host -nonewline $key.Character
			$cursor = $host.ui.rawui.get_cursorPosition()
			write-host -nonewline " "
			$host.ui.rawui.set_cursorPosition($cursor)
			$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyUp")
		}
		Else {
			$charArray.Add($key.Character) | out-null
			Write-host -nonewline $key.Character
			$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyUp")
		}
	}
	""
	$finalString = -join $charArray
	return $finalString
}

There are no comments.



You must be logged in to post a comment.