Error Handling with MSBuild’s Exec Task

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
}

References

Leave a Comment

Your email address will not be published. Required fields are marked *

 

This site uses Akismet to reduce spam. Learn how your comment data is processed.