Powershell ReadKey() case sensitive - powershell

In my script I need to get 1 character from user and process it immediately whithout waiting for an Enter.
Additionally, I want to treat the character case-sensitive.
write-host("Please press a or A:")
$choice = ($host.UI.RawUI.ReadKey(('NoEcho,IncludeKeyUp,IncludeKeyDown'))).character
if($choice -ceq "a")
{
write-host("You pressed a");
}
elseif($choice -ceq "A")
{
write-host("You pressed A");
}
else
{
Write-Host("You pressed neither a nor A")
}
Pause
The issue of this code is when I try to press "A", it shows "You pressed neither a nor A".
The reason is to type "A" I have to press Shift first, Powershell detects Shift pressed and it process immediately without waiting for an A.
Anyone has idea how to solve this?

Try the following code snippet:
if ( $Host.Name -eq 'ConsoleHost' ) {
Write-Host "Please press a or A:"
Do {
$choice = $host.UI.RawUI.ReadKey(14)
} until ( $choice.VirtualKeyCode -in #( 48..90) )
if ( $choice.Character -ceq "a") {
Write-Host "You pressed a";
}
elseif ( $choice.Character -ceq "A") {
Write-Host "You pressed A";
}
else {
Write-Host "You pressed neither a nor A ($($choice.Character))";
}
} else {
# e.g. Windows PowerShell ISE Host:
# the "ReadKey" method or operation is not implemented.
Write-Host '$Host.Name -neq ConsoleHost' -ForegroundColor Magenta
}
As currently written, $choice.VirtualKeyCode -in #( 48..90) condition allows some (limited) subset of printable characters. Adjust it with respect to Keys Enum…

Does the below make up for what is expected,
($key = $Host.UI.RawUI.ReadKey()) | % { if ($_.VirtualKeyCode -eq '16') {
$key = $Host.UI.RawUI.ReadKey()
}
$Choice = $key.Character
if ($Choice -ceq "a"){
"`rYou pressed 'a'"
}
elseif ($Choice -ceq "A"){
"`rYou pressed 'A'"
}
else {
"`rYou neither pressed 'a' nor 'A'"
}
}

The simplest solution is to only react to keypresses that result in a printable character, and then evaluate which character was pressed via a switch statement.
# Wait for a printable character to be pressed.
while (($char=($host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')).Character) -eq 0) {}
# Evaluate it.
switch -CaseSensitive ($char) {
'a' { 'You pressed a' }
'A' { 'You pressed A' }
default { 'You pressed neither a nor A' }
}
Note: While modifier keys Shift, Control, and Alt by themselves do not count as keypresses, combinations with a printable character do; therefore, for instance, Alt-a is treated the same as 'a', and Control-a as control character START OF HEADING, U+0001.
If you want to avoid that, use the following variation instead:
# Wait for a printable character to be pressed, but only if not combined
# with Ctrl or Alt.
while (($key=$host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')).Character -eq 0 -or
$key.ControlKeyState -notin 0, 'ShiftPressed') {}
$char = $key.Character
Note: This works on Windows only - on Unix-like platforms, the .ControlKeyState property is apparently always 0.
However, if you use [Console]::ReadKey() instead, you can make it work on Unix-like platforms too - which assumes that you're willing to assume that your script always runs in a console (terminal), and not other kinds of PowerShell hosts.
# Wait for a printable character to be pressed, but only if not combined
# with Ctrl or Alt.
while (($key = [Console]::ReadKey($true)).KeyChar -eq 0 -or
$key.Modifiers -notin 0, 'Shift') {}
$char = $key.KeyChar

Related

How can I allow only Y/N to be input in Read-Host without having to press Enter in Powershell?

I'm trying to find a way to have something like a Read-Host to ask the user if they want to output to the file listed or not. With this I want them to either press y or n and then the code continues rather than then pressing y/n then pressing enter as well. At the moment this all works well but again it's not quite what I'm wanting.
I've tried looking into Readkey and SendKeys (to push Enter for the user) but neither work as they seem to only execute after the user has pushed Enter on the Read-Host. I'm still very new to Powershell so I'm not entirely sure whether it's actually possible or not and I've spent too much time googling/testing to find an answer that works. If I was to use Write-Host or something to do this, it needs to not show up in the log.
I've included the necessary part of my script below. It basically asks the user if the file location is correct. If it is they press y and it uses it for the output, otherwise if they push n then it loads the FolderBrowserDialog for them to select the folder they want.
I should also note this is all within a Tee-object as this code is what determines where the Tee-object output goes to.
$OutputYN = Read-Host "Do you want the output file generated to $startDirectory\FolderList.txt? (Y/N)"
If (“y”,”n” -notcontains $OutputYN) {
Do {
$OutputYN = Read-Host "Please input either a 'Y' for yes or a 'N' for no"
} While (“y”,”n” -notcontains $OutputYN)
}
if ($OutputYN -eq "Y") {
$OutputLoc = $startDirectory
}
elseif ($OutputYN -eq "N") {
$OutputLocDir = New-Object System.Windows.Forms.FolderBrowserDialog
$OutputLocDir.Description = "Select a folder for the output"
$OutputLocDir.SelectedPath = "$StartDirectory"
if ($OutputLocDir.ShowDialog() -eq "OK") {
$OutputLoc = $OutputLocDir.SelectedPath
$OutputLoc = $OutputLoc.TrimEnd('\')
}
}
EDIT:
I should have been a little more clear. I had already tried message box type stuff as well but I'd really prefer if there is a way that the user types in a y or a n. I'm not really interested in a popup box that the user has to click. If it's not possible then so be it.
Readkey is the right way.
Use the following as template.
:prompt while ($true) {
switch ([console]::ReadKey($true).Key) {
{ $_ -eq [System.ConsoleKey]::Y } { break prompt }
{ $_ -eq [System.ConsoleKey]::N } { return }
default { Write-Error "Only 'Y' or 'N' allowed!" }
}
}
write-host 'do it' -ForegroundColor Green
:prompt gives the outer loop (while) a name which can be used in the switch statement to directly break out entirely via break prompt (and not within the switch statement).
Alternative (for Windows):
Use a MessageBox.
Add-Type -AssemblyName PresentationFramework
$messageBoxResult = [System.Windows.MessageBox]::Show("Do you want the output file generated to $startDirectory\FolderList.txt?" , 'Question' , [System.Windows.MessageBoxButton]::YesNo , [System.Windows.MessageBoxImage]::Question)
switch ($messageBoxResult) {
{ $_ -eq [System.Windows.MessageBoxResult]::Yes } {
'do this'
break
}
{ $_ -eq [System.Windows.MessageBoxResult]::No } {
'do that'
break
}
default {
# stop
return # or EXIT
}
}
Not sure if this is possible in the console. But when I need the user to write one answer of a specified set, I use a do-until-loop like:
Do {
$a = Read-Host "Y / N"
} until ( 'y', 'n' - contains $a )
try this:
$title = 'Question'
$question = 'Do you want the output file generated to $startDirectory\FolderList.txt?'
$choices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'))
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No'))
$decision = $Host.UI.PromptForChoice($title, $question, $choices, 1)
if ($decision -eq 0) {
Write-Host 'Yes'
} else {
Write-Host 'No'
}
If you are on Windows, you can do it :
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
$result = [System.Windows.Forms.MessageBox]::Show('Do you want the output file generated to $startDirectory\FolderList.txt?' , "Question" , [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question)
if ($result -eq 'Yes') {
"Yes"
}
else
{
"No"
}

Why $host.UI.RawUI.ReadKey() method captures a not-really-pressed Enter key?

I want to write a function in PowerShell 7 to wait the user to press a key or a key combination (such as Alt+Ctrl+D). There are two options I can choose: The Console.ReadKey() method in .net core, and $host.UI.RawUI.ReadKey() method in PowerShell.
I have tested these two methods. The Console.ReadKey() method works very well, but the $host.UI.RawUI.ReadKey() method has a strange behavior: It will capture a not-really-pressed "Enter" key (key code: 13).
The code (in a .ps1 script file, for testing the method):
using namespace System.Threading
using namespace System.Management.Automation.Host
Write-Host "Content before waiting keys."
Write-Host "Begin waiting keys..... Press 'Esc' to quit."
while($true)
{
while(-not $host.UI.RawUI.KeyAvailable)
{
Write-Host '.' -NoNewLine
[Thread]::Sleep(1000)
}
# No matter any combination of values of the ReadKeyOptions enum,
# the behavior of capturing not-really-pressed Enter key is the same.
$ki = $host.UI.RawUI.ReadKey("NoEcho, IncludeKeyUp")
Write-Host "[$($ki.ControlKeyState)]" -ForegroundColor Yellow
$altPressed = (($ki.ControlKeyState -band [ControlKeyStates]::LeftAltPressed) -gt 0) -or
(($ki.ControlKeyState -band [ControlKeyStates]::RightAltPressed) -gt 0)
$ctrlPressed = (($ki.ControlKeyState -band [ControlKeyStates]::LeftCtrlPressed) -gt 0) -or
(($ki.ControlKeyState -band [ControlKeyStates]::RightCtrlPressed) -gt 0)
$shiftPressed = (($ki.ControlKeyState -band [ControlKeyStates]::ShiftPressed) -gt 0)
$keyState = $ki.KeyDown ? "Down" : "UP"
Write-Host "`nGot a key:"
Write-Host "`tkey: $($ki.Character)" # Char
Write-Host "`tkey code: $($ki.VirtualKeyCode)" # int.
Write-Host "`tAlt: $altPressed"
Write-Host "`tCtrl: $ctrlPressed"
Write-Host "`tShift: $shiftPressed"
Write-Host "`tkey state: $keyState"
if($ki.VirtualKeyCode -eq 27)
{
break
}
}
Write-Host "`nContent after waiting keys."
After I run the script in PowerShell 7 console, before I pressed any key, I got:
Content before waiting keys.
Begin waiting keys..... Press 'Esc' to quit.
.[NumLockOn, EnhancedKey]
Got a key:
key:
key code: 13
Alt: False
Ctrl: False
Shift: False
key state: UP
...[NumLockOn]
Even I call the $host.UI.RawUI.FlushInputBuffer() method before the while loop, or before the calling of the ReadKey() method, this strange behavior will still occur.
If I use $host.UI.RawUI.ReadKey() method, this behavior will break the working of my function, make it cannot handle a single Enter key correctly, and it cannot be used to block until any key is pressed because it will capture an "Enter" key even though the user hasn't pressed the Enter key.
The Console.ReadKey() method hasn't this problem.
Why?
This is a known bug $Host.UI.RawUI.ReadKey Gets a extra enter key for no reason
as a workaround you can do this (which is not pretty :))
$ki = $host.UI.RawUI.ReadKey("NoEcho, IncludeKeyUp")
$ki = $host.UI.RawUI.ReadKey("NoEcho, IncludeKeyUp")
but $ki.Character will return null, you can still use this [char]$ki.VirtualKeyCode

Set a hotkey press event for the form

Use the following code to set hotkey press event for the form, but when the hotkey is pressed, the system has an alarm sound, why?
In addition, how to set multiple modifier keys, For example ctrl+alt+shift+Q
$form1_KeyDown = [System.Windows.Forms.KeyEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.KeyEventArgs]
if ($_.Alt -and $_.KeyCode -eq 'Q')
{
Write-Host 'Alt-Q pressed'
}
}
I would not know why you get a system beep when pressing a hotkey, but it may have to do with the way you add the KeyEventHandler to the form.
Using $form1.Add_KeyDown({..}) gives me no problem whatsoever:
$form1.Add_KeyDown({
# create a small array to capture the modifier keys used
$modifiers = #()
if ($_.Shift) { $modifiers += "Shift" }
if ($_.Alt) { $modifiers += "Alt" }
if ($_.Control) { $modifiers += "Control" }
# using that array, build part of the output text
$modkeys = ''
if ($modifiers.Count) {
$modkeys = '{0} ' -f ($modifiers -join ' + ')
}
# instead of building up a string 'Shift + Control + Alt', like above, you can also use
# the $_.Modifiers property and replace the commas by a plus sign like this:
# $modkeys = $_.Modifiers -replace ', ', ' + '
# these are the keys you want to react upon as example
# here all keys pressed simply do the same thing, namely display what was pressed,
# so we can shorten the code to simply test if any of the keys in the
# array correspond with the key the user pressed.
if ('Q','A','F1','Escape','NumLock' -contains $_.KeyCode) {
# we create the output string by making use of the '-f' Format operator in POwershell.
# you can read more about that here for instance:
# https://social.technet.microsoft.com/wiki/contents/articles/7855.powershell-using-the-f-format-operator.aspx
Write-Host ('{0}{1} pressed' -f $modkeys, $_.KeyCode)
# The same could have been done with something like:
# Write-Host ($modkeys + ' ' + $_.KeyCode + ' pressed')
# or
# Write-Host "$modkeys $($_.KeyCode) pressed"
}
})

How can I escape a read-host by pressing escape?

just wondering if its possible to escape a read-host in a while loop by pressing escape.
I've tried doing an do-else loop but it will only recognize button presses outside of the read-host.
This is basically what I have
#Import Active Directory Module
Import-Module ActiveDirectory
#Get standard variables
$_Date=Get-Date -Format "MM/dd/yyyy"
$_Server=Read-Host "Enter the domain you want to search"
#Request credentials
$_Creds=Get-Credential
while($true){
#Requests user input username
$_Name=Read-Host "Enter account name you wish to disable"
#rest of code
}
I want to be able to escape it if I want to change the domain
Using Read-Host you cannot do this, but you might consider using a graphical input dialog instead of prompting in the console. After all, the Get-Credential cmdlet also displays a GUI.
If that is an option for you, it can be done using something like this:
function Show-InputBox {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Message,
[string]$Title = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.PSCommandPath),
[string]$defaultText = ''
)
Add-Type -AssemblyName 'Microsoft.VisualBasic'
return [Microsoft.VisualBasic.Interaction]::InputBox($Message, $Title, $defaultText)
}
while($true) {
$_Name = Show-InputBox "Enter account name you wish to disable"
if ([string]::IsNullOrWhiteSpace($_Name)) {
# the box was cancelled, so exit the loop
break
}
# proceed with the rest of the code
}
If the user presses the Esc key, clicks Cancel, or leaves the input blank, you can exit the while loop, otherwise proceed with the code.
You cannot do it with Read-Host, but you can do it via the PSReadLine module (ships with Windows PowerShell version 5 or higher on Windows 10 / Windows Server 2016) and PowerShell Core) and its PSConsoleHostReadline function:
Important:
As of PSReadLine v2.0.0-beta3, the solution below is a hack, because the PSConsoleHostReadline only supports prompting for PowerShell statements, not open-ended user input.
This GitHub feature suggestion asks for the function to be optionally usable for general-purpose user input as well, which would allow for a greatly customizable end-user prompting experience. Make your voice heard there, if you'd like see this suggestion implemented.
The hack should work in your case, since the usernames you're prompting for should be syntactically valid PowerShell statements.
However, supporting arbitrary input is problematic for two reasons:
Inapplicable syntax coloring will be applied - you could, however, temporarily set all configurable colors to the same color, but that would be cumbersome.
More importantly, if an input happens to be something that amounts to a syntactically incomplete PowerShell statement, PSConsoleHostReadline won't accept the input and instead continue to prompt (on a new line); for instance, input a| would cause this problem.
Also:
Whatever input is submitted is invariably added to the command history.
While you can currently remove a temporarily installed keyboard handler on exiting a script, there is no robust way to restore a previously active one - see this GitHub issue.
# Set up a key handler that cancels the prompt on pressing ESC (as Ctrl-C would).
Set-PSReadLineKeyHandler -Key Escape -Function CancelLine
try {
# Prompt the user:
Write-Host -NoNewline 'Enter account name you wish to disable: '
$_Name = PSConsoleHostReadLine
# If ESC was pressed, $_Name will contain the empty string.
# Note that you won't be able to distinguish that from the user
# just pressing ENTER.
$canceled = $_Name -eq ''
# ... act on potential cancellation
} finally {
# Remove the custom Escape key handler.
Remove-PSReadlineKeyHandler -Key Escape
}
I wrote this function that works for me.
# Returns the string "x" when Escape key is pressed, or whatever is indicated with -CancelString
# Pass -MaxLen n to define max string length
function Read-HostPlus()
{
param
(
$CancelString = "x",
$MaxLen = 60
)
$result = ""
$cursor = New-Object System.Management.Automation.Host.Coordinates
while ($true)
{
While (!$host.UI.RawUI.KeyAvailable ){}
$key = $host.UI.RawUI.ReadKey("NoEcho, IncludeKeyDown")
switch ($key.virtualkeycode)
{
27 { While (!$host.UI.RawUI.KeyAvailable ){}; return $CancelString }
13 { While (!$host.UI.RawUI.KeyAvailable ){}; return $result }
8
{
if ($result.length -gt 0)
{
$cursor = $host.UI.RawUI.CursorPosition
$width = $host.UI.RawUI.MaxWindowSize.Width
if ( $cursor.x -gt 0) { $cursor.x-- }
else { $cursor.x = $width -1; $cursor.y-- }
$Host.UI.RawUI.CursorPosition = $cursor ; write-host " " ; $Host.UI.RawUI.CursorPosition = $cursor
$result = $result.substring(0,$result.length - 1 )
}
}
Default
{
$key_char = $key.character
if( [byte][char]$key_char -ne 0 -and [byte][char]$key_char -gt 31 -and ($result + $key_char).Length -le $MaxLen )
{
$result += $key_char
$cursor.x = $host.UI.RawUI.CursorPosition.X
Write-Host $key_char -NoNewline
if ($cursor.X -eq $host.UI.RawUI.MaxWindowSize.Width-1 ) {write-host " `b" -NoNewline }
}
}
}
}
}

Problem with switch statement in a while loop in PowerShell

For whatever reason the While loop works by itself, the Switch statement works by itself, when I combine them.. the While loop works ok, the Switch statement though.. not so much.
y or n are only values the While loop accepts, the problem is that when I give it y or n, none of the code gets executed, the script just finishes.
PowerShell version is 5.1.
While (($UserInput = Read-Host -Prompt "Are you sure? (y/n)") -notmatch '^n$|^y$') {
Switch ($UserInput) {
'y' {
Try {
Write-Output "Success."
}
Catch {
Write-Output "Error."
}
}
'n' {
Write-Output "Cancelled."
}
}
}
here's a rather more robust method to do what you seem to want. it sets up the valid choices, asks for input, detects invalid input, warns about that, displays the "success" or "failure" messages - all without twisty logic. [grin]
$Choice = ''
$ValidChoiceList = #(
'n'
'y'
)
while ([string]::IsNullOrEmpty($Choice))
{
$Choice = Read-Host 'Are you sure? [n/y] '
if ($Choice -notin $ValidChoiceList)
{
[console]::Beep(1000, 300)
Write-Warning ('Your choice [ {0} ] is not valid.' -f $Choice)
Write-Warning ' Please try again & choose "n" or "y".'
$Choice = ''
pause
}
switch ($Choice)
{
'y' {Write-Host 'Success!'; break}
'n' {Write-Warning ' Failure!'; break}
}
}
on screen output ...
Are you sure? [n/y] : t
WARNING: Your choice [ t ] is not valid.
WARNING: Please try again & choose "n" or "y".
Press Enter to continue...:
Are you sure? [n/y] : y
Success!
You are using -notmatch. Hence While loop is resulting in false and loop is not getting executed. As you want to execute the script until you get 'y' or 'n' as input, just use ! which will execute the script until it receives 'y' or 'n' as input.
Use below code:
While (!($UserInput = Read-Host -Prompt "Are you sure? (y/n)") -notmatch '^n$|^y$') {
Switch ($UserInput) {
'y' {
Try {
Write-Output "Success."
}
Catch {
Write-Output "Error."
}
}
'n' {
Write-Output "Cancelled."
}
}
}