Option Menu in Powershell: Continue After Loop - powershell

I would like to create a simple menu in powershell. If you look at my example bellow, I am waiting in do loop until the key 'q' is pressed. If user hits '1' or '2', it should do something specified in switch, leave the loop and continue in script by showing "Another part of code".
If break is used in switch, it doesn't leave the loop. If return is is used, the entire script is exited.
Here's the code:
function Select-Machine
{
cls
Write-Host "1: Option1"
Write-Host "2: Option2"
Write-Host
}
Do{
Select-Machine
$input = Read-Host "Choose Virtual Machine"
switch ($input)
{
'1' {
#1: Do Something
break
}
'2' {
#2: Do Something
break
}
}
}until($input -eq 'q')
Write-Host "Another part of code"

When you use the break statement within a switch you only exit the switch (e.g it doesn't attempt to evaluate any of the other options). You could instead replace where you have break with $input = 'q' and this will result in the loop then completing.
Alternatively you could have an if after the switch to check a list of valid options and do a break at that point, which would then operate on the loop. E.g:
if ($input -in '1','2') { break }

What I would do is use a Do/While loop instead of a Do/Until loop. Also, move the switch outside of the loop, there's no need to have it in there.
Do{
Select-Machine
$Response = Read-Host "Choose Virtual Machine"
}While($Response -notin 1,2,'q')
Switch($Response){
1 {
#Do 1 stuff!
continue
}
2 {
#Do 2 stuff!
continue
}
}
I've done this a number of times in scripts that I know are going to be interactive and have a menu for options. Towards that end I even wrote a function to make decent looking menus for me:
Function MenuMaker{
param(
[parameter(Mandatory=$true)][String[]]$Selections,
[switch]$IncludeExit,
[string]$Title = $null
)
$Width = if($Title){$Length = $Title.Length;$Length2 = $Selections|%{$_.length}|Sort -Descending|Select -First 1;$Length2,$Length|Sort -Descending|Select -First 1}else{$Selections|%{$_.length}|Sort -Descending|Select -First 1}
$Buffer = if(($Width*1.5) -gt 78){[math]::floor((78-$width)/2)}else{[math]::floor($width/4)}
if($Buffer -gt 6){$Buffer = 6}
$MaxWidth = $Buffer*2+$Width+$($Selections.count).length+2
$Menu = #()
$Menu += "╔"+"═"*$maxwidth+"╗"
if($Title){
$Menu += "║"+" "*[Math]::Floor(($maxwidth-$title.Length)/2)+$Title+" "*[Math]::Ceiling(($maxwidth-$title.Length)/2)+"║"
$Menu += "╟"+"─"*$maxwidth+"╢"
}
For($i=1;$i -le $Selections.count;$i++){
$Item = "$(if ($Selections.count -gt 9 -and $i -lt 10){" "})$i`. "
$Menu += "║"+" "*$Buffer+$Item+$Selections[$i-1]+" "*($MaxWidth-$Buffer-$Item.Length-$Selections[$i-1].Length)+"║"
}
If($IncludeExit){
$Menu += "║"+" "*$MaxWidth+"║"
$Menu += "║"+" "*$Buffer+"X - Exit"+" "*($MaxWidth-$Buffer-8)+"║"
}
$Menu += "╚"+"═"*$maxwidth+"╝"
$menu
}
Then I can just call it like:
MenuMaker -Selections 'SQL Server','Exchange Server' -Title 'Choose Virtual Machine' -IncludeExit
And it will spit back:
╔═══════════════════════════════════╗
║ Choose Virtual Machine ║
╟───────────────────────────────────╢
║ 1. SQL Server ║
║ 2. Exchange Server ║
║ ║
║ X - Exit ║
╚═══════════════════════════════════╝
To put that in context to your code, and my suggestions, it would look like:
Do{
cls
MenuMaker -Selections 'SQL Server','Exchange Server' -Title 'Choose Virtual Machine' -IncludeExit
$Response = Read-Host "Choose Virtual Machine"
}While($Response -notin 1,2,'x')
Edit: I just realized that my while vs until argument is pointless, all it does is flip the -in to -notin. The point is that you need to validate their response against the possible options, not just against 'q'.

You can use labels with break to jump out from the outer loop. I don't think it's especially beautiful, much better is to turn until condition true. But here we go. Oh, and please don't use $input variable name, that's PowerShell's automatic variable reserved for, em, input.
Write-Host "Begin"
:menu Do {
$i = Read-Host "Select"
switch ($i) {
'1' {
write-host "Do 1"
break menu
}
'2' {
write-host "Do 2"
break menu
}
}
} until($i -eq 'q')
Write-Host "End"

I think you can simplify this a bit and remove the function:
Do{
#Select-Machine
write-Host "input 1 or 2 - Choose Virtual Machine"
if ($input = Read-Host "type in 1 or 2"){
switch ($input)
{
'1' {
#1: Do Something
Write-host '1 doing something'
break
}
'2' {
#2: Do Something
write-host '2 doing something'
break
}
}}
}until($input -eq 'q')
Write-Host "Another part of code"

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

PowerShell Show Menu

I have the next code written in PowerShell.
When I run the code, the Menu is shown, I enter the option 1..9 and the selected option is not called, the menu is shown again and again(see the screenshot).
When I enter an option I want to be called that function related to each option entered then to display the message "The function has been called" and also to display the menu to enter a new option. (see the scrennshot - code in C++)
Any idea ?
function PORII
{
Write-Host " PORII was called"
}
function DXD-BODY
{
Write-Host " DXD BODY was called"
}
function DXD-PAINT
{
Write-Host " DXD PAINT was called"
}
function DXD-PTO
{
Write-Host " DXD PTO was called"
}
function DXD-TCF
{
Write-Host " DXD TCF was called"
}
function FIS-SERVERS
{
Write-Host " FIS SERVERS was called"
}
function SERVERS
{
Write-Host " SERVERS was called"
}
function Acronis
{
Write-Host " Acronis was called"
}
function Menu
{
param([string]$Title = 'Menu')
Write-Host " ==================== $Title ==================== "
while (1)
{
Clear-Host
Write-Host " Press 1 for PORII: "
Write-Host " Press 2 for DXD BODY: "
Write-Host " Press 3 for DXD PAINT: "
Write-Host " Press 4 for DXD PTO: "
Write-Host " Press 5 for DXD TCF: "
Write-Host " Press 6 for FIS SERVERS: "
Write-Host " Press 7 for SERVERS: "
Write-Host " Press 8 for Acronis Images: "
Write-Host " Press 9 for Exit: "
$a = Read-Host -Prompt "`n Enter your option "
if (($a -eq 1) -or ($a -eq 2) -or ($a -eq 3) -or ($a -eq 4) -or ($a -eq 5) -or ($a -eq 6) -or ($a -eq 7) -or ($a -eq 8) -or ($a -eq 9))
{
switch($a)
{
1{PORII}
2{DXD-BODY}
3{DXD-PAINT}
4{DXD-PTO}
5{DXD-TCF}
6{FIS-SERVERS}
7{SERVERS}
8{Acronis-Images}
9{Exit}
}
}
else
{
continue
}
}
}
Menu
Your functions are being called, but that isn't obvious because Clear-Host is called right after, discarding the called function's output.
Apart from that, your code can be streamlined:
Instead of the if (($a -eq 1) -or ($a -eq 2) ... conditional, you can add a default branch to your switch statement.
Also note that Read-Host always returns a string, whereas your conditionals operate on numbers; thanks to PowerShell's automatic type conversions, this isn't a problem in your particular case (with explicit or implied equality comparison), but it's something to keep in mind.
Through the use of a hash table you can simplify your function while making it easier to maintain.
Function Menu {
param([string]$Title = 'Menu')
while ($TRUE) {
$OptionHT = #{
1="PORII"
2="DXD-BODY"
3="DXD-PAINT"
4="DXD-PTO"
5="DXD-TCF"
6="FIS-SERVERS"
7="SERVERS"
8="Acronis-Images"
9="Exit"
}
Write-Host " ==================== $Title ==================== "
For ($Cntr = 1 ; $Cntr -lt $($OptionHT.Count) + 1; $Cntr++) {
Write-Host "Press $Cntr for $($OptionHT.$($Cntr)):"
}
$a = Read-Host -Prompt "`n Enter your option "
If (($a.length) -eq 1 -and ([byte][char]$a) -ge 49 -and
([byte][char]$a) -le 57) {
& ($OptionHT.[int]$a)
}
} # End While ($True)
} # End Function Menu
By placing your options in the hash table you only have a single point to make changes for called function names.
The if statement vs switch eliminates any value other than a single single number from 1 to 9 (note use of ASCII values to verify number input) from being processed. And since we have eliminated invalid inputs a single statement can be used to search the hash table for the function to execute.
UPDATE: Per the comments below you'll have to either trap the EXIT (9) with an If statement and exit or Create another Function and call it something like Exit-Program and place the Exit command there, I tested a function and it works.
Note: I didn't clear the console between writes of the menu so you could see your selection as mentioned in the comments you can add it where you deem necessary.
HTH

If not empty jump to somewhere in PS

Hello and good morning(:
I'm looking to see if I'm able to jump to somewhere in PS without wrapping it in a ScriptBlock; hell, I'd even be okay with that but, I'm just unsure on how to go about it.
What I'm trying to do is: add a Parameter Set to a function and if something is supplied to the parameter -GrpSelec(I know imma change it), then just skip the rest of the script and go to my $swap variable to perform the switch.
$Group1 = #("1st Group", "2nd Group")
$Group2 = #("3rd Group", "4th Group")
Function Test-Group{
param(
[ValidateSet("Group1","group2")]
[array]$GrpSelec)
if($GrpSelec){ &$swap }
$AllGroups = #("Group1", "Group2")
for($i=0; $i -lt $AllGroups.Count; $i++){
Write-Host "$($i): $($AllGroups[$i])"}
$GrpSelec = Read-Host -Prompt "Select Group(s)"
$GrpSelec = $GrpSelec -split " "
$swap = Switch -Exact ($GrpSelec){
{1 -or "Group1"} {"$Group1"}
{2 -or "Group2"} {"$Group2"}
}
Foreach($Group in $swap){
"$Group"}
}
Is something like this even possible?
I've googled a couple of similar questions which point to the invocation operator &(as shown above), and/or, a foreach which is definitely not the same lol.
take it easy on me, im just experimenting(:
How about a simple if statement?
function Test-Group {
param(
[string[]]$GrpSelec
)
if(!$PSBoundParameters.ContainsKey('GrpSelect')){
# no argument was passed to -GrpSelec,
# populate $GrpSelec in here before proceeding with the rest of the script
}
# Now that $GrpSelec has been populated, let's do the work
$swap = Switch -Exact ($GrpSelec){
{1 -or "Group1"} {"$Group1"}
{2 -or "Group2"} {"$Group2"}
}
# rest of function
}

Powershell terminates early after exiting do-until loop

I'm working on a Powershell script, and I'm trying to accept custom user input from a list of options. The issue I'm running into is that when the exit condition (typing the character 'd') is met, the script terminates and does not execute the remaining code (which is supposed to copy shortcuts based on a user-defined array, $OfficeProgramNames)
#OfficeShortcuts -- Creates shortcuts for the main four Office 2016 applications.
#Optional command-line parameters that can be passed to create icons for All (-a) icons, or a CUSTOM set of icons (-c)
param([switch] $a, [switch] $c)
#Creates a shortcut on the desktop that copies from the start menu shortcuts.
function CreateOfficeDesktopShortcut([string] $ShortcutName)
{
Copy-Item -Path "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\$ShortcutName.lnk" "$env:USERPROFILE\Desktop"
}
function Show-Menu
{
Clear-Host
Write-Host "Select which items you would like placed on the desktop:"
Write-Host "1: Word"
Write-Host "2: Excel"
Write-Host "3: PowerPoint"
Write-Host "4: Outlook"
Write-Host "5: Access"
Write-Host "6: OneNote"
Write-Host "7: Publisher"
Write-Host "D: Press 'D' when done."
}
function GetCustomUserSelection($OfficeProgramNames)
{
$OfficeProgramNames = #()
do
{
Clear-Host
Show-Menu
Write-Host "You have selected: $OfficeProgramNames"
$foo = Read-Host "Please make a selection:"
switch($foo)
{
'1'{
$OfficeProgramNames += "Word*"
}
'2'{
$OfficeProgramNames += "Excel*"
}
'3'{
$OfficeProgramNames += "PowerPoint*"
}
'4'{
$OfficeProgramNames += "Outlook*"
}
'5'{
$OfficeProgramNames += "Access*"
}
'6'{
$OfficeProgramNames += "OneNote*"
}
'7'{
$OfficeProgramNames += "Publisher*"
}
'8'{
$OfficeProgramNames += "Skype*"
}
'd'{
return
}
}
}
until ($foo -eq 'd')
Write-Host "Now do something else"
}
#Loop through the array of Shortcut names, and create a shortcut with a matching alias
function SelectOfficeShortcuts($arg1, $arg2)
{
[string[]] $OfficeProgramNames = #("Word", "Excel", "PowerPoint", "Outlook")
if ($a -eq $True)
{
$OfficeProgramNames += "Access", "OneNote*", "Publisher"
}
elseif($c -eq $True)
{
$OfficeProgramNames = #()
GetCustomUserSelection $OfficeProgramNames
}
for ($i=0; $i -lt $OfficeProgramNames.Length; $i++)
{
CreateOfficeDesktopShortcut $OfficeProgramNames[$i]
}
}
SelectOfficeShortcuts($a, $c)
I have tried inserting a couple of "Write-Host" commands to debug and see where the code stops terminating. I'm rather baffled and I'm having trouble seeing where the control flow is, so if someone could help me with some clarification, that would be greatly appreciated!
For further clarity, the output is as follows(slightly modified since there are some Clear-Host commands):
PS C:\Users\atroach\Documents\GitHub\OfficeShortcuts> .\OfficeShortcuts.ps1 -c
...
Select which items you would like placed on the desktop:
1: Word
2: Excel
3: PowerPoint
4: Outlook
5: Access
6: OneNote
7: Publisher
D: Press 'D' when done.
You have selected: Excel PowerPoint
Please make a selection: d
PS C:\Users\username\Documents\GitHub\OfficeShortcuts>
"SelectOfficeShortcuts ($a, $c)" is called at the end of the script, where $a and $c are switch parameters for the script.