MSBuild Task, External programs and embedded quotes in the command-line

All right, here’s today’s topic…”How to waste time trying to use the MSBuild task to run an external program whose command-line contains embedded quotes”.

Need: I was trying to call to a custom console application that performs code-generation for the passed project. Due to the application needing fully-qualified file paths, I needed to place quotes around the parameters. Here are the relevant snippets from the TFS Team Build .proj file:

<CodeGenUtilityPath>“C:Program FilesCodeGenerationUtilityCodeGenerationUtilityCodeGenerationUtility.exe”CodeGenUtilityPath>

<Exec
Condition=‘@(SolutionToBuild->’%(HasCodeGeneration)’)’ == ‘true’
Command=$(CodeGenUtilityPath) /f=”%(SolutionToBuild.RootDir)%(SolutionToBuild.Directory)” /w=”$(WorkspaceName)” /l=”$(PathToLog)”
/>

Problem: When run, the following error was encountered:

Task “Exec”

Command:
“C:Program FilesCodeGenerationUtilityCodeGenerationUtility.exe”
/f=”C:ProjectsCommonContracts_2_0 ” /w=”VPC-XXXX_XXXX_Common SnapBuild” /cl=”Common SnapBuild_20060525.7″ /r /l=”
\Test4filedropCommon SnapBuild_20060525.7CodeGenLog.xml”

CodeGenerationUtility
=========================

Given a path to a folder, retrieves the list of files that match the pattern *Codegen?.*, checks them out of VSS, generates
code from the .xml files and checks them all back in.
– – – – – –

‘/f’ is not recognized as an internal or external command, operable program or batch file.

‘\Test4filedropCommon’ is not recognized as an internal or external command, operable program or batch file.

‘” ‘ is not recognized as an internal or external command,operable program or batch file.

Issue: As you can see, the command parser is correctly interpreting the quotes around the executable path (as shown by the console app’s help being displayed), but it is not passing the app’s command-line args to it. It is instead trying to run these as commands. It is also splitting the command line arguments without regard to the passed quotes around the paths. It sees the space within \Test4filedropCommon SnapBuild_20060525.7CodeGen.xml as an argument separator. This causes cmd.exe to believe that these are 2 separate arguments/commands. Ok, so how do we deal with this?

First off, let’s have a primer on the task (from MSDN):

This task is useful when a specific MSBuild task for the job that you want to perform is not available. One disadvantage of using the Exec task rather than a more specific task is that it cannot gather output from the tool or command that it runs.

The Exec task calls cmd.exe instead of directly invoking a process.

The last line is what concerns us. When you shell out to cmd.exe, you have a new set of rules to follow with regard to quotes around strings in your command-line.

Here is the relevant parts of the help text from running cmd.exe /?:

Starts a new instance of the Windows XP command interpreter

CMD [/A /U] [/Q] [/D] [/E:ON /E:OFF] [/F:ON /F:OFF] [/V:ON /V:OFF]
[[/S] [/C /K] string]

/C Carries out the command specified by string and then terminates
/K Carries out the command specified by string but remains

If /C or /K is specified, then the remainder of the command line after the switch is processed as a command line, where the following logic is used to process quote (“) characters:

1. If all of the following conditions are met, then quote characters on the command line are preserved:
– no /S switch
– exactly two quote characters
– no special characters between the two quote characters,
where special is one of: &<>()@^
– there are one or more whitespace characters between the
the two quote characters
– the string between the two quote characters is the name
of an executable file.

2. Otherwise, old behavior is to see if the first character is
a quote character and if so, strip the leading character and
remove the last quote character on the command line, preserving
any text after the last quote character.

So it looks to me like the task is calling cmd.exe with the /c option and thus we are seeing the behavior listed in #2 above. This gives us a totally messed up command line to process.

So how do we fix it????

Solution: We follow the old adage “If at first you don’t succeed, give up!” Actually we give up on forcing the MSBuild project to trigger the external app through the task and we build our own Custom Task! to fire this thing off. There a number of benefits to using a custom task for this, including:

  1. We have finer grain control over the running process and can send output to the build log. The task does not have this feature.
  2. We can be more descriptive in our project file as to what we are passing. So instead of seeing
    /f=”C:ProjectsContracts” /l=”\SomeServerfiledropCodeGenLog.xml”
    we can now see

    C:ProjectsContracts
    \SomeServerfiledropCodeGenLog.xml


  3. We can pass data back to the MSBuild engine upon task completion if we want to.

These are really great reasons to build your own custom task. Another reason is that it is really simple to do. There are a number of blogs and sites out there that describe the process. Instead of adding it to this post, I’m giving you a link to steps I used from Bart De Smet’s blog entry on Custom Build Tasks.

Conclusion: The task is great of you need to run a built-in command, such as dir, copy, or even cacls, but it isn’t robust enough to handle a large set of command-line arguments with embedded spaces. If you don’t have the embedded spaces, then you should be fine using . If you need a richer method of running an external process, offering logging, output parameters and descriptive metadata entries; then a custom task is the way to go.