Lesson 1.2: Multiple, Default, and Hidden Commands

Having one command, like we did in our first lesson, is useful but limited. With Spectre.Console.Cli, we can define multiple commands, each with their own arguments and options, and each getting called when the user specifies that command in the command-line arguments. So let’s take a look at defining multiple commands and a default command for your app.

Reminder: you can find the source for this lesson in our Azure DevOps repo: Cli.Lesson1.2 – Repos (azure.com).

First, let’s start by creating a new Console App in Visual Studio named Cli.Lesson1.2… review project creation in Lesson 1.1, for the full details on getting setup.

Defining Multiple Commands

Like we did previously, let’s create a Commands folder in the new project. Then in that folder, we will re-create the same HelloCommand:

using Spectre.Console.Cli;
using Spectre.Console;
using System.ComponentModel;

namespace Cli.Lessons.Commands
{
    public class HelloCommand : Command<HelloCommand.Settings>
    {
        public class Settings : CommandSettings
        {
            [CommandOption("-n|--name <NAME>")]
            [Description("The person or thing to greet.")]
            [DefaultValue("World")]
            public string Name { get; set; }
        }

        public override int Execute(CommandContext context, Settings settings)
        {
            AnsiConsole.MarkupLine($"Hello [bold yellow]{settings.Name}[/]!");
            return 0;
        }
    }
}

The detailed description of the HelloCommand can be found in Lesson 1.1.

Then, we create a second command called ByeCommand (also in the Commands folder):

using Spectre.Console.Cli;
using Spectre.Console;

namespace Cli.Lessons.Commands
{
    public class ByeCommand : Command
    {
        public override int Execute(CommandContext context)
        {
            AnsiConsole.WriteLine("Okay, Byeeee!!!");
            return 0;
        }
    }
}

This is an easier command, with no arguments or options, that simply writes a good-bye message. We are purposefully starting with commands that have minimal code to focus on understanding the Spectre.Console.Cli capabilities.

Finally, we need to update the Program.Main method to create the ConsoleApp, configure both commands, and then run the app.

using Cli.Lessons.Commands;
using Spectre.Console.Cli;

namespace Cli.Lessons
{
    class Program
    {
        public static int Main(string[] args)
        {
            var app = new CommandApp();
            app.Configure(config =>
            {
                config.ValidateExamples();

                config.AddCommand<HelloCommand>("hello")
                    .WithAlias("hi")
                    .WithDescription("Say hello to anyone.")
                    .WithExample(new[] { "hello", "--name", "DarthPedro" });

                config.AddCommand<ByeCommand>("bye")
                    .WithDescription("Says goodbye.")
                    .WithExample(new[] { "bye" });
            });

            return app.Run(args);
        }
    }
}

As we can see, the Main method code is very similar to what we defined in the previous lesson:

  • Line #10 defines the Spectre.Console.Cli.ConsoleApp for this program.
  • Lines #15-18 configures the hello command, like we did in the previous lesson with a description and a usage example.
  • Line #16 WithAlias allows us to define another keyword that can be used in the command-line arguments that will also map to this HelloCommand. In this code, call the app with either lesson hello -n Me or lesson hi -n Me invokes the same command.
  • Lines #20-22 configures the bye command in the same way… this code should be starting to look familiar.
  • Finally, line #25 runs the command-line app.

Let’s build and run our command-line app to see how it behaves now.

1. If we just run the command: lesson -h, we will see the help for the new app. This help text includes descriptions for both commands now.

Fig 1 – Lesson1.2 Help Text

2. Run the commands: lesson hi -n DarthPedro and lesson hello --name DarthPedro. We will see that we get the same behavior.

Fig 2 – Output with HelloCommand

3. Let’s see the bye command: lesson bye.

Fig 3 – Output with ByeCommand

As we can see multiple commands can be coded and processed by the command-line app, providing control flow for you application based on the arguments.

Specifying a Default Command

In our current command configuration, if the user just runs that app with no arguments, the ConsoleApp behavior is to show the help text for the app root commands. However, we can also specify a default command to run in when no command is specified.

using Cli.Lessons.Commands;
using Spectre.Console.Cli;

namespace Cli.Lessons
{
    class Program
    {
        public static int Main(string[] args)
        {
            var app = new CommandApp<HelloCommand>();
            app.Configure(config =>
            {
                config.ValidateExamples();

                config.AddCommand<HelloCommand>("hello")
                    .WithAlias("hi")
                    .WithDescription("Say hello to anyone.")
                    .WithExample(new[] { "hello", "--name", "DarthPedro" });

                config.AddCommand<ByeCommand>("bye")
                    .WithDescription("Says goodbye.")
                    .WithExample(new[] { "bye" });
            });

            return app.Run(args);
        }
    }
}

We do this by just specifying the default command type (HelloCommand in this case) in the generic version of the CommandApp class. Now, if we just call our command-line app: lesson, the HelloComand.Execute gets called with the default Settings.Name.

Fig 4 – Default Command No Arguments

We can also call it with the –name option too (lesson --name DarthPedro).

Fig 5 – Default Command with Name Option

Finally, rather than using the generic CommandApp with the default command type, we can instead define the default command by calling the CommandApp.SetDefaultCommand method.

using Cli.Lessons.Commands;
using Spectre.Console.Cli;

namespace Cli.Lessons
{
    class Program
    {
        public static int Main(string[] args)
        {
            var app = new CommandApp();
            app.SetDefaultCommand<HelloCommand>();

            app.Configure(config =>
            {
                config.ValidateExamples();

                config.AddCommand<HelloCommand>("hello")
                    .WithAlias("hi")
                    .WithDescription("Say hello to anyone.")
                    .WithExample(new[] { "hello", "--name", "DarthPedro" });

                config.AddCommand<ByeCommand>("bye")
                    .WithDescription("Says goodbye.")
                    .WithExample(new[] { "bye" });
            });

            return app.Run(args);
        }
    }
}

If we run the same commands above, we will see the exact same behavior. So there are a couple of ways to set the default command for our app.

Hidden Commands

It is also possible to have hidden commands that will be processed but are not displayed in the help text. These commands can be for power users or to provide some private functionality until we are ready to make it public for everyone. To do this, there is an IsHidden method for command configurations.

First, let’s create a new simple SecretCommand in the Commands folder, which just shows a secret message:

using Spectre.Console.Cli;
using Spectre.Console;

namespace Cli.Lessons.Commands
{
    public class SecretCommand : Command
    {
        public override int Execute(CommandContext context)
        {
            AnsiConsole.WriteLine("Shhhhh.....");
            return 0;
        }
    }
}

Then, we need to configure it in our ConsoleApp:

using Cli.Lessons.Commands;
using Spectre.Console.Cli;

namespace Cli.Lessons
{
    class Program
    {
        public static int Main(string[] args)
        {
            var app = new CommandApp<HelloCommand>();
            app.Configure(config =>
            {
                config.ValidateExamples();

                config.AddCommand<HelloCommand>("hello")
                    .WithAlias("hi")
                    .WithDescription("Say hello to anyone.")
                    .WithExample(new[] { "hello", "--name", "DarthPedro" });

                config.AddCommand<ByeCommand>("bye")
                    .WithDescription("Says goodbye.")
                    .WithExample(new[] { "bye" });

                config.AddCommand<SecretCommand>("secret")
                    .WithDescription("Secret command for power users.")
                    .IsHidden();
            });

            return app.Run(args);
        }
    }
}

As we can see, the AddCommand<SecretCommand> configuration is very similar to the other commands we’ve made earlier. The only difference is line #26, where we call the IsHidden method.

Now if we build and run our command-line app, we will see some new behavior. First, when we get the app help text, we can see that there is no mention of the secret command.

Fig 6 – Help Text with Hidden Command

But when we use the command as a command-line argument, that command is processed and its Execute method is called: lesson secret.

Fig 7 – ConsoleApp Using Hidden Command

In conclusion, we looked at building and configuring multiple commands in our app. Multiple commands with the CommandApp argument processing functionality provides control flow for our app based on command-line arguments, so we can build some complex logic in our CLI apps. Then the default command makes it easy to run a command when no arguments are specified. And command configuration allows us to provide public and hidden commands based on our needs.

Moving forward, we are going to look at nested commands and configuring them using the AddBranch method.

Note: These used to be two separate packages (Spectre.Console and Spectre.Cli), but the have been combined for easier development and support. If you use Spectre.Console prior to version 0.36.0, then this sample code will not work. You can either upgrade the to the latest Spectre.Console package, or include the corresponding Spectre.Cli package.

One thought on “Lesson 1.2: Multiple, Default, and Hidden Commands

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s