28

I have searched but apparently my google foo is weak. What I need is a way to prompt for user input in the console and have the request time out after a period of time and continue executing the script if no input comes in. As near as I can tell, Read-Host does not provide this functionality. Neither does $host.UI.PromptForChoice() nor does $host.UI.RawUI.ReadKey(). Thanks in advance for any pointers.

EDIT: Much thanks to Lars Truijens for finding the answer. I have taken the code that he pointed out and encapsulated it into a function. Note that the way that I have implemented it means there could be up to one second of delay between when the user hits a key and when script execution continues.

function Pause-Host
{
    param(
            $Delay = 1
         )
    $counter = 0;
    While(!$host.UI.RawUI.KeyAvailable -and ($counter++ -lt $Delay))
    {
        [Threading.Thread]::Sleep(1000)
    }
}
EBGreen
  • 36,735
  • 12
  • 65
  • 85

10 Answers10

21

Found something here:

$counter = 0
while(!$Host.UI.RawUI.KeyAvailable -and ($counter++ -lt 600))
{
      [Threading.Thread]::Sleep( 1000 )
}
riQQ
  • 9,878
  • 7
  • 49
  • 66
Lars Truijens
  • 42,837
  • 6
  • 126
  • 143
  • 6
    In PowerShell 2 there is also the `Start-Sleep` cmdlet. – Joey Apr 06 '11 at 16:27
  • `$Host.UI.RawUI.KeyAvailable` is always true for me. There's an open bug about this https://github.com/PowerShell/PSReadLine/issues/959 – Alex from Jitbit Jun 03 '21 at 18:29
  • Link is broken and maybe could you provide the answer with how to read the input and save to variable? Thanks – kame Jan 28 '22 at 07:33
10

It's quite old now but how I solved it based on the same KeyAvailable method is here:

https://gist.github.com/nathanchere/704920a4a43f06f4f0d2

It waits for x seconds, displaying a . for each second that elapses up to the maximum wait time. If a key is pressed it returns $true, otherwise $false.

Function TimedPrompt($prompt,$secondsToWait){   
    Write-Host -NoNewline $prompt
    $secondsCounter = 0
    $subCounter = 0
    While ( (!$host.ui.rawui.KeyAvailable) -and ($count -lt $secondsToWait) ){
        start-sleep -m 10
        $subCounter = $subCounter + 10
        if($subCounter -eq 1000)
        {
            $secondsCounter++
            $subCounter = 0
            Write-Host -NoNewline "."
        }       
        If ($secondsCounter -eq $secondsToWait) { 
            Write-Host "`r`n"
            return $false;
        }
    }
    Write-Host "`r`n"
    return $true;
}

And to use:

$val = TimedPrompt "Press key to cancel restore; will begin in 3 seconds" 3
Write-Host $val
Community
  • 1
  • 1
nathanchere
  • 8,008
  • 15
  • 65
  • 86
  • 2
    An issue with the above is that any keypress is not 'swallowed'. To swallow it you can put the following before the final return: `$host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")` – Martin Connell Feb 22 '18 at 15:29
  • 1
    A little tip, you only need 1 timer variable inside the function, you can update the screen (write-host) using a modulus equasion. Basically, keep and rename the subCounter (to elapsedTime or something) and then instead of "if($subCounter -eq 1000)" you can do "if ($elapsedTime % 1000 -eq 0)" And in the second if you would have to multiple the secondsToWait by 1000 (as you're measuring in ms), or just change pass in ms instead of seconds. – Michaël May 06 '19 at 14:04
  • 1
    If I use `$val = TimedPrompt "Press key to cancel restore; will begin in 3 seconds" 3 Write-Host $val` more than once in a script, it won't wait a second time. – David Gleba Mar 07 '20 at 12:22
  • 1
    in PS 7.2.0 (using Windows Terminal) this did not work. However a small change is required: Change the while expression: `While ( $secondsCounter -lt $secondsToWait ){` and add within the While-loop this extra statement: `if ([Console]::KeyAvailable){break}` – MKZ Dec 08 '21 at 08:25
3

For people who are looking for a modern age solution with an additional constraint for exiting a PowerShell script on a pre-defined key press, the following solution might help you:

Write-Host ("PowerShell Script to run a loop and exit on pressing 'q'!")
$count=0
$sleepTimer=500 #in milliseconds
$QuitKey=81 #Character code for 'q' key.
while($count -le 100)
{
    if($host.UI.RawUI.KeyAvailable) {
        $key = $host.ui.RawUI.ReadKey("NoEcho,IncludeKeyUp")
        if($key.VirtualKeyCode -eq $QuitKey) {
            #For Key Combination: eg., press 'LeftCtrl + q' to quit.
            #Use condition: (($key.VirtualKeyCode -eq $Qkey) -and ($key.ControlKeyState -match "LeftCtrlPressed"))
            Write-Host -ForegroundColor Yellow ("'q' is pressed! Stopping the script now.")
            break
        }
    }
    #Do your operations
    $count++
    Write-Host ("Count Incremented to - {0}" -f $count)
    Write-Host ("Press 'q' to stop the script!")
    Start-Sleep -m $sleepTimer
}
Write-Host -ForegroundColor Green ("The script has stopped.")

Sample script output: enter image description here

Refer Microsoft document on key states for handling more combinations.

Credits: Technet Link

3
timeout.exe 5

still works from powershell. Waits 5 seconds or until key press.

Inelegant perhaps. But easy.

However,

From the Powershell_ISE it pops up a new command prompt window, and returns immediately, so it doesnt wait (From the powershell console it uses that console and does wait). You can make it wait from the ISE with a little more work (still pops up its own window tho):

if ($psISE) {
    start -Wait timeout.exe 5
} else {
    timeout.exe 5
}
john v kumpf
  • 431
  • 3
  • 8
  • even just `timeout /t 5` works fine for me in PS7 still. Looks like it's a windows exe, so maybe won't work on non windows systems, but for me for now, is the most reliable (I couldn't get other RawUI approaches to work reliably). – Jayden Apr 16 '22 at 09:54
2

Here is a keystroke utility function that accepts:

  • Validation character set (as a 1-character regex).
  • Optional message
  • Optional timeout in seconds

Only matching keystrokes are reflected to the screen.

Usage:

$key = GetKeyPress '[ynq]' "Run step X ([y]/n/q)?" 5

if ($key -eq $null)
{
    Write-Host "No key was pressed.";
}
else
{
    Write-Host "The key was '$($key)'."
}

Implementation:

Function GetKeyPress([string]$regexPattern='[ynq]', [string]$message=$null, [int]$timeOutSeconds=0)
{
    $key = $null

    $Host.UI.RawUI.FlushInputBuffer() 

    if (![string]::IsNullOrEmpty($message))
    {
        Write-Host -NoNewLine $message
    }

    $counter = $timeOutSeconds * 1000 / 250
    while($key -eq $null -and ($timeOutSeconds -eq 0 -or $counter-- -gt 0))
    {
        if (($timeOutSeconds -eq 0) -or $Host.UI.RawUI.KeyAvailable)
        {                       
            $key_ = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown,IncludeKeyUp")
            if ($key_.KeyDown -and $key_.Character -match $regexPattern)
            {
                $key = $key_                    
            }
        }
        else
        {
            Start-Sleep -m 250  # Milliseconds
        }
    }                       

    if (-not ($key -eq $null))
    {
        Write-Host -NoNewLine "$($key.Character)" 
    }

    if (![string]::IsNullOrEmpty($message))
    {
        Write-Host "" # newline
    }       

    return $(if ($key -eq $null) {$null} else {$key.Character})
}
crokusek
  • 5,345
  • 3
  • 43
  • 61
  • `Exception calling "FlushInputBuffer" with "0" argument(s): "The method or operation is not implemented."` PSVersion = 5.1.17763.771 – Ross Presser Feb 26 '20 at 15:13
  • Ah, the exception only happens in PowerShell ISE. In a straight PS window it's fine. It appears raw UI can't be checked at all in ISE, so beware. – Ross Presser Feb 26 '20 at 15:21
1
function ReadKeyWithDefault($prompt, $defaultKey, [int]$timeoutInSecond = 5 ) {
    $counter =  $timeoutInSecond * 10
    do{
        $remainingSeconds = [math]::floor($counter / 10)
        Write-Host "`r$prompt (default  $defaultKey  in $remainingSeconds seconds): " -NoNewline
        if($Host.UI.RawUI.KeyAvailable){
            $key = $host.UI.RawUI.ReadKey("IncludeKeyUp")
            Write-Host 
            return $key
        }
        Start-Sleep -Milliseconds 100
    }while($counter-- -gt 0)

    Write-Host $defaultKey
    return $defaultKey
}

$readKey = ReadKeyWithDefault "If error auto exit( y/n )" 'y' 5
1

Another option: you can use choice shell command which comes with every Windows version since Windows 2000

Choice /C yn /D n /t 5 /m "Are you sure? You have 5 seconds to decide"
if ($LASTEXITCODE -eq "1") # 1 for "yes" 2 for "no"
{
    # do stuff
}
else
{
    # don't do stuff
}

Stackoverflow syntax higlighting doesn't work for powershell, # means "comment" here

Alex from Jitbit
  • 53,710
  • 19
  • 160
  • 149
  • This doesn't actually work. powershell hides the prompt, and you can never respond to it. Then it always returns the default answer. Test it out. It does not help to add cmd /c on the front either. :( – SpaceGhost440 Mar 15 '23 at 14:33
1

The $Host.UI.RawUI.KeyAvailable seems to be buggy so I had more luck using [Console]::KeyAvailable:

function keypress_wait {
    param (
        [int]$seconds = 10
    )
    $loops = $seconds*10
    Write-Host "Press any key within $seconds seconds to continue"
    for ($i = 0; $i -le $loops; $i++){
        if ([Console]::KeyAvailable) { break; }
        Start-Sleep -Milliseconds 100
    }
if ([Console]::KeyAvailable) { return [Console]::ReadKey($true); }
else { return $null ;}
}
1

Flushing Input buffer is very important, try this, worked like a charm for me.

function Pause-Host {
    param ($Delay = 1)
    
    $host.UI.RawUI.FlushInputBuffer()
    While(!$host.UI.RawUI.KeyAvailable -and (0 -lt $Delay--))
    {
        Write-Host "`rWaiting for $Delay Seconds or Any key Press ..." -NoNewline
        Start-Sleep -Seconds 1
    }
}
# Optional: Flush last key-press
$host.UI.RawUI.FlushInputBuffer()
0

To optionally pause the script before it exits (useful for running headless scripts and pausing on error output), I appended nathanchere's answer with:

if ([Console]::KeyAvailable) { $pressedKey = [Console]::ReadKey($true); read-host; break; }
  elseif ($secondsCounter -gt $secondsToWait) { 
      Write-Host "`r`n"
      return $false;
  }
Pavman
  • 125
  • 7