Transforming Tree Surgeon using the Adaptive Console Framework

I'm a command line app junkie as I love working with it. I guess it's my DOS/Linux roots but I find things go faster when you're not dealing with a GUI and a mouse. Command line tools like NAnt and MSBuild have all sorts of options and syntax. Some of it discoverable, some of it not so much. NAnt for example will try to find a buildfile to run and execute it. It also will display the name and version of the app (which is useful in build logs so you know what's going on). There are other things like trying to find out how to run a command line tool. For example if you type "nant /?" you'll get this:

NAnt 0.86 (Build 0.86.3075.0; nightly; 02/06/2008)
Copyright (C) 2001-2008 Gerry Shaw
http://nant.sourceforge.net

Unknown argument '/?'

Try 'nant -help' for more information

Entering the proper syntax of "nant -help" displays this:

NAnt 0.86 (Build 0.86.3075.0; nightly; 02/06/2008)
Copyright (C) 2001-2008 Gerry Shaw
http://nant.sourceforge.net

NAnt comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it under certain
conditions set out by the GNU General Public License.  A copy of the license
is available in the distribution package and from the NAnt web site.

Usage : NAnt [options] <target> <target> ...
Options :

  -t[argetframework]:<text>      Specifies the framework to target
  -defaultframework:<text>       Specifies the framework to target (Short format: /k)
  -buildfile:<text>              Use given buildfile (Short format: /f)
  -v[erbose][+|-]                Displays more information during build process
  -debug[+|-]                    Displays debug information during build process

  -q[uiet][+|-]                  Displays only error or warning messages during
build process
  -e[macs][+|-]                  Produce logging information without adornments
  -find[+|-]                     Search parent directories for build file
  -indent:<number>               Indentation level of build output
  -D:<name>=<value>              Use value for given property
  -logger:<text>                 Use given type as logger
  -l[ogfile]:<filename>          Use value as name of log output file
  -listener:<text>               Add an instance of class as a project listener
  -ext[ension]:<text>            Load NAnt extensions from the specified assembly
  -projecthelp[+|-]              Prints project help information
  -nologo[+|-]                   Suppresses display of the logo banner
  -h[elp][+|-]                   Prints this message
  @<file>                        Insert command-line settings from a text file.

A file ending in .build will be used if no buildfile is specified.

A lot of options there but pretty standard fare for a console application. And a lot of work to parse the options, validate them, display help messages, etc. I had a link to this thing called the Adaptive Console Framework sitting in my Action folder in Outlook and finally got around to looking at it. It's a library by Sunny Chen that takes the pain of command line junk by doing most of the heavy lifiting for you.

This is what the console framework provides for you. A nice, simple way of not having to write a lot of code to deal with complex command line options and something that gives you a few other benefits along the way like automatic help text generation and easy access to command line options. Notice that the syntax for displaying NAnt help was "nant -help" but it wouldn't allow variations like "nant /?" or "nant -?". The framework as we'll see let's us make it easy to just add variations to command line syntax without doing a lot of work.

The framework is a little gem of a library that I didn't think much about before but now after spending an entire hour of my hard earned time I think it's pretty slick. Here's a transformation of the console version of Tree Surgeon.

The old version of the Tree Surgeon console (betcha didn't even think there was one!) was a little boring and actually broken. If you ran it without any arguments you got this:

TreeSurgeon version 1.1
Copyright (C) 2007 - 2008 Bil Simser
Copyright (C) 2005 - 2006 Mike Roberts, ThoughtWorks, Inc

Creates a .NET Development tree

TreeSurgeon projectName

Please note - project name must not contain spaces. We recommend you use CamelCase for project names.

You could probably surmise you need to provide a project name at least. But what about those other options like version and what unit test framework to use? And frankly this is wrong since it's not version 1.1, this output was from the 2.0 version. Lots of little problems here.

Here's the source for the command line runner:

[STAThread]

private static int Main(string[] args)

{

    try

    {

        return RunApp(args);

    }

    catch (Exception e)

    {

        Console.WriteLine("Unhandled Exception thrown. Details follow: ");

        Console.WriteLine(e.Message);

        Console.WriteLine(e.StackTrace);

        return -1;

    }

}

And here's the RunApp method:

private static int RunApp(string[] args)

{

    Console.WriteLine("TreeSurgeon version 1.1");

    Console.WriteLine("Copyright (C) 2007 - 2008 Bil Simser");

    Console.WriteLine("Copyright (C) 2005 - 2006 Mike Roberts, ThoughtWorks, Inc");

    Console.WriteLine();

    if (args.Length != 2)

    {

        Usage();

        return -1;

    }

    Console.WriteLine("Starting Tree Generation for " + args[0]);

    Console.WriteLine();

    string outputDirectory = new TreeSurgeonFrontEnd(

        Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), args[1]).

        GenerateDevelopmentTree(args[0], "NUnit");

    Console.WriteLine("Tree Generation complete. Files can be found at " + outputDirectory);

    return 0;

}

RunApp would output the logo and copyright info and give you the ugly Usage() message (which wasn't too useful) if you didn't pass in at least a project name. You could pass in a version to build (2003, 2005, or 2008) but the unit test framework was hard coded to NUnit. Like I said, not too useful.

After taking a quick glance at what the Adaptive Console Framework (ACF) could do I decided to transform the Tree Surgeon console runner using it and see what we could get.

The ACF basically has two steps to it (this is overly simplifying it but you'll see it's pretty easy). First you make a slight adjustment to your main console application method, then you get down and dirty by creating option contracts (via attributes, classes, and properties). This has a big bonus that I immediately saw which was to move the command line options into a separate assembly and class which meant I could test it without actualy having to run the application and secondly it would take care of most of the heavy lifting of dealing with command line syntax.

So the first thing I did was to ditch that RunApp method and replace the call to have the ConsoleApplicationManager class from the ACF do my work. Here's the updated Main method from the Tree Surgeon console app:

[STAThread]

private static void Main(string[] args)

{

    try

    {

        ConsoleApplicationManager.RunApplication(args);

    }

    catch (Exception e)

    {

        Console.WriteLine("Unhandled Exception thrown. Details follow:");

        Console.WriteLine(e.Message);

        Console.WriteLine(e.StackTrace);

    }

}

Next I created a new assembly (called TreeSurgeonConsoleApplication.dll) and added an app.config file to the console app so the ACF could find my option contracts and added a reference to the ACF assembly. Here's the newly added app.config file:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

  <configSections>

    <section name="AdaptiveConsole"

             type="AdaptiveConsole.Config.AdaptiveConsoleConfigHandler, AdaptiveConsole"/>

  </configSections>

  <AdaptiveConsole provider="TreeSurgeonConsoleApplication.TreeSurgeon, TreeSurgeonConsoleApplication"

                   contractRepository="TreeSurgeonConsoleApplication"/>

</configuration>

The app.config file just tells the ACF two things. The name and location of my console provider and the assembly where to find the option contracts. That was all I had to do in my TreeSurgeonConsole project so after removing the reference to the Core project (where the actual Tree Generation would happen) I closed down the console app project. Thinking about it, with the app.config file you could really use a generic console application project for *any* console app since there's nothing specific in here anymore. Nice.

The console provider is a class derived from ConsoleApplicationBase in the ACF and has two string overrides you provide, a logo and a description. Here's the TreeSurgeon class that we just specified in our app.config file:

public class TreeSurgeon : ConsoleApplicationBase

{

    public TreeSurgeon(string[] args) : base(args)

    {

    }

 

    protected override string Logo

    {

        get

        {

            var sb = new StringBuilder();

            sb.AppendFormat("TreeSurgeon version 2.0{0}", Environment.NewLine);

            sb.AppendFormat("Copyright (C) 2007 - 2008 Bil Simser{0}", Environment.NewLine);

            sb.Append("Copyright (C) 2005 - 2006 Mike Roberts, ThoughtWorks, Inc.");

            return sb.ToString();

        }

    }

 

    protected override string Description

    {

        get { return "Creates a .NET development tree"; }

    }

}

We're emulating part of the old RunApp method here. When I run the console app now I get this:

TreeSurgeon version 2.0
Copyright (C) 2007 - 2008 Bil Simser
Copyright (C) 2005 - 2006 Mike Roberts, ThoughtWorks, Inc.

Creates a .NET development tree

Looks pretty much the same however like I said, I can now test the TreeSurgeon class (for example make sure the logo is set correctly because I might decide down the road to make the property a little more dynamic like fetching values using Reflection). I'm also not actually running anything yet so if I was building my app using TDD this fits nicely with that approach.

That's it for this part of the conversion but like I said, I don't have it running my TreeSurgeonFrontEnd class yet or generating the development tree or verifying the command line or displaying help. That now comes with our options.

With the ACF you define your command line options through something called "Option Contracts". There are four types in the ACF: None, Exact, Patternized, and Free. For Tree Surgeon I want the user to be able to run the application using these options:

  • Display help if the user enters nothing
  • Provide the name of the project to generate (required)
  • Provide an optional version of the system to generate (with a default)
  • Provide an optional unit test framework to use (with a default)

We'll only look at the None Contract and the Patternized contract types.

The None Contract is a class that you inherit from OptionContractBase. It will be executed if the user provides no command line arguments to the application. Create a class that derives from OptionContractBase in your contract assembly. Here's the None contract for Tree Surgeon:

[OptionContract(

    Type = ContractType.None,

    Description = "Prints the help information on the screen.")]

public class TreeSurgeonEmptyContract : OptionContractBase

{

    public override void Execute(

        ConsoleApplicationBase consoleApplication,

        IList<ArgumentInfo> args)

    {

        consoleApplication.PrintHelpMessage();

    }

}

The class is decorated with an OptionContractAttribute that let's you specify the type of contract (None, Exact, Free, Patternized) and a description. Note we haven't done anything anywhere else in the system (the app.config file is done, the console Main method is done, and the ConsoleApplicationBase class is baked). All we're doing is adding a new class to the assembly we specified as our contractRepository in our app.config file.

Here's the output of the app now when no arguments are passed to it:

TreeSurgeon version 2.0
Copyright (C) 2007 - 2008 Bil Simser
Copyright (C) 2005 - 2006 Mike Roberts, ThoughtWorks, Inc.

Creates a .NET development tree


> Calling the application without arguments
  Prints the help information on the screen.

Sweet. Now let's start adding our options for actually running the app.

We'll add a new class called TreeSurgeonCommandsContract (again derived from OptionContractBase). This time rather than specifying the type as "None" we'll use "Patternized". The Patternized type is a contract type where your console application requires a complex command line argument. You can define the options that are mandatory or not within the contract, you can define the options that carry a list of values and you can even define the switches in the patternized contracts. here's our TreeSurgeonCommandsContract class:

[OptionContract(

    Type = ContractType.Patternized,

    Description = "Generates a new .NET development tree for a given project name.")]

public class TreeSurgeonCommandsContract : OptionContractBase

The main thing we need to capture is the project name that we want to generate the tree for. We'll do this by creating a property (called ProjectName) and decorating it with the OptionAttribute:

[Option(

    Type = OptionType.SingleValue,

    Name = "/p;/project",

    Required = true,

    Description = "Specifies the project name.\r\n\t" +

                  "Please note - project name must not contain spaces.\r\n\t" +

                  "We recommend you use CamelCase for project names.")]

public string ProjectName { get; set; }

This tells the ACF that a) this option has a single value b) it's specified by either "/p:" or "/project:" and c) it's required. There's also a description we provide which will be displayed in our output that looks like this now:

TreeSurgeon version 2.0
Copyright (C) 2007 - 2008 Bil Simser
Copyright (C) 2005 - 2006 Mike Roberts, ThoughtWorks, Inc.

Creates a .NET development tree

TreeSurgeonConsole.exe </p|/project:>

> Calling the application without arguments
  Prints the help information on the screen.

> Generates a new .NET development tree for a given project name.
  /p|/project:value (required):
        Specifies the project name.
        Please note - project name must not contain spaces.
        We recommend you use CamelCase for project names.

Notice that we now have the application name (TreeSurgeonConsole.exe) along with a required property. And the help is displayed for that property. Again, pretty damn simple so far. At this point we could actually implement the required Execute method on the TreeSurgeonCommandsContract class and call out to our TreeSurgeonFrontEnd, passing it the ProjectName property. We would generate a developement tree just like the original system and we're done. However we're only about 20 minutes into our conversion so we can do a lot more.

First we'll add a property to specify the version of the development tree we want to generate. This is again just a string property in our TreeSurgeonCommandsContract class decorated with the OptionAttribute. We'll make this optional and provide a default value for it along with instructions:

[Option(

    Type = OptionType.SingleValue,

    Name = "/v;/version",

    Required = false,

    Default = "2008",

    Description = "Specifies the Visual Studio version to generate.\r\n\t" +

                  "Valid options are: \"2003\", \"2005\", or \"2008\"\r\n\t" +

                  "Default is \"2008\"")]

public string Version { get; set; }

Then we'll do the same for our UnitTestFramework we want to specify (NUnit or MbUnit):

[Option(

    Type = OptionType.SingleValue,

    Name = "/t;/test",

    Required = false,

    Default = "NUnit",

    CaseSensitive = true,

    Description = "Specifies the Unit Test framework to use when generating the tree.\r\n\t" +

          "Valid options are: \"NUnit\", or \"MbUnit\"\r\n\t" +

          "Default is \"NUnit\"")]

public string UnitTestFramework { get; set; }

Now we can run our app and see the help the ACF is providing:

TreeSurgeon version 2.0
Copyright (C) 2007 - 2008 Bil Simser
Copyright (C) 2005 - 2006 Mike Roberts, ThoughtWorks, Inc.

Creates a .NET development tree

TreeSurgeonConsole.exe </p|/project:> [/v|/version:] [/t|/test:]

> Calling the application without arguments
  Prints the help information on the screen.

> Generates a new .NET development tree for a given project name.
  /p|/project:value (required):
        Specifies the project name.
        Please note - project name must not contain spaces.
        We recommend you use CamelCase for project names.

  /v|/version:value :
        Specifies the Visual Studio version to generate.
        Valid options are: "2003", "2005", or "2008"
        Default is "2008"

  /t|/test:value :
        Specifies the Unit Test framework to use when generating the tree.
        Valid options are: "NUnit", or "MbUnit"
        Default is "NUnit"

Lots of great stuff here and all we've done was specify some attributes around a few properties. What I really like are a few things we got for free:

  • Our required parameters are specified here and included in the help message
  • Optional parameters are surrounded by "[xxx]" in our command line syntax display
  • We're able to add varations to our command line options ("/t" or "/test") just by specifying the values in the OptionAttribute

Now we'll actually implement the code to run our generator and use whatever values you pass along in the command line.

To get the framework to do our bidding, we implement the Execute method in our TreeSurgeonCommandsContract class. This method passes in a copy of the ConsoleApplicationBase class (we specified above as TreeSurgeon) and an IList of ArgumentInfo values which were passed into the application. This is more than just a string so we can get information from our arguments like what type of argument they are.

For Tree Surgeon, we need at least one option (the project name). We'll use a little LINQ to get the list of options from our passed in parameter and check to make sure that a) we have at least 1 option and b) we have a project name:

var options = from arg in args

            where arg.Type == ArgumentType.Option

            select arg;

 

if(options.Count() < 1 || string.IsNullOrEmpty(ProjectName))

{

    consoleApplication.PrintHelpMessage();

    return;

}

Now that we've got a valid command line we'll reproduce what our old RunApp method did, namely invoke the TreeSurgeonFrontEnd class which will generate our development tree for us. We'll make it a little more interesting than version 1.1 and print out a little more information on what options we're using to generate the tree. Here's our Execute method so far:

public override void Execute(ConsoleApplicationBase consoleApplication, IList<ArgumentInfo> args)

{

    var options = from arg in args

                where arg.Type == ArgumentType.Option

                select arg;

 

    if(options.Count() < 1 || string.IsNullOrEmpty(ProjectName))

    {

        consoleApplication.PrintHelpMessage();

        return;

    }

 

    consoleApplication.PrintLogo();

 

    Console.WriteLine("Starting Tree Generation{0}", Environment.NewLine);

 

    Console.WriteLine("       Project Name: \"{0}\"", ProjectName);

    Console.WriteLine("            Version: \"{0}\"", Version);

    Console.WriteLine("Unit Test Framework: \"{0}\"", UnitTestFramework);

 

    Console.WriteLine();

 

    var frontEnd = new TreeSurgeonFrontEnd(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), Version);

    var outputDirectory = frontEnd.GenerateDevelopmentTree(ProjectName, UnitTestFramework);

    Console.WriteLine("Tree Generation complete.{0}{0}Files can be found at:{0}\"{1}\"", Environment.NewLine, outputDirectory);

}

And here's the output using the command line "treesurgeonconsole.exe /p:test":

TreeSurgeon version 2.0
Copyright (C) 2007 - 2008 Bil Simser
Copyright (C) 2005 - 2006 Mike Roberts, ThoughtWorks, Inc.

Creates a .NET development tree

Starting Tree Generation

       Project Name: "test"
            Version: "2008"
Unit Test Framework: "NUnit"

Tree Generation complete.

Files can be found at:
"C:\Documents and Settings\simserb\My Documents\TreeSurgeon\test"

Wait! We're only 45 minutes into our conversion and there's more features we can take on. Most apps let you turn off the silly logo/copyright info (usually with a "/nologo" switch). The ACF has a nice feature to specify switches on properties. You just add a boolean property to your class and decorate accordingly. Here's our "/nologo" switch:

[Option(

    Type = OptionType.Switch,

    Name = "/nologo",

    Description = "When turned on, the logo and description\r\n\t" +

                  "information will not be displayed.")]

public bool NoLogo { get; set; }

Now that we have a bool property if the user adds "/nologo" to the command line we should not print out the header info:

if(!NoLogo)

{

    consoleApplication.PrintLogo();

}

Finally one last thing before we're done. A bug in the old system was that if you tried to generate a new tree over top of an existing directory, it would bomb out with something like this:

TreeSurgeon version 1.1
Copyright (C) 2007 - 2008 Bil Simser
Copyright (C) 2005 - 2006 Mike Roberts, ThoughtWorks, Inc

Starting Tree Generation for test

Unhandled Exception thrown. Details follow:
Can't generate directory [C:\Documents and Settings\simserb\My Documents\TreeSurgeon\test] since it already exists on disk. Wait until a later version, or delete the existing directory!
   at ThoughtWorks.TreeSurgeon.Core.SimpleDirectoryBuilder.CreateDirectory(String directoryName) in C:\Development\TreeSurgeon-2_0_0_0.source\src\Core\SimpleDirectoryBuilder.cs:line 12
   at ThoughtWorks.TreeSurgeon.Core.TreeSurgeonFrontEnd.GenerateDevelopmentTree(String projectName, String unitTestName) in C:\Development\TreeSurgeon-2_0_0_0.source\src\Core\TreeSurgeonFrontEnd.cs:line 42
   at ThoughtWorks.TreeSurgeon.TreeSurgeonConsole.TreeSurgeonConsoleMain.RunApp(String[] args) in C:\Development\TreeSurgeon-2_0_0_0.source\src\TreeSurgeonConsole\TreeSurgeonConsoleMain.cs:line 44
   at ThoughtWorks.TreeSurgeon.TreeSurgeonConsole.TreeSurgeonConsoleMain.Main(String[] args) in C:\Development\TreeSurgeon-2_0_0_0.source\src\TreeSurgeonConsole\TreeSurgeonConsoleMain.cs:line 15

Highly useful. Let's add a new feature to our command line, an "/overwrite" swtich. It'll be just like the "/nologo" switch except that if it's specified, we'll delete the directory before we generate the tree:

[Option(

    Type = OptionType.Switch,

    Name = "/overwrite",

    Description = "When turned on, any project with the same name\r\n\t" +

          "will be deleted.")]

public bool Overwrite { get; set; }

And here's the updated tree generation code with the check to see if we should delete the output directory first: 

var frontEnd = new TreeSurgeonFrontEnd(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), Version);

 

if (Overwrite)

{

    Directory.Delete(frontEnd.GetOutputPath(ProjectName), true);

}

 

var outputDirectory = frontEnd.GenerateDevelopmentTree(ProjectName, UnitTestFramework);

Console.WriteLine("Tree Generation complete.{0}{0}Files can be found at:{0}\"{1}\"", Environment.NewLine, outputDirectory);

That's it! In under 60 minutes we were able to totally transform the command line tool into something a little more robust and testable (and even add a new feature to fix an old bug). Now when we run the Tree Surgeon console app we get a rich descriptive help screen:

TreeSurgeon version 2.0
Copyright (C) 2007 - 2008 Bil Simser
Copyright (C) 2005 - 2006 Mike Roberts, ThoughtWorks, Inc.

Creates a .NET development tree

TreeSurgeonConsole.exe </p|/project:> [/v|/version:] [/nologo] [/overwrite] [/t|/test:]

> Calling the application without arguments
  Prints the help information on the screen.

> Generates a new .NET development tree for a given project name.
  /p|/project:value (required):
        Specifies the project name.
        Please note - project name must not contain spaces.
        We recommend you use CamelCase for project names.

  /v|/version:value :
        Specifies the Visual Studio version to generate.
        Valid options are: "2003", "2005", or "2008"
        Default is "2008"

  [/nologo]:
        When turned on, the logo and description
        information will not be displayed.

  [/overwrite]:
        When turned on, any project with the same name
        will be deleted.

  /t|/test:value :
        Specifies the Unit Test framework to use when generating the tree.
        Valid options are: "NUnit", or "MbUnit"
        Default is "NUnit"

A few benefits I got from this conversion:

  • Options can be specified in any order. In the original code args[0] was the project name and args[1] was the version number. Now the user can specify the project name anywhere
  • The old system would bomb out if we tried to overwrite an existing directory. It will still do that, but we now have an "/overwrite" option that was added using one property and 3 lines of code
  • A highly descriptive help message is displayed to the user so discoverabilyt of what options are available is now there and didn't cost me anything in formatting
  • Users can specify options using long names "/version" or short "/v". Also I could add a new variation just by updating the attribute
  • My options are now fully testable and I don't have to run the app or even mock or fake it out 
  • I have a highly flexible command line runner that I can extend with ease

So, if you've got a console application sitting around you might want to give the ACF a spin and try it out. Or if you're building a new app take a look at it. It was low impact and high value for my investment and gave me a better end result that's now testable and easy to extend. You might find it useful like I did. Many thanks to Sunny Chen for putting this library together, it's a great tool.

Enjoy!

2 Comments

  • I think camel case is first letter lowered, all other words upper:
    camelCase, lookAtThis, turnVariable, doSomething

    Pascal case is all first upper:
    PascalCase, LookAtThis, TurnVariable, DoSomething

    just a nit-pick fyi ;-)

  • Now, everybody: Raise your hand if you've ever written your own console-parameter-handler-solution-thingy.

    *hand raised*

    next console app will definately give ACF a spin and contribute back. thanks for the heads up bil:)

Comments have been disabled for this content.