A good old fashioned RTFM would have probably saved me a bunch of time the other week, while playing around with MSBuild’s <Exec ... />
task. In order for future me or anyone else randomly stumbling up on my blog, to not have to succumb to the same fate, let me share a few tips and as usual some reference links at the bottom.
The Basics
The basics of an Exec
-task is to launch an executable from an MSBuild target, for example like this:
<Target Name="Extract">
<Exec Command="7z x Tst.7z"/>
</Target>
It’s important to understand that MSBuild will capture the return/exit code of the command and anything that is non-zero (negative or positive) will be interpreted as failure.
Additionally, the command can produce output on stdout and stderr, for which MSBuild provides options on how to handle.
What isn’t obvious at all, is that MSBuild also runs regular expressions (regex) against the output to determine if there was a warning or an error. This can lead the MSBuild process to fail, even though the command’s exit code was zero.
The Silence
In case you don’t care about any of the potential errors, you can silence the checking of the exit code as well as disable all output parsing:
<Target Name="Extract">
<Exec Command="7z x Tst.7z" IgnoreExitCode="true" IgnoreStandardErrorWarningFormat="true"/>
</Target>
The Regex
Now for the step that cost me a couple of hours to figure out. By default the Exec
task comes with a regular expression for determining, if there’s an error in the output and one for detecting warnings. This means, that even if you ignore the exit code or always return zero, the task will still be marked as failed, if the regex matches on the output. You can find the default regular expression on GitHub.
Luckily you can change those expressions, but there’s another big gotcha, that I only understood by reading the source code, because the documentation isn’t very clear on this. My impression was, that if you provide your own custom regular expression, it would simply override the default regex, but that’s not true! What happens instead is, that it’s a progressive failure. First it checks for any matches in the custom regex, if nothing matches, it will then try to match the default regular expression. If you don’t want this to happen, you need to set IgnoreStandardErrorWarningFormat
to true
.
<Target Name="Extract">
<Exec Command="7z x Tst.7z" CustomErrorRegularExpression="!! Error(?!.*NOTHING)"/>
</Target>
The regular expression should match a string that contains !! Error
and is not followed by the string NOTHING
at some point afterwards. For example !! Error: CRASH
would match, but !! Error: REPORTING_NOTHING
would not match.
This works, but the issue as presented above will manifest, as the standard error regex just looks for the word error
and thus while my regex doesn’t match, the standard one will and the task fails regardless. Once you understand this, the fix is trivial:
<Target Name="Extract">
<Exec Command="7z x Tst.7z" CustomErrorRegularExpression="!! Error(?!.*NOTHING)" IgnoreStandardErrorWarningFormat="true"/>
</Target>
The Output
The Exec
task also offers some options on how to handle the output of the task. For one you can fetch the output contents into an array with the Outputs
option. I’ve never used that, so can’t say more about it. Next, you can make sure that the standard output and standard error streams are captured and provided to MsBuild by setting ConsoleToMsBuild
to true
. If needed, you can further process this with ConsoleOutput
, but I just enabled it, so that the output appears in MsBuild’s output.
The Bonus
One final tip for the output handling when calling a PowerShell script. If you want to do some filtering on the output within the PowerShell script, while still streaming the output to stdout, you can use the Tee-Object
cmdlet, which essentially inserts a junction into the data stream. Meaning, you’re writing the stream of data to the output and to a variable at the same time.
Invoke-Expression "7z x Tst.7z" | Tee-Object -Variable expressionOutput
The PS
Putting it all together and utilizing a PowerShell script to get even more control over the output filtering and exit code handling:
<Target Name="Extract">
<Exec Command="powershell.exe Extract.ps1 Tst.7z" CustomErrorRegularExpression="!! Error(?!.*NOTHING)" IgnoreStandardErrorWarningFormat="true"/>
</Target>
param(
[Parameter(Mandatory)]
[string]$Archive
)
# Run 7z and capture the output, while still writing to stdout
Invoke-Expression "7z x $Archive" | Tee-Object -Variable expressionOutput
# NOTHING is not considered a failure
if ($expressionOutput -and $expressionOutput -match "NOTHING") {
# Insert custom handling and exit with a zero exist code
exit 0
}