Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save iamarcel/9bdc3f40d95c13f80d259b7eb2bbcabb to your computer and use it in GitHub Desktop.
Save iamarcel/9bdc3f40d95c13f80d259b7eb2bbcabb to your computer and use it in GitHub Desktop.
Structuring Neat .NET Core Command Line Apps Neatly

Structuring Neat .NET Core Console Apps Neatly

So you’ve created a really neat console app, but it’s growing and you need a way to keep it all neatly organized and preferrably with some Good Practices.

The guys at Entity Framework have thought about this and structured their console app really neatly. Today, we’ll take the ninja app we built previously and make it all look pretty and stuff.

I know what I’m doing, just gimme the damn framework!

Separation of Concerns, Please

We’ll start by breaking up our commands. A couple of requirements:

  • Each Command should live in its own file/class
  • Separate setting up options and arguments from the actual execution
  • Let the main execution start at the root; Command classes shouldn’t do anything on their own

Every Command will have three methods:

  • A static Configure method that sets up the Arguments, Options and nested Commands
  • A constructor to pass in properties (done in OnExecute, in Configure)
  • A Run method that actually executes the command

Like this:

// Commands/ICommand.cs
public interface and {
    void Run();
}

// Commands/AttackCommand.cs
public class AttackCommand : ICommand {
    public static void Configure(CommandLineApplication command) {

        command.Description = "Instruct the ninja to hide in a specific location.";
        command.HelpOption("-?|-h|--help");

        var locationArgument = command.Argument("[location]",
                                                "Where the ninja should hide.");

        command.OnExecute(() => {
                (new AttackCommand(locationArgument.Value)).Run();
                return 0;
            });
    }

    private readonly string _location;

    public AttackCommand(string location) {
        _location = location;
    }

    public void Run() {
        var location = _location != null
            ? _location
            : "in a trash can";
        Console.WriteLine("Ninja is hidden " + location);
    }
}

Now we can clean up Main quite a bit:

// Set up the app
var app = new CommandLineApplication();
app.Name = "ninja";
app.HelpOption("-?|-h|--help");

// Register commands
app.Command("hide", HideCommand.Configure);
app.Command("attack", AttackCommand.Configure);

app.OnExecute(() => {
        (new CommandLineApplication(app)).Run();
        return 0;
    });

// Fire!
app.Execute(args);

Root Application

It’s starting to look neat but we can do even better. First, since the app is just another command we can move it to its own class:

// Commands/RootCommand.cs
public class RootCommand : ICommand {

    public static void Configure(CommandLineApplication app) {
        app.Name = "ninja";
        app.HelpOption("-?|-h|--help");

        // Register commands
        app.Command("hide", HideCommand.Configure);
        app.Command("attack", AttackCommand.Configure);

        app.OnExecute(() => {
                (new RootCommand(app)).Run();
                return 0;
            });
    }

    private readonly CommandLineApplication _app;

    public RootCommand(CommandLineApplication app) {
        _app = app;
    }

    public void Run() {
        _app.ShowHelp();
    }

}

So now Main is only three lines!

// Program.cs
var app = new CommandLineApplication();
RootCommand.Configure(app);
app.Execute(args);

Global Options

One more feature: I’d like to have some global options that we can pass along and use in any child Command we’d like.

To do this, we’ll create a CommandLineOptions class and basically move everything there. And while we’re at it, store the Command to be executed as an option. That fulfills the requirement of the Commands not doing anything by themselves.

public class CommandLineOptions {

    public static void Parse(string[] args) {
        var options = new CommandLineOptions();

        var app = new CommandLineApplication();

        var isQuietOption = app.Option("--extra-quiet|-q",
                                       "Instruct the ninja to do its best to be even more quiet",
                                       CommandOptionType.NoValue);

        RootCommand.Configure(app, options);

        options.IsQuiet = isQuietOption.HasValue();

        var result = app.Execute(args);

        if (result != 0 || options.Command == null) {
            Console.Error.WriteLine("Fail");
            return null;
        }

        return options;
    }

    public CommandLineApplication Command;
    public bool IsQuiet;

}

We’ll need to make three changes in every Command now:

  1. Change OnExecute so it changes options.Command to the command itself
  2. Update Configure to accept the new argument
  3. Update any child Command calls in order ro be compatible with the new Configure API

For example, in RootCommand:

// Commands/RootCommand.cs
public class RootCommand : ICommand {

    public static void Configure(CommandLineApplication app, CommandLineOptions options) {
        app.Name = "ninja";
        app.HelpOption("-?|-h|--help");

        // Changed here
        app.Command("hide", c => HideCommand.Configure(c, options));
        app.Command("attack", c => AttackCommand.Configure(c, options));

        app.OnExecute(() => {
                options.Command = new RootCommand(app);
                return 0;
            });
    }

    private readonly CommandLineApplication _app;

    public RootCommand(CommandLineApplication app) {
        _app = app;
    }

    public void Run() {
        _app.ShowHelp();
    }

}

You get the idea.

But wait, that’s not all!

There’s some boilerplating going on here. So I made a little starter kit for you to kickstart your adventures creating console applications!

It’s up on GitHub so go ahead, use it and make more beautiful console apps, on the outside and on the inside!

@menjoo
Copy link

menjoo commented Jun 24, 2017

awesome thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment