How Do I Exit While Loop From Within a Switch Statement - powershell

In PowerShell, how do I exit this while loop that is nested inside a switch statement, without executing the code immediately following the while block? I can't seem to figure it out. Everything I've tried so far results in that block of code being executed.
Here's what I'm trying to accomplish:
Check for the presence of a file and notify user if file is
detected.
Check again every 10 seconds and notify user
If the file is not detected, then exit the loop and switch, and continue
with Step #2
If the file is still detected after 30 seconds, timeout and exit
the script entirely.
Here's the code:
try {
#Step 1
$Prompt = <Some Notification Dialog with two buttons>
switch ($Prompt){
'YES' {
# Display the Windows Control Panel
#Wait for user to manually uninstall an application - which removes a file from the path we will check later.
$Timeout = New-Timespan -Seconds 30
$Stopwatch = [Dispatch.Stopwatch]::StartNew()
while ($Stopwatch.elapsed -lt $Timeout) {
if (Test-Path -Path "C:\SomeFile.exe" -PathType Leaf) {
Write-Host "The file is still there, remove it!"
return
}
Start-Sleep 10
}
#After timeout is reached, notify user and exit the script
Write-Host "Timeout reached, exiting script"
Exit-Script -ExitCode $mainExitCode #Variable is declared earlier in the script
}
'NO' {
# Do something and exit script
}
}
# Step 2
# Code that does something here
# Step 3
# Code that does something here
} catch {
# Error Handling Code Here
}

You can use break with a label, to exit a specific loop (a switch statements counts as a loop), see about_break.
$a = 0
$test = 1
:test switch ($test) {
1 {
Write-Output 'Start'
while ($a -lt 100)
{
Write-Output $a
$a++
if ($a -eq 5) {
break test
}
}
Write-Output 'End'
}
}
Write-Output "EoS"

Is that what you see without the try/catch? I get an exception: Unable to find type [Dispatch.Stopwatch]. Otherwise the return works ok for me.
I think what you want is break with a label going outside the switch? Then steps 2 & 3 will run. I altered the code to make a manageable example. This is more ideal when asking a question. I don't know what exit-script is.
echo hi > file
#try {
#Step 1
$Prompt = '<Some Notification Dialog with two buttons>'
$Prompt = 'yes'
:atswitch switch ($Prompt){
'YES' {
'Display the Windows Control Panel'
#Wait for user to manually uninstall an application - which removes a file from the path we will check later.
$Timeout = New-Timespan -Seconds 30
#$Stopwatch = [Dispatch.Stopwatch]::StartNew()
while (1) {
if (Test-Path -Path "file" -PathType Leaf) {
Write-Host "The file is still there, remove it!"
break atswitch
}
Start-Sleep 10
}
#After timeout is reached, notify user and exit the script
Write-Host "Timeout reached, exiting script"
'Exit-Script -ExitCode $mainExitCode #Variable is declared earlier in the script'
}
'NO' {
'Do something and exit script'
}
}
# Step 2
'Code that does something here'
# Step 3
'Code that does something here2'
#} catch {
# 'Error Handling Code Here'
#}

Related

PowerShell Switch Executing Again On Quit

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.

Powershell: Trap continue breaks entire loop

Trying to create a better way to handle errors for some automated stuff.
My understanding is that, when using "continue" in Trap, it should just keep looping, but skip the rest of the current iteration. However, in the following code, everything handle as I expect, except in the loop, code completely stops, and no matter what I try I can't get it to keep looping.
trap {
Clear-Host
Write-Host "Running script: '$PSCommandPath' as user: '$(whoami)' with process id (PID): '$PID'`n"
Write-Host "Exception error: " $_
Write-Host "`nVariables at run time:"
Get-Variable *
continue;
}
try {
throw "TryCatchError"
} catch { Write-Host "test 2 caught" $_ }
throw "TrapError"
#this loop doesnt work
While ($true) { Throw "error"; Start-sleep 1}
Can anybody explain why or have a solution that would let the loop continue, while still trapping (not try/catching) the error?
Based on the comment I did the way I would do it is handle this is using a try-catch statement since thats the correct way to do it. This will continue your while-statement gracefully and ignore the Start-sleep 1; row.
While ($true) {
try {
Throw "error";
}
catch {
Clear-Host
Write-Host "Running script: '$PSCommandPath' as user: '$(whoami)' with process id (PID): '$PID'`n"
Write-Host "Exception error: " $_
Write-Host "`nVariables at run time:"
Get-Variable *
continue;
}
Start-sleep 1;
}

Timed loop that restarts if criteria is met or time expires?

I am trying to run through a loop every 3 seconds for xx minutes. Inside the while loop is an IF statement. If the statement is true, then fire off my command and restart the loop for the predefined number of minutes again.
If the if statement is never true after the minutes defined, fire off my command and then restart the loop all over again with the time set above.
I could do this in a batch file with goto but cannot figure out how to do this in PowerShell.
Here is a representation of what I want to accomplish.
$minutes = 5
while(loop for 5 minutes)
{
if(1 -eq 1)
{
do something
restart the while loop for the minutes defined above
}
start-sleep -seconds 3
}
do something here
restart the while loop for the minutes defined above
Update:
Here is what I came up with. This is my first time trying to write a script in PowerShell so I am almost certain there is a more elegant way to write this.
# we need a constant loop going
while(1 -eq 1)
{
# now we need our timed loop. set the timer -seconds 3 (3 seconds right now for testing)
$timeout = new-timespan -seconds 3
$sw = [diagnostics.stopwatch]::StartNew()
while ($sw.elapsed -lt $timeout)
{
# check to see if things are still true
if($something -eq "true")
{
echo "Do nothing."
}
else
{
echo "Do something and restart."
# break out of this timed loop since we want to restart it
break
}
# check every 1 second
start-sleep -seconds 1
}
echo "$something did not equal true in the IF above or the timer has run out. Do something and restart."
# continue restarts the loop
continue
}
Shouldn't you be able to just reset $sw?
$sw = [diagnostics.stopwatch]::StartNew()
while ($sw.elapsed -lt $timeout) {
if ($Condition) {
$sw.Reset()
}
}

How to redirect Access Denied and all other errors in Powershell to a variable (Loop until command doesnt fail)

I am trying to create a bootstrap script to setup servers and add them to the domain. The problem is some of the networking changes do not get implemented for a varying amount of time causing the command to join the domain to fail (always with the same error). TO get around this all I have to do is run the command again a minute later but this is not practical as part of a long script.
My solution is along the lines of:
$exit = 1
while ($exit -ne 0){
$exit = 0
join-domain-command
$exit = $?
sleep 20
}
However $exit is always 0/false even when I put in some non existent command that is bound to fail.
My other idea was to redirect all errors to a variable and search the variable for text containing the error I commonly come across and if that error is not present, stop the loop.
Trying to redirect the stderr in powershell doesn't work for me either
$exit = & (dummy-command -confirm:$false) 2>$exit
echo "exit = $exit"
SO here I deliberately set the ExecPol in an un-elevated prompt:
you can use -errorvariable
$exit = 1
while ($exit -ne 0){
$exit = 0
join-domain-command -errorvariable $error
$exit = $?
sleep 20
}
Or if you want to do something with the error:
try {
join-domain-command -errorvariable $error
}
catch{
$error | out-file C:\errors.txt -append
#or if $error -contains "something"
#...do something
}
then search the text file for your errors
EDIT
So a few things the actual correct use of errorVariable doesnt use the $ so it would be -errorvariable myError
If you want to search an error a better way to do it would be this:
while ($exit -ne 0)
{
try {
join-domain-command
}
catch{
if(!($error[0].tostring().contains("specific error text"))) #error text is not your specific error
{$exit = 1}
}
sleep 20
}
All errors can be found in $error and if you want to check that last error you use $error[0] which give you the last error that was received.
I usually do something like the following and I put it in a separate function to keep the main code path clean. A counter can be added to limit the number of retries.
$Succeeded = $false
while($Succeeded -eq $false){
try{
#commands that may fail
$Succeeded = $true
}
catch{
#write a message or log something
}
start-sleep -s 60
}

How to get script to start over

I have this section of code that I need to loop through if the file is locked.
What I want to do is if the file is locked the script goes to sleep for 10 seconds then goes back to the if (test-Path) and runs through again until the file(s) are no longer locked.
I'm just not understanding how to do this, any help is appreciated.
if (Test-Path -Path "$path\*")
{
# Code for directory not empty
# enumerate the items array
foreach ($item in $items)
{
# if the item is NOT a directory, then process it.
if ($item.Attributes -ne "Directory")
{
$filePath = $path+$item.name
}
else
{
}
function isFileLocked([string]$LockedPath)
{
$oFile = New-Object System.IO.FileInfo $LockedPath
# Make sure the path is good
if ((Test-Path -LiteralPath $LockedPath) -eq $false)
{
#If file is locked go to sleep for 2 minutes and check again, loop until the file is no longer locked.
Start-Sleep -s 10
# Go back and check directory again to see if more files have come in
#return $false
}
You need to use a while or do loop if you want it to go back to the if statement. If you want it to skip to the next item in the foreach loop you can use continue. help about_continue
Edit: example:
Do {
Start-Sleep -s 10
}
Until (Test-Path $LockedPath)
or
#Script block executes as long as condition value = True
While (! (Test-Path $Lockedpath) )
{Sleep 10}
The do until is easier logically to use, but the benefit of the second option is it tests the condition before executing the script block which in some cases (such as this one), it will be more efficient.