powershellread-host

Why does Read-Host require to hit enter 2 times enter before sending result?


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
...

Solution

  • 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
    }
    

    Note:


    Workaround for the .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.