Bill Tudor

Weblog

  Home  |   Contact  |   Syndication    |   Login
  49 Posts | 0 Stories | 95 Comments | 0 Trackbacks

News

Copyright © Bill Tudor

Archives

Post Categories

The Process class in .Net makes it easy to run command line tools from within a .Net program. Here is an implementation with a couple of twists:

  • Environment variable expansion
  • Optional asynchronous execution
  • Transformation of output into an enumerable collection of objects

The implementation takes the form of an object named CommandLineTool with the following constructors:

public CommandLineTool(string command)
: this(command, String.Empty, Environment.CurrentDirectory, _defaultTransform) { }
public CommandLineTool(string command, string arguments)
: this(command, arguments, Environment.CurrentDirectory, _defaultTransform) { }
public CommandLineTool(string command, string arguments, string workingDirectory)
: this(command, arguments, workingDirectory, _defaultTransform) { }
public CommandLineTool(string command, string arguments, string workingDirectory, Func<string, object> transform)
{
this.Command = Environment.ExpandEnvironmentVariables(command);
this.Arguments = Environment.ExpandEnvironmentVariables(arguments);
this.WorkingDirectory = Environment.ExpandEnvironmentVariables(workingDirectory);
this.Transform = transform;
_isBusy = false;
}

The simplest form provides for an executable filename only, while the last constructor accepts arguments, working directory, and transformation function for the output lines. Note that environment variables, such as "%ComSpec%, may be included in any of the first three arguments.

Output Transformation

Each line of output (strings) can be sent to a transformation function, defined as a Func<string, object>, and returned as an enumerable collection of objects (IEnumerable<Object>) accessed via the OutputObjects property. Output can also be expressed as a single string (Output) or an array of lines of text (OutputLines). These are exposed as properties. The transformation function is free to return null to exclude an item from the OutputObjects collection.

   1: /// <summary>
   2: /// Command output.
   3: /// </summary>
   4: public string Output { get; private set; }
   5: /// <summary>
   6: /// Command output lines.
   7: /// </summary>
   8: public string [] OutputLines
   9: {
  10:     get
  11:     {
  12:         if (Output != null)
  13:         {
  14:             return Output.Split(_seperators, StringSplitOptions.RemoveEmptyEntries);
  15:         }
  16:         return null;
  17:     }
  18: }
  19: /// <summary>
  20: /// Output Objects.
  21: /// </summary>
  22: /// <remarks>
  23: /// Output lines are converted to output objects using the transform
  24: /// function. The default transform functions results in an enumeration
  25: /// of the out lines (strings).
  26: /// </remarks>
  27: public IEnumerable<Object> OutputObjects
  28: {
  29:     get
  30:     {
  31:         if (Transform != null)
  32:         {
  33:             return OutputLines.Select(Transform).Where(o => o != null);
  34:         }
  35:         return null;
  36:     }
  37: }    

Asynchronous and Synchronous Operation

Both asynchronous and synchronous modes of operation are provided. An event (ExecuteCompleted) is raised following the completion of an asynchronous execution. Overloads are provided for both the Execute() and ExecuteAsync() methods to allow for an alternate transformation function to be applied in successive calls. Only one asynchronous operation at a time is allowed. The asynchronous version simply calls on the synchronous version using a background thread from the thread pool. Error output is provided in a similar manner to standard output, except that there is no option for transformation of lines of text on the error output to objects.

   1: /// <summary>
   2: /// Execute the command.
   3: /// </summary>
   4: /// <param name="transform">Transform to apply to the output.</param>
   5: /// <returns>Command result.</returns>
   6: public int Execute(Func<string, object> transform)
   7: {
   8:     this.Transform = transform;
   9:     return Execute();
  10: }
  11: /// <summary>
  12: /// Executes the command.
  13: /// </summary>
  14: /// <returns>Command result.</returns>
  15: public int Execute()
  16: {
  17:     ProcessStartInfo info = new ProcessStartInfo();
  18:     info.FileName = this.Command;
  19:     info.Arguments = this.Arguments;
  20:     info.WorkingDirectory = this.WorkingDirectory;
  21:     info.CreateNoWindow = true;
  22:     info.UseShellExecute = false;
  23:     info.RedirectStandardError = true;
  24:     info.RedirectStandardOutput = true;
  25:     Process p = Process.Start(info);
  26:     Output = p.StandardOutput.ReadToEnd();
  27:     ErrorOutput = p.StandardError.ReadToEnd();
  28:     return p.ExitCode;
  29: }
  30: /// <summary>
  31: /// Execute the command asynchronously.
  32: /// </summary>
  33: /// <param name="transform">Transform to apply to the output.</param>
  34: public void ExecuteAsync(Func<string, object> transform)
  35: {
  36:     this.Transform = transform;
  37:     ExecuteAsync();
  38: }
  39: /// <summary>
  40: /// Execute the command asynchronously.
  41: /// </summary>
  42: public void ExecuteAsync()
  43: {
  44:     lock (this)
  45:     {
  46:         if (!_isBusy)
  47:         {
  48:             _isBusy = true;
  49:             ThreadPool.QueueUserWorkItem((WaitCallback)((state) =>
  50:             {
  51:                 int result = Execute();
  52:                 if (ExecuteCompleted != null)
  53:                 {
  54:                     ExecuteCompleted(this, EventArgs.Empty);
  55:                 }
  56:                 lock (this)
  57:                 {
  58:                     _isBusy = false;
  59:                 }
  60:             }));
  61:         }
  62:         else
  63:         {
  64:             throw new InvalidOperationException("Async operation in progress.");
  65:         }
  66:     }
  67: }

Using the Code

Using the code is fairly easy. Just create a new CommandLineTool object and call either the Execute() or ExecuteAsync() methods. Here is a snippet from one of the unit tests:

   1: string command = "cmd.exe";
   2: string arguments = "/c dir";
   3: CommandLineTool target = new CommandLineTool(command, arguments);
   4: int expected = 0;
   5: int actual;
   6: actual = target.Execute();
   7: Assert.AreEqual(expected, actual);

A more involved example (somewhat contrived) includes transformation of the results and asynchronous operations. [Results are transformed into FileObject objects].

   1: // The FileObject class ...
   2: internal class FileObject
   3: {
   4:     public DateTime ModifiedDate { get; set; }
   5:     public string Name { get; set; }
   6: }
   7:  
   8: // Code ...
   9:  
  10:     string command = "cmd.exe";
  11:     string arguments = "/c dir /s";
  12:     Func<string, object> transform = new Func<string, object>(s =>
  13:         {
  14:             DateTime modified = DateTime.MaxValue;
  15:             if (s.Length > 39 && DateTime.TryParse(s.Substring(0, 20), out modified))
  16:             {
  17:                 return new FileObject()
  18:                 {
  19:                     ModifiedDate = modified,
  20:                     Name = s.Substring(39)
  21:                 };
  22:             }
  23:             return null;
  24:         });
  25:     CommandLineTool target = new CommandLineTool(command, arguments);
  26:     target.ExecuteAsync(transform);
  27:     while (target.IsBusy) Thread.Sleep(0);
  28:  
  29:     List<FileObject> files = target.OutputObjects.Cast<FileObject>().ToList();
  30:  
  31:     ... etc ... 

Hope this comes in handy for someone. Let me know if you see any errors/problems or room for improvement.

  • Share This Post:
  • Share on Twitter
  • Share on Facebook
  • Share on Technorati
posted on Tuesday, September 22, 2009 4:36 AM