I'm attempting to write a PowerShell menu with multiple switches, but can't figure out why previously run commands will execute again on quit. Any help would greatly be appreciated. The code I have so far is as follows:
function Show-Menu {
param (
[string]$Title = 'Menu'
)
Clear-Host
Write-Host "`n============= $Title =============`n"
Write-Host "Press 'A' to run all commands"
Write-Host "Press '1' to run foo"
Write-Host "Press '2' to run bar"
Write-Host "Press 'Q' to quit"
}
do {
Show-Menu
$Selection = Read-Host "`nPlease make a selection"
switch ($Selection) {
'A' {
$Actions = @('foo', 'bar')
}
'1' {
$Actions = "foo"
}
'2' {
$Actions = "bar"
}
}
switch ( $Actions ) {
'foo' {
Write-Host "foo executed"
Start-Sleep -Seconds 2
}
'bar' {
Write-Host "bar executed"
Start-Sleep -Seconds 2
}
}
}
until ($Selection -eq 'q')
Simplify.
Instead of saving actions in a variable, and then taking another step of evaluating that variable... do the actions.
Instead of having a loop that checks the exit condition in a separate location (i.e. using until), use an endless loop and an explicit break. This helps keeping the logic in one place.
function foo { Write-Host "foo"; Start-Sleep -Seconds 1 }
function bar { Write-Host "bar"; Start-Sleep -Seconds 1 }
:menuLoop while ($true) {
Clear-Host
Write-Host "`n============= Menu =============`n"
Write-Host "Press 'A' to run all commands"
Write-Host "Press '1' to run foo"
Write-Host "Press '2' to run bar"
Write-Host "Press 'Q' to quit"
switch (Read-Host "`nPlease make a selection") {
'A' { foo; bar }
'1' { foo }
'2' { bar }
'Q' { break menuLoop }
}
}
Your approach does not work correctly because in your code, pressing Q does not immediately exit the loop, and $Actions is still filled from the last iteration.
That's another lesson: Variable values don't reset on their own in loops. Always set your variables to $null at the start of the loop to get a clean state.
Note the :mainLoop label. Without it, break would only apply to the switch statement itself. See MSDN
That being said, PowerShell has a pretty nifty menu system built-in that you can use.
using namespace System.Management.Automation.Host
function foo { Write-Host "foo"; Start-Sleep -Seconds 1 }
function bar { Write-Host "bar"; Start-Sleep -Seconds 1 }
# set up available choices, and a help text for each of them
$choices = @(
[ChoiceDescription]::new('run &all commands', 'Will run foo, and then bar')
[ChoiceDescription]::new('&1 run foo', 'will run foo only')
[ChoiceDescription]::new('&2 run bar', 'will run bar only')
[ChoiceDescription]::new('&Quit', 'aborts the program')
)
# set up script blocks that correspond to each choice
$actions = @(
{ foo; bar }
{ foo }
{ bar }
{ break menuLoop }
)
:menuLoop while ($true) {
$result = $host.UI.PromptForChoice(
"Menu", # menu title
"Please make a selection", # menu prompt
$choices, # list of choices
0 # default choice
)
& $actions[$result] # execute chosen script block
}
Run this in PowerShell ISE and regular PowerShell to see how it behaves in each environment.
This is happening because of your do...until loop. You're committing to executing the loop before the user input is taken. Since this is the case, $Actions is already set from the previous iteration of the loop, so it runs what had previously run.
This means that if you don't override $Actions for every iteration of the loop, this will happen for other commands as well.
A simple fix to this would be to add a case for q to set $Actions to something that isn't in the switch statement evaluating $Actions. In this case, an empty string should do.
If you need it to work in a similar way for other commands as well, rather than having a case specifically for q, you can use the default case to set the $Actions variable.