How to break out of script using invoke-expression - powershell

I have a powershell script that works like a menu. After making a choice, it starts another powershell script.
The other scripts are fairly large, and I have many points in the scripts where I need to abort that script and return back to the menu.
I call the other scripts using:
invoke-expression -Command "& '.\script.ps1'"
In the script itself, at various locations, I use the following command to break out of the script.
break script
This works well, as it terminates the script, but it does not return to the previous invoked script.
The break command with parameter means that it breaks to a label. If no label is present it will break out of the entire script. But given how I invoke the script currently, it sees everything as one big script and breaks out of everything.
In my subscript, I placed the following at the end of the file:
:End_Of_Script
and used break End_Of_Script but that fails the same.
So my question is boiling down to this:
How can I invoke another script and at any point break out of that script and return back to the previous script without altering the complete structure of that script?
Here's a snippet of what the subscript is:
write-host "1. Choice A"
write-host "2. Choice B"
$choice = read-host "What choice?"
switch ($choice)
1
{
write-host "The choice is A"
}
2
{
write-host "The choice is B"
}
default
{
write-host "Invalid choice"
break script
}
get-childitem | foreach-object {
if( $_.Name -like "*test*")
{
break script
}
}
In my main script, the commands after invoke-expression ... are never executed.

You can use exit to do this.
The exit keyword is best used in functions, and when those functions are called in a script. It's a great way to terminate execution of functions.
reference PowerShell Exit Function & Code Execution | Pluralsight
If the script encounters an error and needs to exit, throw can be used to better prompt the specific cause of the error.
Calling another script can be done with & $FilePath, invoke-expression -Command "& '$FilePath'" is a cumbersome and unnecessary usage.

Related

Powershell Invoke-Expressions pauses

I wrote a Powershell script that uses Steam's command line tool to login and check for updates for a community server I am running. See below:
$steamcmdFolder = 'C:\download\steam'
$steamcmdExec = $steamcmdFolder+"\steamcmd.exe"
$forceinstall = 'force_install_dir'+$steamcmdFolder
$appupdate = 'app_update 258550'
$cmdOutput = "$steamcmdExec +login anonymous"
do {
Write-Host Checking for an update....
Invoke-Expression $cmdOutput
Invoke-expression $forceinstall
Invoke-expression $appupdate
}
while ($Update = 1)
The Invoke-Expression lines are individual command-line statements I want executed in the order I have them. For some reason, the first Invoke-Expression works fine but the others do not -- everything just stops. I can type in the value of $forceinstall on the PowerShell command-line and it works as expected. But why can't I do this using PowerShell? Any suggestions are welcome!
If you convert the other two lines down to what they are, it seems like they are not real commands.
#Invoke-expression $forceinstall
Invoke-Expression "force_install_dirC:\download\steam"
#Invoke-expression $appupdate
Invoke-Expression "app_update 258550"
Looking into the SteamCMD documents, it appears that you might want to change it to be a single line command.
Invoke-Expression "steamcmd +login anonymous +force_install_dir C:\download\steam +app_update 258550 +quit"

Write-output when executing script in a wpf event script

I have a script that accepts lots of parameters and is fairly big. I've been asked to build a GUI for executing the script, so users can easily execute the script without making mistakes. So the idea was that the GUI script collects all the parameters, and then executes the script.
The "main" script uses write-output and start-transcript for logging to the shell and to a file. This also works.
But when I execute the script from the GUI script I'm not getting every output to the shell or to the log. I figured that this is because of write-output because Write-Host does work, but everywhere I go I hear people say that you shouldn't use write-host (for instance: https://youtu.be/SSJot1ycM70?t=24m1s).
So how do I get this to work?
Currently I use this to execute the script from the gui:
& $PSscriptroot\guitest2.ps1 -switchparam1:$localvar1 -switchparam2:$localvar2 -stringparam $localvar3
I have tried to run a new instance of powershell via cmd, but I don't seem to get this to work. I have also no idea how to "send" the parameters to the script this way.
Invoke-Expression 'cmd /c powershell.exe -file C:\...\script.ps1 -paramters.
or
Invoke-Expression 'cmd /c powershell.exe -command {C:\..\script.ps1 -paramters..}
The two scripts below do exactly what I want. But when I execute the script in a wpf event (ie after the start button has been pressed) the second script only shows the write-warning, write-error and write-host output, so not the write-output.
script1:
#gui script
$boolean = $true
$string = "test"
& $PSscriptroot\script2.ps1 -switch:$boolean -string $string
script2:
param(
[switch]$switch,
[string]$string
)
Start-Transcript
write-output "output"
write-host "host"
$a = "Variable"
$a
write-error "error"
Write-Warning "warning"
Write-Debug "debug"
Stop-Transcript
EDIT:
did some more testing. Apparently write-output completely doesn't work when called in a wpf event. For example:
$wpf.startButton.Add_Click({
write-output "This message is not shown"
})
Not sure if this is a bug or not
Write-Output passes an object through the pipeline to another command. Essentially the statement
Write-Output <expression>
is in many cases exactly identical to
<expression>
Other Write-* cmdlets behave a bit differently. In your case you have an event handler, which has no return value anyway. Your example is the same as
"This message is not shown"
or even
return "This message is not shown"
As the string is part of the return value of that script block, which no one ever looks at.
If you want output in the host application, use Write-Host. That's what it's for. Write-Output is something you rarely, if ever, need in a script.

Call a PowerShell script in a new, clean PowerShell instance (from within another script)

I have many scripts. After making changes, I like to run them all to see if I broke anything. I wrote a script to loop through each, running it on fresh data.
Inside my loop I'm currently running powershell.exe -command <path to script>. I don't know if that's the best way to do this, or if the two instances are totally separate from each other.
What's the preferred way to run a script in a clean instance of PowerShell? Or should I be saying "session"?
Using powershell.exe seems to be a good approach but with its pros and cons, of course.
Pros:
Each script is invoked in a separate clean session.
Even crashes do not stop the whole testing process.
Cons:
Invoking powershell.exe is somewhat slow.
Testing depends on exit codes but 0 does not always mean success.
None of the cons is mentioned is a question as a potential problem.
The demo script is below. It has been tested with PS v2 and v3. Script names
may include special characters like spaces, apostrophes, brackets, backticks,
dollars. One mentioned in comments requirement is ability to get script paths
in their code. With the proposed approach scripts can get their own path as
$MyInvocation.MyCommand.Path
# make a script list, use the full paths or explicit relative paths
$scripts = #(
'.\test1.ps1' # good name
'.\test 2.ps1' # with a space
".\test '3'.ps1" # with apostrophes
".\test [4].ps1" # with brackets
'.\test `5`.ps1' # with backticks
'.\test $6.ps1' # with a dollar
'.\test ''3'' [4] `5` $6.ps1' # all specials
)
# process each script in the list
foreach($script in $scripts) {
# make a command; mind &, ' around the path, and escaping '
$command = "& '" + $script.Replace("'", "''") + "'"
# invoke the command, i.e. the script in a separate process
powershell.exe -command $command
# check for the exit code (assuming 0 is for success)
if ($LastExitCode) {
# in this demo just write a warning
Write-Warning "Script $script failed."
}
else {
Write-Host "Script $script succeeded."
}
}
If you're on PowerShell 2.0 or higher, you can use jobs to do this. Each job runs in a separate PowerShell process e.g.:
$scripts = ".\script1.ps1", ".\script2.ps1"
$jobs = #()
foreach ($script in $scripts)
{
$jobs += Start-Job -FilePath $script
}
Wait-Job $jobs
foreach ($job in $jobs)
{
"*" * 60
"Status of '$($job.Command)' is $($job.State)"
"Script output:"
Receive-Job $job
}
Also, check out the PowerShell Community Extensions. It has a Test-Script command that can detect syntax errors in a script file. Of course, it won't catch runtime errors.
One tip for PowerShell V3 users: we (the PowerShell team) added a new API on the Runspace class called ResetRunspace(). This API resets the global variable table back to the initial state for that runspace (as well as cleaning up a few other things). What it doesn't do is clean out function definitions, types and format files or unload modules. This allows the API to be much faster. Also note that the Runspace has to have been created using an InitialSessionState object, not a RunspaceConfiguration instance. ResetRunspace() was added as part of the Workflow feature in V3 to support parallel execution efficiently in a script.
The two instances are totally separate, because they are two different processes. Generally, it is not the most efficient way to start a Powershell process for every script run. Depending on the number of scripts and how often you re-run them, it may be affecting your overall performance. If it's not, I would leave everything AS IS.
Another option would be to run in the same runspace (this is a correct word for it), but clean everything up every time. See this answer for a way to do it. Or use below extract:
$sysvars = get-variable | select -Expand name
function remove-uservars {
get-variable |
where {$sysvars -notcontains $_.name} |
remove-variable
}

PowerShell 2.0 - Running scripts for the command line call vs. from the ISE

After writing deployment scripts from within the ISE, we need our continuous integration (CI) server to be able to run them automatically, i.e. from the command line or via a batch file.
I have noticed some significant differences between the following calls:
powershell.exe -File Script.ps1
powershell.exe -Command "& '.\Script.ps1'"
powershell.exe .\Script.ps1
Some simple examples:
When using -File, errors are handled in the exact same way as the ISE.
The other two calls seem to ignore the $ErrorActionPreference variable, and do not catch Write-Error in try/catch blocks.
When using pSake:
The last two calls work perfectly
Using the ISE or the -File parameter will fail with the following error:
The variable '$script:context' cannot be retrieved because it has not been set
What are the implications of each syntax, and why they are behaving differently? I would ideally like to find a syntax that works all the time and behaves like the ISE.
Not an answer, just a note.
I searched for explanation of -file parameter. Most sources say only "Execute a script file.". At http://technet.microsoft.com/en-us/library/dd315276.aspx I read
Runs the specified script in the local scope ("dot-sourced"), so that the functions
and variables that the script creates are available in the current session. Enter
the script file path and any parameters.
After that I tried to call this:
powershell -command ". c:\temp\aa\script.ps1"
powershell -file c:\temp\aa\script.ps1
powershell -command "& c:\temp\aa\script.ps1"
Note that first two stop after Get-Foo, but the last one doesn't.
The problem I describe above is related to modules -- if you define Get-Foo inside script.ps1, all the 3 calls I described stop after call to Get-Foo.
Just try to define it inside the script.ps1 or dotsource the file with Get-Foo and check it. There is a chance it will work :)
Here is a concrete example of the behaviour I described.
MyModule.psm1
function Get-Foo
{
Write-Error 'Failed'
}
Script.ps1
$ErrorActionPreference = 'Stop'
$currentFolder = (Split-Path $MyInvocation.MyCommand.Path)
Import-Module $currentFolder\MyModule.psm1
try
{
Get-Foo
Write-Host "Success"
}
catch
{
"Error occurred"
}
Running Script.ps1:
From the ISE, or with the -File parameter
will output "Error occurred" and stop
From the command line without the -File parameter
will output "Failed" followed by "Success" (i.e. not caught)

Powershell window disappears before I can read the error message

When I call a Powershell script, how can I keep the called script from closing its command window. I'm getting an error and I'm sure I can fix it if I could just read the error.
I have a Powershell script that sends an email with attachment using the .NET classes. If I call the script directly by executing it from the command line or calling it from the Windows Scheduler then it works fine. If I call it from within another script (IronPython, if that matters) then it fails. All scenarios work fine on my development machine. (I really do have to get that "Works on My Machine" logo!) I've got the call to Powershell happening in a way that displays a command window and I can see a flicker of red just before it closes.
Sorry: Powershell 1.0, IronPython 1.1
Solution: powershell -noexit d:\script\foo.ps1
The -noexit switch worked fine. I just added it to the arguments I pass from IronPython. As I suspected, it's something that I can probably fix myself (execution policy, although I did temporarily set as unrestricted with no effect, so I guess I need to look deeper). I'll ask another question if I run into trouble with that.
Thanks to all for the help. I learned that I need to investigate powershell switches a little more closely, and I can see quite a few things that will prove useful in the future.
Try with the -noexit switch:
powershell -noexit d:\script\foo.ps1
You basically have 3 options to prevent the PowerShell Console window from closing, that I describe in more detail on my blog post.
One-time Fix: Run your script from the PowerShell Console, or launch the PowerShell process using the -NoExit switch. e.g. PowerShell -NoExit "C:\SomeFolder\SomeScript.ps1"
Per-script Fix: Add a prompt for input to the end of your script file. e.g. Read-Host -Prompt "Press Enter to exit"
Global Fix: Change your registry key to always leave the PowerShell Console window open after the script finishes running.
Here are the registry keys to modify for option #3:
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\Applications\powershell.exe\shell\open\command]
#="\"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -NoExit \"& \\\"%1\\\"\""
[HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\Shell\0\Command]
#="\"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -NoExit \"-Command\" \"if((Get-ExecutionPolicy ) -ne 'AllSigned') { Set-ExecutionPolicy -Scope Process Bypass }; & \\\"%1\\\"\""
See my blog for more information and a .reg file that will apply these registry changes automatically.
I've needed this before and usually I didn't want to modify the script (typically for scripts fired off from the Task Scheduler). I just wanted to see what was spit out to console.
All you need to do is just append a Read-Host command after the script invocation e.g.:
PowerShell.exe -command { .\foo.ps1; read-host "Press enter key to continue" }
BTW the problem with using Start-Transcript is that it doesn't capture EXE output. And any form of attempted logging in V1 and even V2 with the standard host will not capture the verbose, debug, progress or warning streams. You can only see these by viewing the associated host window.
One cheesy but effective way to capture all script output (stdout, stderr, verbose, warning, debug) is to use another host like cmd.exe e.g.:
cmd.exe /c powershell.exe "$pwd\foo.ps1" > foo.log
I am generaly fine with scripts autoclosing except when an error occurs, where I need to see the error. Assuming you have not changed $ErrorActionPreference away from the default 'Continue', then for the behaviour I described do this at the end of you script
if ($Error)
{
Pause
}
There is no ordinary Try...Catch construction in Powershell; however you can trap exceptions instead and react properly.
I.E:
Function Example() {
trap [Exception] {
write-host "We have an error!";
write-error $("ERROR: " + $_.Exception.Message);
sleep 30;
break;
}
write-host "Hello world!";
throw "Something very bad has happened!";
}
You can also simulate Try...Catch construction:
Function Example2() {
${
write-host "Our try clause...";
throw "...caused an exception! It hurts!";
}
trap [Exception] {
write-error $_.Exception.Message;
sleep 30;
continue;
}
Of course as soon as you will trap an exception, you can log it, sleep, or whatever you want with the error message. My examples just sleep, allowing you to read what happened, but it's much better to log all the errors. (The simplest way is to redirect them with >>).
Look also at:
http://huddledmasses.org/trap-exception-in-powershell/
A quick and dirty solution is to use CTRL+S to halt the scrolling of the display and CTRL+Q to resume it.
You have three options:
Do a catch in the script (if using
Powershell V2)
Write a dummy
script which catches and redirects
stdout which you can then access as a
variable from your IronPython script.
VBS/Wscript Intro An addition to
this is just liberally drop
Read-Host commands everywhere,
and hit return to page through.
Rather than outputting anything to the shell, wrap your powershell script in a second script that redirects all output to a log file.
PS C:> myscript.ps1 |Out-File myscript.log
Create run_ps_script.bat file containing
#PowerShell.exe -command "try { %1 } finally { read-host 'Press ENTER...' }"
and make it default program to open PowerShell scrips.
My solution was to execute the script with a command line from the console window instead of right-clicking the file -> execute with powershell.
The console keeps displaying the error messages,
even though the execution of the script ended.
Have you thought about redirecting stdout and stderr to a file ex:
./ascript.ps1 >logs 2>&1
Note: You can create wrapper script in powershell that calls your powershell script with all necessary redirections.
My .PS1 script ran fine from the Powershell console but when "double-clicking" or "right-click open with powershell" it would exhibit the 'open/close' problem.
The Fix for me was to rename the script folder to a Name Without Spaces.
Then it all worked - Windows couldn't deal with
"C:\This is my folder\myscript.ps1" but
"C:\This_is_my_folder\myscript.ps1" worked just fine
A couple more ideas...You could use the start-sleep cmdlet at the end of you script to give you enough time to review the error.
You might also be able to use start-transcript to record the session to a text file.