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')
CodePudding user response:
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 ran.
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.
CodePudding user response:
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.
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.