Custom MSBuild Task and capturing Command Line Output

I recently had a need to “capture the output” of a command line tool, but within a MSBuild Custom Task (context).

While I know there are “msbuild’ish” ways to accomplish the below example (getting a list of directories from a “dir” command), the below is an ~~example~~ of how to capture the output of a command line call.

What am I talking about?

Well, for example, if run the command (from a command line prompt):

dir “c:\”

You would see (in the command line window) something like the below:

C:\>dir “c:\”

Volume in drive C has no label.
Volume Serial Number is JER-33_3

Directory of c:\

10/15/2012 01:20 PM <DIR> Program Files
11/08/2012 02:29 PM <DIR> Program Files (x86)
08/16/2012 02:23 PM <DIR> Users
11/08/2012 02:08 PM <DIR> Windows

0 File(s) 0 bytes
4 Dir(s) 33,490,378,752 bytes free

C:\>

So how can I “capture” the output while inside of a custom MSBuild Task?

First.  I didn’t figure this out on my own.  I googled and bing searched by rumpus off.  And then I came across an example.

https://msbuildextensionpack.svn.codeplex.com/svn/Solutions/Main3.5/Framework/Framework/CommandLine.cs

namespace MSBuild.ExtensionPack.Framework
{
[HelpUrl(“http://www.msbuildextensionpack.com/help/3.5.12.0/html/324b0e31-5ff0-baac-40ae-bf26297e5821.htm&#8221;)]
public class CommandLine : Task
{
//not seen stuff here
}
}

And I totally ripped their code.

But what I did do is package this up in a smaller (more digestable?) example as seen below.
And I remember when I first started with writing my own custom MSBuild tasks that what is obvious to me now, was not obvious when I first started.

So, in a nutshell:
“CollectedOutput” will be the property that has all the contents for the ‘dir “c:\”‘.

The CSharp code will need to be put into a “Class Library” DotNet csproj of course.
If you want the example to run “out of the box”, then name the new csproj with the name “GranadaCoder.Framework.CrossDomain.MSBuild” (If you don’t do this, you’ll need to adjust the name of the .dll mentioned in the .msbuild file).
After compiling, place the .dll in the same folder as the .msbuild and .bat file.

//START CSharp Code//

namespace GranadaCoder.Framework.CrossDomain.MSBuild.Tasks.Temp
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.ComponentModel;
using System.Reflection;

using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using System.Diagnostics;

public class CommandLineDirectoryTask : ToolTask
{

private static readonly string WINDIR_ENVIRONMENT_VARIABLE = “windir”;
private static readonly string CMD_RUNTIME_EXE = “cmd.exe”;
private static readonly string COMMAND_LINE_SWITCH_C_DIR = “/c dir”;

public CommandLineDirectoryTask()
{
base.ToolExe = CMD_RUNTIME_EXE; // see http://msdn.microsoft.com/en-us/library/microsoft.build.utilities.tooltask.toolexe.aspx
this.ToolPath = this.ReadSystem32EnvironmentVariable();
}

[Required]
public string DirectoryNameToList { get; set; }

/// <summary>
/// Gets or sets the collected output from the command-line.
/// </summary>
///
[Output]
public string CollectedOutput { get; set; }

/// <summary>
/// Gets or sets the StdErr stream encoding. Specifies the encoding of the captured task standard error stream.
/// The default is the current console output encoding.
/// </summary>
/// <remarks>Exec Equivalent: StdErrEncoding</remarks>
[Output]
public string StdErrEncoding { get; set; }

/// <summary>
/// Gets or sets the StdOut stream encoding. Specifies the encoding of the captured task standard output stream.
/// The default is the current console output encoding.
/// </summary>
/// <remarks>Exec Equivalent: StdOutEncoding</remarks>
[Output]
public string StdOutEncoding { get; set; }

/// <summary>
/// Gets the name of the tool.
/// </summary>
/// <value>The name of the tool.</value>
protected override string ToolName
{
get { return base.ToolExe; }
}

/// <summary>
/// Generates the full path to tool.
/// </summary>
/// <returns></returns>
protected override string GenerateFullPathToTool()
{
return Path.Combine(ToolPath, ToolName);
}

/// <summary>
/// Executes this instance.
/// </summary>
/// <returns></returns>
public override bool Execute()
{
//return base.Execute();
return InternalExecute();
}

private bool InternalExecute()
{

string command = this.GenerateFullPathToTool();
string commandLineArguments = GenerateSvnExportCommandLineArguments();

this.Log.LogMessage(“Execute: {0} {1}”, command, commandLineArguments);
ProcessStartInfo startInfo = this.GetCommandLine(command, commandLineArguments, this.DirectoryNameToList, this.StdErrEncoding, this.StdOutEncoding);
using (Process process = Process.Start(startInfo))
{
//this.Log.LogMessage(“Collect Standard Output Stream”);

while (!process.StandardOutput.EndOfStream || !process.HasExited)
{
this.CollectOutputLine(process.StandardOutput.ReadLine());
}

//this.Log.LogMessage(“Collect Standard Error Stream”);
while (!process.StandardError.EndOfStream)
{
this.CollectOutputLine(process.StandardError.ReadLine());
}

}

return true;

}

/// <summary>
/// Gets a command process object with the command specified.
/// </summary>
/// <param name=”command”>The command to execute</param>
/// <param name=”workingDirectory”>The command working directory</param>
/// <param name=”standardErrorEncoding”>The standard error stream encoding</param>
/// <param name=”standardOutputEncoding”>The standard output stream encoding</param>
/// <returns>Returns a command prompt start information that is ready to start</returns>
/// <remarks>StdErr and StdOut are always redirected.</remarks>
private ProcessStartInfo GetCommandLine(string command, string commandLineArguments, string workingDirectory, string standardErrorEncoding, string standardOutputEncoding)
{
var process = new ProcessStartInfo
{
FileName = command,
Arguments = commandLineArguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
ErrorDialog = false
};

if (!string.IsNullOrEmpty(standardErrorEncoding))
{
try
{
process.StandardErrorEncoding = Encoding.GetEncoding(standardErrorEncoding);
}
catch (ArgumentException ex)
{
this.Log.LogMessage(“Non-fatal exception caught: invalid encoding specified for standard error stream: {0}”, standardErrorEncoding);
this.Log.LogWarningFromException(ex);
}
}

if (!string.IsNullOrEmpty(standardOutputEncoding))
{
try
{
process.StandardOutputEncoding = Encoding.GetEncoding(standardOutputEncoding);
}
catch (ArgumentException ex)
{
this.Log.LogMessage(“Non-fatal exception caught: invalid encoding specified for standard output stream: {0}”, standardOutputEncoding);
this.Log.LogWarningFromException(ex);
}
}

if (!string.IsNullOrEmpty(workingDirectory))
{
if (Directory.Exists(workingDirectory))
{
process.WorkingDirectory = workingDirectory;
}
else
{
this.Log.LogWarning(“Non-fatal input error: provided working directory does not exist: {0}”, workingDirectory);
}
}

return process;
}

/// <summary>
/// Generates the command line commands.
/// </summary>
/// <returns></returns>
protected string GenerateSvnExportCommandLineArguments()
{
StringBuilder builder = new StringBuilder();
AppendIfPresent(builder, CommandLineDirectoryTask.COMMAND_LINE_SWITCH_C_DIR, QuoteItUp(this.DirectoryNameToList));
Log.LogCommandLine(builder.ToString());
return builder.ToString();
}

/// <summary>
/// Collects a line of output.
/// </summary>
/// <param name=”text”>The text line</param>
private string CollectOutputLine(string text)
{
text = string.IsNullOrEmpty(text) ? null : text.Trim();
if (!string.IsNullOrEmpty(text))
{
this.CollectedOutput += Environment.NewLine + text;
this.Log.LogMessage(text);
}

return text;
}

/// <summary>
/// Append the command and argument only if the argument exists. Repped from the internet at http://www.zorched.net/2009/01/08/msbuild-task-for-partcover/
/// </summary>
/// <param name=”builder”>The containing string builder.</param>
/// <param name=”cmdArg”>The command argument switch.</param>
/// <param name=”value”>The value of the command argument.</param>
protected static void AppendIfPresent(StringBuilder builder, string cmdArg, string value)
{
if (!String.IsNullOrEmpty(value))
{
builder.AppendFormat(“{0} {1} “, cmdArg, value);
}
}

/// <summary>
/// Wrap quotes around a string and escape charcters if needed.
/// </summary>
/// <param name=”builder”>The containing string builder.</param>
protected static string QuoteItUp(string args)
{
if (String.IsNullOrEmpty(args))
{
return args;
}

bool alreadyHasQuoteBookEnds = false;

if (args.StartsWith(“\””) && args.EndsWith(“\””))
{
alreadyHasQuoteBookEnds = true;
}

// Escape internal quotes if any
if (args.Contains(“\””))
{
args = args.Replace(“\””, “\\\””);
}

if (!alreadyHasQuoteBookEnds)
{
// quote string
args = String.Format(“\”{0}\””, args);
}
return args;
}

private string ReadSystem32EnvironmentVariable()
{
string returnValue = string.Empty;

if (!String.IsNullOrEmpty(System.Environment.GetEnvironmentVariable(WINDIR_ENVIRONMENT_VARIABLE)))
{
returnValue = System.Environment.GetEnvironmentVariable(WINDIR_ENVIRONMENT_VARIABLE);
returnValue = Path.Combine(returnValue, “System32”);
}
else
{
base.Log.LogWarning(string.Format(“The ‘{0}’ Environment Variable was not available. You will need to manually set the ToolPath value”, WINDIR_ENVIRONMENT_VARIABLE));
}

return returnValue;
}
}
}

//END CSHARP Code

—————————————

//Start file “CommandLineDirectoryTaskTest.msbuild”

<?xml version=”1.0″ encoding=”utf-8″?>
<Project DefaultTargets=”AllTargetsWrapper” xmlns=”http://schemas.microsoft.com/developer/msbuild/2003″&gt;
<!– –>
<PropertyGroup>
<WorkingDirectory>.</WorkingDirectory>
</PropertyGroup>
<!– –>
<PropertyGroup>
<GranadaCoderMSBuildFileName Condition=”Exists(‘$(WorkingDirectory)\MSBuildHelpers\GranadaCoder.Framework.CrossDomain.MSBuild.dll’)”>$(WorkingDirectory)\MSBuildHelpers\GranadaCoder.Framework.CrossDomain.MSBuild.dll</GranadaCoderMSBuildFileName>
<GranadaCoderMSBuildFileName Condition=”Exists(‘$(WorkingDirectory)\GranadaCoder.Framework.CrossDomain.MSBuild.dll’)”>$(WorkingDirectory)\GranadaCoder.Framework.CrossDomain.MSBuild.dll</GranadaCoderMSBuildFileName>
<GranadaCoderMSBuildFileName Condition=”$(GranadaCoderMSBuildFileName)==””>CannotFind_GranadaCoder.Framework.CrossDomain.MSBuild.dll</GranadaCoderMSBuildFileName>
</PropertyGroup>
<!– –>
<!– –>
<UsingTask AssemblyFile=”$(GranadaCoderMSBuildFileName)” TaskName=”CommandLineDirectoryTask”/>
<!– –>
<!– –>
<Target Name=”AllTargetsWrapper”>
<CallTarget Targets=”CommandLineDirectoryTask1″ />
</Target>
<!– –>
<!– –>
<Target Name=”CommandLineDirectoryTask1″>

<CommandLineDirectoryTask DirectoryNameToList=”C:\wutemp\”>
<Output TaskParameter=”CollectedOutput” PropertyName=”MyCollectedOutput1″/>
</CommandLineDirectoryTask>

<Message Text=”The CollectedOutput1 is $(MyCollectedOutput1)”/>
<Message Text=” “/>
<Message Text=” “/>
<Message Text=” “/>
<Message Text=” “/>

<CommandLineDirectoryTask ContinueOnError=”true” DirectoryNameToList=”C:\work3\” >
<Output TaskParameter=”ExitCode” PropertyName=”MyErrorCode”/>
<Output TaskParameter=”CollectedOutput” PropertyName=”MyCollectedOutput2″/>
</CommandLineDirectoryTask>

<Message Text=”The CollectedOutput2 is $(MyCollectedOutput1)”/>
<Message Text=” “/>
<Message Text=” “/>
<Message Text=”The exit code (MyErrorCode) is $(MyErrorCode)”/>

</Target>

</Project>

//End file “CommandLineDirectoryTaskTest.msbuild”

—————————————

//Start CommandLineDirectoryTaskTest.bat file
set msBuildDir=%WINDIR%\Microsoft.NET\Framework\v3.5
set msBuildDir=%WINDIR%\Microsoft.NET\Framework\v2.0.50727
set msBuildDir=%WINDIR%\Microsoft.NET\Framework\v3.5

call %msBuildDir%\msbuild /target:AllTargetsWrapper “CommandLineDirectoryTaskTest.msbuild” /p:Configuration=Release;FavoriteFood=Popeyes /l:FileLogger,Microsoft.Build.Engine;logfile=CommandLineDirectoryTaskTest_AllTargetsWrapped.log

set msBuildDir=

//End CommandLineDirectoryTaskTest.bat file

Advertisements
This entry was posted in Software Development. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s