gracefully exit from program spawned by powershell script - powershell

Lets assume I have the following script:
$originalPath=pwd
D:\code\ps1\misc\title.ps1 "dynamo db"
$CURPATH = "$PSScriptRoot\path.txt"
$DB_DIR= cat $CURPATH
cd $DB_DIR
java -D"java.library.path=./DynamoDBLocal_lib" -jar DynamoDBLocal.jar
cd $originalPath
This script starts the java program correctly, however, when I hit CTRL+C, the last cd command is not executed.
Is this by design for powershell? Or can I change this?

Written as of PowerShell Core 7.2.5.
When you use Ctrl-C to abort an external program, the current PowerShell script (runspace) is terminated too.
While you cannot prevent your script from getting terminated, you can perform clean-up actions, namely in the finally block of a try / catch / finally statement:
$originalPath = $PWD
# ...
try {
cd $DB_DIR
java -D"java.library.path=./DynamoDBLocal_lib" -jar DynamoDBLocal.jar
} finally {
# This block is *always* executed, but limitations apply
# if Ctrl-C was pressed:
# * Execution invariably ends with this block.
# * You cannot output to the success output stream or error stream,
# neither explicitly nor implicitly: doing so quietly and instantly
# aborts processing.
# * However, the *other* output streams *can* be targeted, namely with
# the following cmdlets (but note that except for Write-Host and Write-Warning,
# producing visible output is opt-in):
# Write-Host, Write-Warning, Write-Verbose, Write-Debug, Write-Information
cd $originalPath
}

Related

PowerShell Tee-Object not capturing debug lines in file

I have a PowerShell script that runs via automation, so I need to capture the output of the script to a file, but I'd also like to capture the commands that were run, to give the output some context (I'd use set -x in a Linux shell script). I can't figure out how to capture those commands into the output file in Windows though; any help is much appreciated!
# script.ps1
Set-PSDebug -Trace 1
Write-Host "Hello, World!"
How I'm calling the script (from cmd):
$ powershell -command "./script.ps1 *>&1 | Tee-Object -FilePath ./output.txt"
Terminal output:
DEBUG: 2+ >>>> Write-Host "Hello, World!"
Hello, World!
output.txt file contents:
Hello, World!
You can see that the output file is missing the debug line that was shown in the terminal. Ideally, I'd like the debug lines to only be in the output file (not the terminal output), but having the debug lines in both places would be fine too.
Note that PowerShell 5.1 is the version I have installed.
Unfortunately, PowerShell's tracing output (activated via Set-PSDebug) operates outside PowerShell's system of output streams, so such output cannot be captured from inside a PowerShell session.[1]
However, an outside caller of PowerShell's CLI[2], such as cmd.exe in your case, does receive tracing output via stdout (the standard output stream)[3], so you can use cmd.exe's redirection to capture everything in a file and print it to the console afterwards.
A simplified example (from cmd.exe):
C:\>powershell -c "Set-PSDebug -Trace 1; Write-Host 'Hello, World!'" > output.txt & type output.txt
DEBUG: 1+ Set-PSDebug -Trace 1; >>>> Write-Host 'Hello, World!'
Hello, World!
[1] Perhaps surprisingly, despite the word "Debug" in the cmdlet name, tracing output does not go through stream number 5, the debugging stream, the way that Write-Debug output does.
[2] Note that you can use this technique of calling the CLI, invariably as a child process, from inside an existing PowerShell session as well, in which case that existing session acts as the outside caller. However, note that passing the command to execute via a script block ({ ... }), which is normally preferable, would not work in this case, because a script block-based invocation uses PowerShell's remoting infrastructure behind the scenes, which preserves the PowerShell-specific output streams (and also type fidelity, with limitations), so that the tracing output again goes straight to the console.
[3] Perhaps surprisingly, all of PowerShell's output streams - as well as to-console-only output such as produced by tracing - are by default mapped to an outside caller's stdout stream - including errors. While you can at least separate out errors on demand via a stderr redirection - 2> - on the caller side, all remaining streams are invariably sent to stdout - see the bottom section of this answer and GitHub issue #7989 for more information.

Why is an unwanted empty line output on running a PowerShell script from within a Windows batch file?

When I execute the following hello.ps1 in PowerShell, I get an output like I would expect, a Hello World! with a newline.
#powershell script hello.ps1
"Hello World!"
Output of run
PS C:\test>.\hello.ps1
Hello World!
PS C:\test>
But when I call the script from a bat file, I get two (2) newlines output in Windows console.
:: Windows CMD script name: mytest.bat
powershell -ExecutionPolicy Bypass hello.ps1
Now the output has two newlines, note the empty line after Hello World!.
C:\test>mytest.bat
Hello World!
C:\test
How can I avoid this second newline?
Mandragor -
Firstly, that is not two lines, it's just one.
Secondly, this is not a PowerShell specific thing.
You are calling one shell from another, and both must provide you a response and return to interactive mode. Calling an external command for one shell to another must be processed by the called shell first and results returned from the called shell to the calling shell, then exit the called process, and that calling shell must complete its process and any stdout stuff, and exit that process.
Double-clicking a bat/cmd file, starts cmd.exe, task manager shows
that process is running
In your bat/cmd you are calling another standalone exe, task manager
shows that the process is running. It must execute and return stdout
results, then close/exit.
cmd.exe the completes, and returns its stdout stuff, if any, and places the cursor at the next line for more interactive work.
Hence, the two responses, only if you ask for cmd.exe stdout stuff will you see it, vs just null.
You'll always have one more line return to get you back to the calling process.
Also, point of note:
Write-Host except in specific circumstances, and what you are doing
is not one of them.
You only need that bypass policy setting if you are in a restricted
environment. RemoteSigned is now the PowerShell default, meaning all local scripts will run, and remote scripts must be signed.
Simple strings should use single quotes in most cases. Double quotes
are for expanding variable content and other formatting scenarios.
Try the same thing calling any other shell, you'll get the same results. Heck, even calling another bat/cmd from a bat/cmd.
Example:
Running mytest.bat in cmd.exe that only contains these two lines, to show that only 1 CRLF is actually returned, per process, then you get bat the interactive shell.
powershell -File D:\Temp\hello.ps1
call d:\temp\hello.bat
# Results
Hello World!
D:\Temp>call d:\temp\hello.bat
D:\Temp>echo 'Hello World!'
'Hello World!'
D:\Temp>
You can change your powershell script like this:
#powershell script hello.ps1
if ($args[0] -eq "cmd") {
write-host "Hello World!" -nonewline
}
else {
"Hello World!"
}
Then change the caller batch file:
#echo off
powershell -executionpolicy Bypass TheNameOfPowershellScript.ps1 "cmd"
Now when run from cmd use the caller batch file and from powershell directly use the ps1 script. It will give the expected result(Tested).

Equivalent of bash "set -o errexit" for windows cmd.exe batch file?

What's the Windows batch file equivalent of set -o errexit in a bash script?
I have a long batch file filled with different programs to run on Windows command line... basically its an unrolled make file with every compiler command that needs to be run to build an exe in a long sequence of commands.
The problem with this method is that I want it to exit the batch command on the first non-zero return code generate by a command in the script.
As far as I know, Windows batch files have a problem where they don't automatically exit on the first error without adding a lot of repetitive boilerplate code between each command to check for a non-zero return code and to exit the script.
What I'm wondering about, is there an option similar to bash's set -o errexit for Windows cmd.exe? or perhaps a technique that works to eliminate too much boilerplate error checking code... like you set it up once and then it automatically exits if a command returns a non-zero return code without adding a bunch of junk to your script to do this for you.
(I would accept PowerShell option as well instead of cmd.exe, except PowerShell isn't very nice with old-unix-style command flags like: -dontbreak -y ... breaking those commands without adding junk to your command line like quotes or escape characters... not really something I want to mess around with either...)
CMD/Batch
As Ken mentioned in the comments, CMD does not have an equivalent to the bash option -e (or the equivalent -o errexit). You'd have to check the exit status of each command, which is stored in the variable %errorlevel% (equivalent to $? in bash). Something like
if %errorlevel% neq 0 then exit /b %errorlevel%
PowerShell
PowerShell already automatically terminates script execution on errors in most cases. However, there are two error classes in PowerShell: terminating and non-terminating. The latter just displays an error without terminating script execution. The behavior can be controlled via the variable $ErrorActionPreference:
$ErrorActionPreference = 'Stop': terminate on all errors (terminating and non-terminating)
$ErrorActionPreference = 'Continue' (default): terminate on terminating errors, continue on non-terminating errors
$ErrorActionPreference = 'SilentlyContinue': don't terminate on any error
PowerShell also allows more fine-grained error handling via try/catch statements:
try {
# run command here
} catch [System.SomeException] {
# handle exception of a specific type
} catch [System.OtherException] {
# handle exception of a different type
} catch {
# handle all other exceptions
} finally {
# cleanup statements that are run regardless of whether or not
# an exception was thrown
}

Powershell executable isn't outputting to STDOUT

From a powershell script (with nuget installed and on the path), I'm trying to execute an executable (.net, if that matters) ... but for some reason, I can't get the STDOUT to display in the command window.
nuget install mdoc -OutputDirectory packages -ExcludeVersion
start-process "packages/mdoc/tools/mdoc.exe" "--version"
echo "done"
This should output mdoc 5.7.2 (at the time of this post, current version). But you'll see the nuget output, then done.
Any thoughts on why this is not showing up?
As Ansgar's comment implies: On Windows, Start-Process runs console programs in a new console window by default, asynchronously.
If that program completes quickly, you may see the new console window flash only briefly, as it opens and closes soon thereafter, or you may miss the flash altogether - either way, its output will not show in the caller's console window.
Adding -Wait to the Start-Process call would make the invocation synchronous, and adding -NoNewWindow would make it run in the same console, yet the calling PowerShell session wouldn't be able to capture or redirect the invoked program's output - see below.
Taking a step back: Do NOT use Start-Process[1] if you want to run a console program synchronously, with its standard streams connected to PowerShell's streams - just invoke such a program directly:
packages/mdoc/tools/mdoc.exe --version
If the external program's path / name must be quoted (because its path contains spaces) and/or it is stored in a variable, simply use &, the call operator, to invoke it:
# Via a quoted string:
& "packages/mdoc/tools/mdoc.exe" --version
# Via a variable:
$exePath = "packages/mdoc/tools/mdoc.exe"
& $exePath --version
Using the direct-invocation approach gives you:
synchronous execution by default
the ability to capture and/or redirect the invoked program's stdout and stderr streams.
access to the program's process exit code is via PowerShell's automatic $LASTEXITCODE variable.
To put it all together (based on your later comments):
nuget install mdoc -OutputDirectory packages -ExcludeVersion
$exePath = "packages/mdoc/tools/mdoc.exe"
& $exePath --version
"done"
This prints the version number - mdoc 5.7.2 as of this writing - just before printing done (verified on Windows PowerShell v5.1.17134.48 on Microsoft Windows 10 Pro (64-bit; Version 1709, OS Build: 16299.371)).
Optional reading: capturing stdout / stderr output from external programs:
To capture stdout output, simply assign the call to a variable:
$version = & $exePath --version # $version receives stdout output as an *array of lines*
$version receives either a string scalar (single string) if there was only 1 line of output, or an array of strings representing the output lines.
To also capture stderr output, use redirection 2>&1:
[string[]] $allOutput = & $exePath --version 2>&1
Note the cast to [string[]], which ensures that the stderr lines are captured as strings too.
By default, they are captured as [System.Management.Automation.ErrorRecord] instances, which in Windows PowerShell will somewhat confusingly print them as if they were PowerShell errors - this problem has been fixed in PowerShell Core.
Conversely, however, if you don't convert the type of the elements of the array that is returned to strings, you can examine each element with -is [System.Management.Automation.ErrorRecord] to determine whether it originated from stdout or stderr.
This answer uses this approach to separate stdout lines from stderr lines after merging with 2>&1, so that they can be saved to separate files (which with > in Windows PowerShell requires explicitly stringifying the [System.Management.Automation.ErrorRecord] instances).
Note: When PowerShell communicates with external programs, character-encoding issues come into play: see this answer for details.
[1] Or the underlying .NET API, System.Diagnostics.Process.
For guidance on when Start-Process is or isn't appropriate, see GitHub docs issue #6239

Can I make the PowerShell $error object behave consistently in both script and interactive mode?

I'm writing a PowerShell script that uses $error to detect and respond to errors. My problem is that I get different behavior for the $error object depending on how I run the script. If I run it interactively (specifically, from PowerShell ISE), then errors get added to the collection, but if I run the same script from the command line, the same errors occur, but are not added to the collection.
Here's the script (pared down to illustrate the issue):
# export_exception_test.ps1
# show me how many errors before we start
"Count = " + $error.Count
try {
# treat non-terminating errors as terminating errors.
$ErrorActionPreference = "Stop"
# echo the setting to output
$ErrorActionPreference
# use sqlcmd to save the output of stored procedures to text files.
# The first and third call will fail because the output folder does not exist.
# The second call will succeed.
'first call to sqlcmd'
sqlcmd -S myserver\myinstance -E -s `"`t`" -Q "EXEC mydatabase.dbo.FI_Codes" -b -o "B:\Exports_new\FI_Codes.txt"
'second call to sqlcmd'
sqlcmd -S myserver\myinstance -E -s `"`t`" -Q "EXEC mydatabase.dbo.FI_Codes" -b -o "B:\Exports_newt\FI_Codes.txt"
'third call to sqlcmd'
sqlcmd -S myserver\myinstance -E -s `"`t`" -Q "EXEC mydatabase.dbo.FI_Codes" -b -o "B:\Exports_new\FI_Codes.txt"
# and a whole bunch more of these...
# The error count should be two more than when we started.
"Count = " + $error.Count
# And this should be the most recent error message
"Message = " + $error[0].Message
}
catch [Exception]
{
'exception was caught!!!'
"Count in catch clause = " + $error.Count
$_.Exception.Message
# the ultimate goal is to return a non-successful return code when the exports fail
exit 1
}
finally {
# set this back to what it was
$ErrorActionPreference = "Continue"
# primitive trace output
'finally.'
}
When I run this from PowerShell ISE, it behaves as expected. SQLCMD raises a non-terminating error, which is caught and handled. Here's the output:
PS U:\> C:\Users\etmatt\Documents\PowerShellScripts\export_exception_test.ps1
Count = 0
Stop
first call to sqlcmd
exception was caught!!!
Count in catch clause = 1
Sqlcmd: Error: Error occurred while opening or operating on file B:\Exports_new\FI_Codes.txt (Reason: The system cannot find the path specified).
finally.
But when I run it from the command line, as I would if I were to set up a scheduled task, nothing gets added to $error, and no exceptions are raised. Here's the output:
C:\Users\etmatt>powershell.exe -file "C:\Users\etmatt\Documents\PowerShellScripts\export_exception_test.ps1"
Count = 0
Stop
first call to sqlcmd
Sqlcmd: Error: Error occurred while opening or operating on file B:\Exports_new\FI_Codes.txt (Reason: The system cannot find the path specified).
second call to sqlcmd
third call to sqlcmd
Sqlcmd: Error: Error occurred while opening or operating on file B:\Exports_new\FI_Codes.txt (Reason: The system cannot find the path specified).
Count = 0
Message =
finally.
C:\Users\etmatt>
I also get the same results if I run the script file from the powershell command line rather than cmd.exe (which makes sense). For example:
PS C:\> ."C:\Users\etmatt\Documents\PowerShellScripts\export_exception_test.ps1"
I suspect this has something to do with the execution context or maybe something about parser modes that I'm still not quite grokking, or maybe even my profile (although so far I've been running everything from the same PC under the same account). I've seen a lot of examples online that use this basic approach of try/catch with $ErrorActionPreference = "Stop", so it seems like I should be able to make this work.
So my question is essentially: can I make this script work like I think it should? And if not, what am I misunderstanding? How do I catch these sorts of errors? If it helps, I don't need super detailed exception handling for this one, I just need to know when something goes wrong so that my task monitor can alert me. A non-zero return code from powershell.exe would be sufficient.
I'm looking into the use $?, and $LASTEXITCODE, but I'm betting $? will have the same issue as $error, and both only apply to the last statement executed, which is less than ideal since my real script is quite a bit longer than this example.
EDIT:
OK, I've since learned that Windows executables (such as sqlcmd), never add anything to the $error collection, even if they return a non-zero exit code. $error is only used by cmdlets, if I understand correctly. I got my script working through the repeated use of $LASTEXITCODE, although I could have used $? as well, because it recognizes Windows executable exit codes and gets set to false for non-zero values.
To sum up, the behavior of the script when run from the command line is the correct, expected behavior. Still don't know why I got different results from the PowerShell ISE, though.
For me I am calling batch command using cmd /C <command> and I have the similar problem. For non zero return code $? is set correctly but there is no $error object (how would powershell know? so this is ok) and the code behaves similarly in ISE as well as command line execution.
My problem is that in such cases when $ErrorActionPreference = "Stop" it is not throwing exception. Probably because it doesn't have $error object to throw. So now I need to check $? after every call to other script or program which is not neat. They should come up with good solution for such situation (probably by throwing in-built exception saying call to external script/program failed)