Not the last console application. GoCommando

 
event

The first one in this series is GoCommando.
As it needs to be in order to be considered, comes as a NuGet package and it praises itself to be small and with a mixture of interfaces and attributes.

Command-centered

A command implements ICommand, gets its command-line metadata from attributes (CommandAttribute and DescriptionAttribute).

Properties of the command become command line parameters if decorated with ParameterAttribute and all possible features of the parameter (long name, short name, optionality, default value) are provided via constructor with default values.

Implementing our example

The implementation of our commands is pretty straightforward:

[Command("something")]
[Description("Do something")]
public class Something : ICommand
{
private readonly ADoerOfSomething _command;
public Something(TextWriter writer)
{
_command = new ADoerOfSomething(writer);
}
[Parameter("location", "l")]
[Description("place where the something was done")]
public string Location { get; set; }
[Parameter("times", "t", optional: true, defaultValue: "1")]
[Description("number of times something was done")]
public int Times { get; set; }
[Parameter("awesome", "a", optional: true)]
[Description("include if the something was awesome")]
public bool Awesome { get; set; }
public void Run()
{
var options = new OptionsForSomething
{
Location = Location,
Awesome = Awesome,
Times = Times
};
_command.Do(options);
}
}
[Command("something-else")]
[Description("Do something else")]
public class SomethingElse : ICommand
{
private readonly ADoerOfSomethingElse _command;
public SomethingElse(TextWriter writer)
{
_command = new ADoerOfSomethingElse(writer);
}
[Parameter("locations", "l")]
[Description("places where the something else was done")]
[Example("singleLocation")]
[Example("multiple,comma-separated-locations")]
public string Locations { get; set; }
[Parameter("awesome", "a", optional: true)]
[Description("include if the something else was awesome")]
public bool Awesome { get; set; }
private static readonly char[] _comma = {','};
public void Run()
{
var options = new OptionsForSomethingElse
{
Locations = Locations.Split(_comma, StringSplitOptions.RemoveEmptyEntries),
NotSoAwesome = !Awesome
};
_command.Do(options);
}
}
class Program
{
static void Main(string[] args)
{
Go.Run();
}
}
view raw Program.cs hosted with ❤ by GitHub

Just translate the properties of the ICommand to the argument object and call the wrapped command.
If the framework was embraced completely, we would implement our commands directly.

The Challenges

Mandatory Arguments

Mandatory arguments are the default and optional arguments is a matter of changing the value: [Parameter("awesome", "a", optional: true)].

Reporting a missing mandatory argument just works:

*NotLastConsole_GoCommando*> .\doer.exe something
The following required parameters are missing:
-location - place where the something was done
Invoke with -help <command> to get help for each command.
Exit code: -1
view raw mandatory.sh hosted with ❤ by GitHub

Any extra, non-mapped arguments will also throw an exception, which is nice, unless that behavior in unwanted.

*NotLastConsole_GoCommando*> .\doer.exe something-else -l asd -extra value
The following switches do not have a corresponding parameter:
-extra = value
Invoke with -help <command> to get help for each command.
Exit code: -1
view raw extra.sh hosted with ❤ by GitHub

Non-Textual arguments

Declaring the right type for the property takes one very far and flags (boolean properties) are supported OOTB.
Failures, however, are not handled gracefully and the exception flows:

*NotLastConsole_GoCommando*> .\doer.exe something -l here -times x
System.FormatException: Could not set value 'x' on property named 'Times' on DgonDotNet.Blog.Samples.NotLastConsole_GoCommando.Something ---> System.FormatException: Input string was not in a correct
format.
at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info)
at System.String.System.IConvertible.ToInt32(IFormatProvider provider)
at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
at System.Convert.ChangeType(Object value, Type conversionType)
at GoCommando.Internals.Parameter.SetValue(Object commandInstance, String value)
--- End of inner exception stack trace ---
at GoCommando.Internals.Parameter.SetValue(Object commandInstance, String value)
at GoCommando.Internals.CommandInvoker.ResolveParametersFromSwitches(IEnumerable`1 switches, ICommand commandInstance, ISet`1 setParameters)
at GoCommando.Internals.CommandInvoker.InnerInvoke(IEnumerable`1 switches, EnvironmentSettings environmentSettings)
at GoCommando.Internals.CommandInvoker.Invoke(IEnumerable`1 switches, EnvironmentSettings environmentSettings)
at GoCommando.Go.InnerRun(ICommandFactory commandFactory)
at GoCommando.Go.Run(ICommandFactory commandFactory)
Exit code: -2
view raw non-textual.sh hosted with ❤ by GitHub

Default values are provided as strings, so expect the same conversion rules.

Multi-arguments

As one can see from the implementation of the locations parameter of somethin-else, such property is not a collection, but a string property that needs to be parsed into a collection of strings. Unfortunately, out-of-the-box values are converted using Convert.ChangeTpe() which is very limited and does not support collections and there is no way to inject some custom behavior.
It is not a deal breaker but is something to keep in mind. Fortunately, we can help our users by providing examples of valid values:

-locations / -l
places where the something else was done
Examples:
-locations singleLocation
-locations multiple,comma-separated-locations
view raw multi.sh hosted with ❤ by GitHub

Showing Help

Running the program without arguments provides a nice overview:

*NotLastConsole_GoCommando*> .\doer.exe
Please invoke with a command - the following commands are available:
something - Do something
something-else - Do something else
Invoke with -help <command> to get help for each command.
Exit code: -1
view raw help.sh hosted with ❤ by GitHub

Which can be extended to the command level by using the -help command argument:

*NotLastConsole_GoCommando*> .\doer.exe -help something-else
Do something else
Type
doer.exe something-else <args>
where <args> can consist of the following parameters:
-locations / -l
places where the something else was done
Examples:
-locations singleLocation
-locations multiple,comma-separated-locations
-awesome / -a (flag/optional)
include if the something else was awesome
view raw help-command.sh hosted with ❤ by GitHub

Being an attribute-driven framework, localization becomes a challenge and there is nothing I could see to help us with that. Code is simple enough and well-factored to send a pull request to support resource files and string keys, the same way we can do in other attribute-driven frameworks, such as MVC.

Command dispatching

The concept of a command is The central concept of the library, so it fits nicely into our example.
On the other hand, if what we have is single command utility, sticking the sole command after the executable might be confusing.
There is an open issue so it is not far-fetched to assume that a default, single command feature will be added.

Invoking the command is a matter of calling Go.Run() from your console program entry point.

One nice and not widely announced feature is that it provides a way to hook up your IOC container to create command instances, by implementing the ICommandFactory interface.
In our sample, I just did an extremly poor man's DI, but plugging in your favorite container is a breeze:

internal class CustomFactory : ICommandFactory
{
public ICommand Create(Type commandType)
{
// do some extremelly poor-man's service locations
if (commandType == typeof(Something))
{
return new Something(Console.Out);
}
if (commandType == typeof(SomethingElse))
{
return new SomethingElse(Console.Out);
}
throw new ArgumentException($"Unknown command type: {commandType}");
}
public void Release(ICommand command)
{
// we could gently free up resources from commands here
}
}

When using command factories, one just specifies the type with the generic call Go.Run<CustomFactory>();.

Conclusion

GoCommando it is indeed small, useful and focused. The code is extremely well structured and the help is about right (bringing the extra features such as command factories from code samples into the wiki would cherry-top the package).

Definitely worth considering for simple (and not so simple) scenarios.

Last Console Series

  1. The beginning
  2. GoCommando (this)
  3. Command Line Parser
  4. PowerArgs
  5. LBi.Cli.Arguments
  6. Command Line Utils
  7. CLAP
  8. Wrap-up