Introduction to TestApi – Part 2: Command-Line Parsing APIs


Series Index

+++

Command-line parsers remind me of linked lists in C++: everybody has written several at various points in their careers. While everybody should write each of those at least once, I doubt that many people out there are particularly excited about writing and re-writing fundamental data structures on a regular basis – it gets old very quickly. Not to mention that doing so is error-prone and decreases the maintainability of a world that’s already hard to maintain.

That’s why modern-day frameworks such as .NET provide standard implementations of the common data structures. And that’s why TestApi provides a reusable command-line parsing APIs via the CommandLineDictionary and CommandLineParser classes, the latter being a type-safe layer on top of the former. Obviously, these are not test APIs per se – they are general utility APIs that happen to be more often used when writing tests.

A few quick examples follow.

Simple Command-Line Parsing

As seen from the first example below, extracting command-line parameters that are primitives is easy. Primitive command-line parameters are either boolean (e.g. the “verbose” flag below), or a key-value pair, that one can extract with the indexer of the CommandLineDictionary instance (see the “testId” key below), just as one would expect from a Dictionary.

// 
// EXAMPLE #1: Parsing a command-line such as "RunTests.exe /verbose /testId=123"
// 

using System;
using System.Linq;
using Microsoft.Test.CommandLineParsing;

public class Program 
{ 
    public static void Main(string[] args) 
    { 
        CommandLineDictionary d = new CommandLineDictionary(args);

        bool verbose = d.ContainsKey("verbose");
        int testId = Int32.Parse(d["testId"]);
                
        // use the parsed command-line parameters
    }
}

By default flags/keys are indicated with the forward slash (“/”) character and values are indicated with the equals character (“=”), but the user can override that upon initialization of of the CommandLineDictionary object:

// 
// EXAMPLE #1b: Parsing a command-line such as "RunTests.exe –verbose –testId:123"
// 

...
        CommandLineDictionary d = new CommandLineDictionary(args, '-', ':');
...

Finally, you one can use the ToString method to get a string representation of the command-line arguments.

 

Command-Line Argument Structures

Another common pattern when dealing with command-line arguments is populating a structure which contains all parsed arguments. The CommandLineParser class makes this easy:

// EXAMPLE #2:
// Sample for parsing the following command-line:
// Test.exe /verbose /runId=10
// This sample declares a class in which the strongly typed arguments are populated
class CommandLineArguments
{
   public bool? Verbose { get; set; }
   public int? RunId { get; set; }
}

...

CommandLineArguments a = new CommandLineArguments();
CommandLineParser.ParseArguments(a, args);

...

 

Type-Safe Commands

A third common approach is forming strongly-typed commands from the command-line parameters. This is common for cases when the command-line looks as follows:

some-exe  COMMAND  parameters-to-the-command

The parsing in this case is a little bit more involved:

  1. Create one class for every supported command, which derives from the Command abstract base class and implements an expected Execute method.
  2. Filter out the COMMAND part of the command-line arguments (note that Skip method used below is an extension method brought in by LINQ, so you’ll need to use System.Linq as shown above). Create an instance of the appropriate Command-derived class, based on the passed COMMAND argument.

  3. Pass the created Command-derived object to along with the rest of the command-line arguments to  CommandLineParser.ParseArguments – the method will initialize appropriately the  strongly-typed Command instance based on the the rest of the command-line.

  4. Execute the initialized Command instance.

// EXAMPLE #3:
// Sample for parsing the following command-line:
// Test.exe run /runId=10 /verbose 
// In this particular case we have an actual command on the command-line (“run”), 
// which we want to effectively de-serialize and execute.
public class RunCommand : Command
{
   public bool? Verbose { get; set; }
   public int? RunId { get; set; }

   public override void Execute()
   {
      // Implement your "run" execution logic here.
   }
}

public class Program
{
   public static void Main(string[] args)
   {
      if (String.Compare(args[0], "run", StringComparison.InvariantCultureIgnoreCase) == 0)
      {
         Command c = new RunCommand();
         c.ParseArguments(args.Skip(1)); 
         // or CommandLineParser.ParseArguments(c, args.Skip(1))
         c.Execute();
      }
   }
}

Besides the parsing logic, CommandLineParser provides a few additional helper methods. One of them is CommandLineParser.PrintCommandUsage, which prints the usage for specific commands (or all supported commands) to the console.

 

In Conclusion

The command-line parsing APIs released with TestApi provide a simple and layered access to the command-line. Strictly speaking these APIs are not test APIs, but have nevertheless been included in TestApi as tests often have a need of parsing parameters on the command-line.

Comments (13)

  1. Last week, the WPF test team released a new version of their Test API: 0.2. You can download the new

  2. trompelecode says:

    Does this library support parsing of something like this: "TestApp.exe –verbose –param1:123 image1.jgp image2.jpg"?

    The idea is to be able to receive an undefined number of additional arguments (file names) that are not commands.

    Thanks!

  3. ivom says:

    @trompelecode:

    No, we don’t support that yet.

    We only support /key=value pairs and /flags

  4. cchaffee says:

    The last example seems incomplete.  Is CommandLineParser.ParseCommand not implemented at this time?  The provided example does not cover it at all and it seems to be missing from the library.

  5. ivom says:

    @cchaffee:

    Actually this is a mistake in the blog post. There is no CommandLineParser.ParseCommand. There is only CommandLineParser.ParseArguments, which takes an object and an args and attempts to use the args to initialize the object (which can be a Command or any other arbitrary class/structure). ParseArguments is an extension method of object, so you can write the following code:

           public class RunCommand : Command

           {

               public bool? Verbose { get; set; }

               public int? RunId { get; set; }

               public override void Execute()

               {

                   // do something

               }

           }

           public class Program

           {

               public static void Main(string[] args)

               {

                   if (String.Compare(args[0], "run", StringComparison.InvariantCultureIgnoreCase) == 0)

                   {

                       Command c = new RunCommand();

                       c.ParseArguments(args.Skip(1));

                       c.Execute();

                   }

               }

           }

    Thanks for catching this mistake! I will go ahead and fix the post and we will update the documents appropriately.

    Ivo

  6. Mike Ramirez says:

    There seems to another mistake, this time with the simple command-line parsing example.

    CommandLineDictionary d = new CommandLineDictionary(args);

    With that I get the error "Error 1 'Microsoft.Test.CommandLineParsing.CommandLineDictionary' does not contain a constructor that takes 1 arguments"

    What works for me is:  

    CommandLineDictionary d = CommandLineDictionary.FromArguments(args);

    Mike

    (sorry if this is a double post had an error in the previous one)

  7. ivom says:

    Mike,

    Thanks for the report. Where do you see this mistake? In the docs somewhere?

    Ivo

  8. out of date says:

    this seems to be out of date.  commandlinedictionary constructor doesn't take any arguments.  It appears there is a new static function called "FromArguments" that does what the constructor used to do.