Problem with switch statement in a while loop in PowerShell - 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."
}
}
}

Related

ArrayList not displaying when first referenced in function

Facing a couple logistical issues in PowerShell - clearly I'm missing a basic concept:
Setup: Create the menu.ps1 file (shown below), launch PowerShell 7.2.2 and call the file locally.
Issues:
The first time you choose option 1 for the ArrayList ($psArrayList), it does not display (although we see from the initial screen load that the items are populated). If you return to the menu and choose option 1 again, it will display on the second pass. ($psArray does load fine on first try, so is this is a type issue.?)
When the script ends, $psArrayList and $psArray are still in the current session variables, as indicated by: Get-Variable psArray*. Even if I instantiate them with $script:psArrayList = [System.Collections.ArrayList]#() and $script:psArray = #() they seem to stay within the session scope. Is there a "right" way to clear them when the ps1 ends?
menu.ps1 contents:
$psArrayList = [System.Collections.ArrayList]#()
# example of populating later in function etc...
$psArrayList.Add([pscustomobject]#{name="bird";color="blue"})
$psArrayList.Add([pscustomobject]#{name="cat";color="orange"})
$psArrayList.Add([pscustomobject]#{name="bear";color="brown"})
$psArray = #()
# example of populating later in function etc...
$psArray += "dog"
$psArray += "fish"
$psArray += "squirrel"
function End-Script {
Remove-Variable psArray*
Exit
}
function Display-Menu {
[int]$choice=-1
Write-Host "This is a menu..." -ForegroundColor Green
Write-Host "Here are your options:"
Write-Host
Write-Host "`t1 - ArrayList"
Write-Host "`t2 - Array"
Write-Host "`t0 - quit (do nothing)"
Write-Host
while ($choice -lt 0) { $choice= Read-Host -Prompt "Choose 1-2 (or 0 to quit)" }
Process-Menu($choice)
}
function Process-Menu([int]$choice) {
switch($choice) {
1 { Write-Host "You chose ArrayList:"; Write-Output $psArrayList }
2 { Write-Host "You chose Array:"; Write-Output $psArray }
0 { Write-Host "You chose to quit. Exiting."; End-Script }
}
$yn=""
while ($yn -eq "") { $yn= Read-Host -Prompt "Return to main menu? (y/n)" }
if ($yn -eq "y") { Display-Menu } else { Write-Host "Ending..."; End-Script }
}
Display-Menu
Regarding the first issue, you would need to use Out-Host or Out-Default so that both outputs (Write-Host together with the arrays) are correctly displayed to the console. See these helpful answers for in depth details on this:
https://stackoverflow.com/a/50416448/15339544
https://stackoverflow.com/a/34858911/15339544
Regarding the second issue, your End-Script function would have a scope issue, Remove-Variable is trying to remove variables defined inside the function's scope (Local), if you want to target the variables defined outside it (Script), you would need to use the -Scope parameter, for example:
function End-Script {
Get-Variable psArray* | Remove-Variable -Scope Script
# `Remove-Variable psArray* -Scope Script` would be valid too
}
From the cmdlet's Parameters section we can read the following for the -Scope parameter:
A number relative to the current scope (0 through the number of scopes, where 0 is the current scope and 1 is its parent)
In that sense, -Scope 1 would also work.
Below you can see an example of your script with some improvements as well as input validation:
$psArrayList = [System.Collections.ArrayList]#()
$psArrayList.AddRange(#(
[pscustomobject]#{name="bird";color="blue"}
[pscustomobject]#{name="cat";color="orange"}
[pscustomobject]#{name="bear";color="brown"}
))
$psArray = "dog", "fish", "squirrel"
function End-Script {
Get-Variable psArray* | Remove-Variable -Scope Script
}
function Display-Menu {
Write-Host "This is a menu..." -ForegroundColor Green
Write-Host "Here are your options:"
Write-Host
Write-Host "`t1 - ArrayList"
Write-Host "`t2 - Array"
Write-Host "`t0 - quit (do nothing)"
Write-Host
# one of many methods for input validation is a Recursive Script Block:
$tryInput = {
try {
[ValidateSet(0, 1, 2)] $choice = Read-Host "Choose 1-2 (or 0 to quit)"
$choice
}
catch {
Write-Warning 'Invalid choice!'
& $tryInput
}
}
Process-Menu (& $tryInput)
}
function Process-Menu([int] $choice) {
switch($choice) {
1 {
Write-Host "You chose ArrayList:"
$psArrayList | Out-Host
}
2 {
Write-Host "You chose Array:"
$psArray | Out-Host
}
0 {
Write-Host "You chose to quit. Exiting."
End-Script
Return # => Exit this function
}
}
$tryInput = {
try {
[ValidateSet('y', 'n')] $choice = Read-Host "Return to main menu? (y/n)"
$choice
}
catch {
Write-Warning 'Invalid choice!'
& $tryInput
}
}
# No need to check for `N`
if((& $tryInput) -eq 'y') { Display-Menu }
}
Display-Menu

Color Guessing Game in Powershell [closed]

Closed. This question is not reproducible or was caused by typos. It is not currently accepting answers.
This question was caused by a typo or a problem that can no longer be reproduced. While similar questions may be on-topic here, this one was resolved in a way less likely to help future readers.
Closed last year.
Improve this question
I'm working on writing a script in PowerShell for a Color guessing game. The computer randomly picks a color then the player tries to guess the color. I had it working up until I switched some lines of code in the script in an attempt to get these two variables to display correctly. Now, I can't get the code to run past the point where a player declares if they want to play the game or not. My current error is with a do loop, where the console doesn't see that I have a while loop, and so throws an error and won't run the rest of the code.
I managed to get the formatting fixed with Visual Studio Code's format document feature, but I still can't get this one while loop problem figured out.
Write-Host ''; 'Hello again my friend!'; ''
$name = Read-Host "What is your name?"
Write-Host ''; "It's good to see you again, $name! Would you like to guess my favorite color?"; ''
$command = Read-Host #'
"How do you answer? (Yes or No?)"
1. Yes (y)
2. No (n)
3. Quit (q)
Enter Choice
'#
switch -Wildcard ($command) {
'Y' { 'Yes!'; '' }
{ $_ -eq 'y' -or $_ -like 'Ye*' } {
Write-Host "This will be fun! Let us begin!"
Break
}
'N' { 'No!'; '' }
{ $_ -eq 'n' -or $_ -like 'No*' } {
Write-Host "That's too bad, perhaps another day!"
Exit
}
'Q' { 'Quit'; '' }
{ $_ -eq 'q' -or $_ -like 'qu*' } {
Write-Host 'So long!'
Exit
}
default {
'Invalid Command, Please start over.'
Exit
}
}
[string]$playagain = 'y'
[int]$playerwins = 0
[int]$compwins = 0
[int]$totalguesses = 0
[int]$playergames = 0
[int]$compgames = 0
[int]$round = 1
[int]$game = 1
$cpuchoice = $color
while ($playagain -eq 'y') {
Write-Host ''; "Game $game!"; ''
$cpuchoice = #([System.Enum]::GetValues([System.ConsoleColor])) | Get-Random -Count 1
do {
Write-Host "Round $round! What is my favorite color?"; ''
$listcolor = Read-Host "Would you like to see a list of available colors? Choose 'y' for yes, and 'n' for no."
if ($listcolor -eq 'y') {
[System.Enum]::GetValues([System.ConsoleColor])
}
elseif ($listcolor -eq 'n') {
Write-Host "Suit yourself, let's start."
}
else {
Write-Host "Your choice was invalid. Please choose 'y' for yes, or 'n' for no."
}
do {
$playerchoice = Read-host "Enter your guess"
} while (([System.Enum]::GetValues([System.ConsoleColor])) -notcontains $playerchoice) {
if ($playerchoice -eq $cpuchoice ) {
Write-Host "You win, my favorite color is $cpuchoice." -ForegroundColor $cpuchoice; ''
$playerwins = $playerwins + 1
$totalguesses = $totalguesses + 1
}
elseif ($playerchoice -ne $cpuchoice ) {
Write-Host "You lose, try again."; ''
$playerguesses += $playerchoice
$playerguesses = $playerguesses.Split(',')
$totalguesses = $totalguesses + 1
Write-Host "Here are your guesses so far: "
$playerguesses
''
}
$round = $round + 1
}
until($playerwins -eq 1) {
$playergames = $playergames + 1
Write-Host "You've won this round and have won $playergames games." -ForegroundColor Green
Write-Host "Your total guesses: $totalguesses."
Write-Host "Your wins - $playergames" -ForegroundColor Yellow
Write-Host "Computer wins - $compgames" -ForegroundColor Yellow
''
}
$playagain = Read-Host "I enjoyed this game. Would you like to challenge again, $name? Y or N"
while (("y", "n") -notcontains $playagain) {
if ($playagain -eq "y") {
Write-Host "I look forward to our next battle!"; ''
$playerwins = 0
$compwins = 0
$game = $game + 1
}
elseif ($playagain -eq "n") {
Write-Host "Thank you for playing!"
exit
}
}
}
}
The do loop that causes the error is the one that starts with "Write-Host "Round $round!" after the first while statement.
Any help is appreciated! Thanks!
Your program is being parsed like this:
while ($playagain -eq 'y')
{
# do loop #1
do
{
# do loop #2
do {
}
while (([System.Enum]::GetValues([System.ConsoleColor])) -notcontains $playerchoice)
# floating script block #1
# (doesn't execute the scriptblock, but it gets sent to the output stream instead)
{
if ($playerchoice -eq $cpuchoice ) {
... etc ...
}
# try to invoke a cmdlet "until" with 2 parameters
# i.e. ($playerwins -eq 1) and { ... }
until ($playerwins -eq 1) {
$playergames = $playergames + 1
... etc ...
}
# while loop #1
while (("y", "n") -notcontains $playagain) {
...
}
}
}
The error is telling you the first do (do loop #1) doesn't have a trailing while or until.
There's no clear and simple fix I can offer to make your code run short of a significant rework because there's a number of issues (e.g. the floating script block #1, the dangling until and the while \ until-less do), but that's what the current error is saying anyway...

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

Powershell ReadKey() case sensitive

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

Read-Host in While loop with if statement

This should be really simple, but I cannot make it work. I'm new to the PowerShell so my mistake is probably obvious. I'm trying to ask a user for an input using a while loop, and prompting the user to try again if what was entered was not numeric. In bash it would look like this:
while read -p "What is the first number? " first; do
if [[ $first = *[[:digit:]]* ]]; then
break # if $first is numeric, break loop
else
echo "$first is not numeric. Enter a numeric value. "
fi
done
Here's what I have for the PS:
While ($first = (Read-Host "What is the first number?") -as [double]) {
if ($first -eq $null) { Write-Host "Please enter a numeric value." }
else { break }
}
In the example above, I can break the loop fine when a number is entered, but I can't see the Write-Host statement if I type a string.
Looking at the While line:
While ($first = (Read-Host "What is the first number?") -as [double])
This will only enter/continue the loop body when the input is already a double, because it rolls up the cast to double as part of the loop condition. You want to enter the loop body on any input, and only check if it's a double afterwards. At least, that's what the bash code does:
While ($first = (Read-Host "What is the first number?")) {
if ( ($first -as [double]) -eq $null) { Write-Host "Please enter a numeric value." }
else { break }
}
Or you could continue using the cast as part of the condition by negating the whole expression, and thus avoid the need for the awkward break:
While (-not ($first = (Read-Host "What is the first number?") -as [double])) {
Write-Host "Please enter a numeric value."
}
You could use this to keep prompting for a valid number input as you are looking for.
Do {
"Please enter a numeric value." | Out-Host
$first = (Read-Host "What is the first number?") -as [double]
} While($null -eq $first)