I have a simeple loop to read for text and also detect the escape ESC [0x1b = chr(27)]
key, to quit.
The Expected Behaviour
<Enter loop>
<Enter any string, such as "AT" and hit Return>
# Some Output
<Repeat above OR>,
...
<Hit the ESC (Escape) key to exit loop>
<Exit Loop>
Actual Behaviour
I have to:
The Code:
do {
$key = if ($host.UI.RawUI.KeyAvailable) { $host.UI.RawUI.ReadKey('NoEcho, IncludeKeyDown') }
if ($port.IsOpen) {
$at = Read-Host
$port.Write("${at}`r")
} else {
Write-Host -Fo Yellow "[INFO] Port was Closed!"
break
}
} until ($key.VirtualKeyCode -eq 27) # Repeat until a 'ESC'
}
Q: How Can I fix the above to get the intended functionality?
(Why do I need to hit enter 2 times before the input string is processed?)
Experimenting, this one-liner is behaving very weird...
while (1) { if($host.UI.RawUI.ReadKey('IncludeKeyDown').VirtualKeyCode -eq 81) { break };$s=''; $s=Read-Host; if ($s -ne "w") { Write-Host ": $s" -Non | Out-Host } else { "Hit W!"} }
If you want to list the names and codes of all available keys, you can use:
1..255| ForEach-Object { '{0} = {1}' -f $_, [System.Windows.Forms.Keys]$_ }
# <output>
...
16 = ShiftKey
17 = ControlKey
18 = Menu
19 = Pause
...
The following solution preserves the usual Read-Host
command-line editing experience, so that editing the string being typed by the user is possible:
$wshell = (New-Object -ComObject WScript.Shell) # For sending keystrokes
while ($true) {
# Solicit an initial keypress without echoing it.
$c = $host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').Character
if ($c -eq [char] 27) { # ESC pressed? Exit the loop.
'You pressed ESC - Quitting'; break
}
elseif ($c -in '', "`r") {
# Ignore keypresses that don't translate into printable characters
# as well as an initial Enter keypress with nothing to submit.
continue
}
# Send the character as a keypress to Read-Host to prefill its edit buffer...
$wshell.SendKeys("{$c}")
# ... and solicit a line of input, as usual.
$userInput = Read-Host
# Process the input.
Write-Host -Fo DarkGreen "[\r] Sending: " -NoNewline
Write-Host -Fo DarkYellow $userInput
}
The above waits for a single, initial keypress.
Otherwise:
The initially typed character is sent as a keystroke to the keyboard buffer, where Read-Host
will pick it up; that is, the edit buffer will be prefilled with the typed character, and it will appear as if the user had just typed that character into the prompt.
Caveat: Due to use of New-Object -ComObject WScript.Shell
, this solution is Windows-only and isn't fully robust: Keeping Shift held down in order to type a sequence of uppercase letters doesn't work reliably - for certain characters, such as o
, everything from the 2nd character unexpectedly remains lowercase[1] - see the next section for a potential workaround IF you have WSL installed.
On Linux, you could replace the keystroke-sending + Read-Host
call more elegantly with (syntax assumes PowerShell 7.3+):
$userInput = bash -c ('read -e -i ''{0}''; printf %s "$REPLY"' -f ($c -eq "'" ? "'\''" : $c)) || $(break)
On macOS, this doesn't work, unfortunately, and there's no simple workaround.
Note:
Pressing Ctrl-C exits too, but not just the loop, but invariably the entire script (call stack); while you cannot prevent that, you can perform cleanup operations and/or write to any of PowerShell's output streams other than the success output stream in the finally
block of an enclosing try
/ catch
/ finally
statement.
Pressing Esc to exit the loop only works before entering the Read-Host
prompt; that is, once you've typed a printable character, Ctrl-C is your only option to directly abort the prompt - along with your script and all its callers.
Read-Host
prompt in realtime, and pressing ESC invariably just clears what has been typed so far, without leaving the prompt..SendKeys()
problem on Windows, assuming you have WSL installed:# Switch the console encoding (temporarily) to UTF-8, so that
# PowerShell interprets WSL output correctly.
$prevEnc = [Console]::OutputEncoding; [Console]::OutputEncoding = [Text.Utf8Encoding]::new()
while ($true) {
# Solicit an initial keypress without echoing it.
$c = ($host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')).Character
if ($c -eq [char] 27) { # ESC pressed? Exit the loop.
'You pressed ESC - Quitting'; break
}
elseif ($c -in '', "`r") {
# Ignore keypresses that don't translate into printable characters
# as well as an initial Enter keypress with nothing to submit.
continue
}
# Use Bash, via WSL, to prompt for the input, taking advantage of its ability
# to pre-fill the edit buffer
$userInput = wsl -e bash -c ('read -e -i ''{0}''; printf %s "$REPLY"' -f $(if ($c -eq "'") { "'\''" } else { $c }))
if ($LASTEXITCODE) {
# Implies that Ctrl-C was pressed.
# Exit the loop. Note that this is NOT the same as pressing Ctrl-C
# with Read-Host, which terminates the entire script and its call stack.
break
}
# Process the input.
Write-Host -Fo DarkGreen "[\r] Sending: " -NoNewline
Write-Host -Fo DarkYellow $userInput
}
# Restore the previous console endoding, if needed.
[Console]::OutputEncoding = $prevEnc
[1] Seemingly, the simulation of keyboard input, which involves programmatically pressing and releasing the Shift
key, can interfere with properly detecting the physical keyboard state.