Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Implementing Command Execution in a Console Application

0.00/5 (No votes)
17 Jan 2013 1  
Describes a simple yet nice approach to make an interactive console application

Introduction

Sometimes, we have the requirement to implement a program as a console application. Often, we need some feedback from the program, to see what happens, or even more - to execute a command, which changes the state of the Domain part.

This article describes the naive approach, and the possible solution for the problems that arise.

Note that the approach given here is for "medium-sized" programs, which have roughly 20 to 50 possible commands which therefore don't require sophisticated parsing. If you are writing a command-line interface with more than 100 commands, with auto-completion feature and so on - then the approach shown only might be the start point to develop from.

Background

When a .NET assembly is intended to work on Linux, and the UI might be present or not (for example, init(3) executed, or UI even is simply not installed), the best solution is to make a console application and start it with Mono. You might have other reasons to implement your program as a console application.

If the program is hard enough, you'll want to see what happens in real-time. Like "show me this queue, is it too full?", "show me the state of the channel, isn't the connection broken?" and so on. Time passes - and you need executing commands that do some kind of search in the Domain part, or even change this Domain. So you receive the interactive console application.

The Naive Approach

Here is the code that implements a hypothetical interactive console application. The examples will have 3 to 5 commands - just to keep things simple - but remember that we are considering situations where the number of commands is more than 20.

class Program
{
    static void printCounters1()
    {
        Console.WriteLine("Printing first counters");
    }

    static void printCounters2()
    {
        Console.WriteLine("Printing second counters");
    }

    static void printHelp()
    {
        Console.WriteLine("Supported: stop, print first counters (pfc), 
                           print second counters (psc)");
    }

    static void Main(string[] args)
    {
        Dictionary<string, string> domain = new Dictionary<string, string>
                            {{"first", "value 1"}, {"second", "value 2"}};
        bool shouldfinish = false;
        while (shouldfinish == false)
        {
            string command = Console.ReadLine();
            switch(command)
            {
                case "stop":
                    {
                        Console.WriteLine
                        ("Do you really want to stop? (type 'yes' to stop)");
                        string acceptString = Console.ReadLine();
                        if (acceptString == "yes")
                        {
                            shouldfinish = true;
                            continue;
                    }
                    printHelp();
                    break;
                    }
                case "print first counters":
                case "pfc":
                    {
                        printCounters1();
                        break;
                    }
                case "print second counters":
                case "psc":
                    {
                        printCounters2();
                        break;
                    }
                    
                default:
                    {
                        if (command.StartsWith("add"))
                        {
                            Console.WriteLine("Parsing plus executing the command 
                                               \" "+ command + "\"");
                            break;
                        }
                        printHelp();
                        break;
                    }
            }
        }
        Console.WriteLine("Disposing domain and stopping");
        Console.ReadKey();
    }
}

Nothing really hard. If the user enters "pfc", he gets the printCounters1() function executed and some useful information is on the screen.

If the command is not entered or entered incorrectly, the help string will be put out. Consider the default clause: we've got a command execution here, which should try to add an object to the "domain" - the dictionary. The exact parsing and dictionary interaction is not implemented not to make the code harder.

The problems we get:

  • Adding new command also requires adding its description to the help string
  • Messy switch statement
  • Some not-so easy to notice things

Let's say just, that this approach is not flexible.

Suggested Solution

Let's first consider the solution diagram:

Image 1

If you want to do something simple, like show some part of a system, and the command doesn't need any parsing - you can always use the SimpleCommand class. But to implement hard things, you should implement a class for each problem. Like the class AddCommand in the example. Abstract functions getSyntaxDescription, possibleStarts, processCommand force you not to forget about all the needed attributes for nice functioning.

All the commands are registered in the ConsoleCommandHolder instance. See the Main code now:

class Program
{
    static void printCounters1()
    {
        Console.WriteLine("Printing first counters");
    }

    static void printCounters2()
    {
        Console.WriteLine("Printing second counters");
    }
 
    static void printHelp()
    {
        Console.WriteLine("Supported: stop, print first counters (pfc), 
                           print second counters (psc)");
    }

    static void Main(string[] args)
    {
        Dictionary<string, string> domain = new Dictionary<string, 
          string> { { "first", "value 1" }, { "second", "value 2" } };
        bool shouldfinish = false;
        ConsoleCommandHolder holder = new ConsoleCommandHolder();
        holder.AddRange( new ConsoleCommand[]{ new SimpleCommand
                       ("Stop application", new string[]{"stop"}, 
            (x)=>
                {
                    Console.WriteLine("Do you really want to stop? (type 'yes' to stop)");
                    string acceptString = Console.ReadLine();
                    if (acceptString == "yes")
                    {
                        shouldfinish = true;
                    }

                }), 
                new SimpleCommand("Print first command", 
                new string[]{"print first command", "pfc"},(x)=>printCounters1() ),
                new SimpleCommand("Print second command", 
                new string[]{"print second command", "psc"},(x)=>printCounters2() ), 
                new SimpleCommand("Print dictionary", new string[]{"print dictionary", "pd"},
                    (x)=>
                        {
                            foreach (var next in domain)
                                Console.WriteLine("{0} -> {1}", next.Key, next.Value);
                        }), 
                new AddCommand(domain), 
        });
        while (shouldfinish == false)
        {
            string command = Console.ReadLine();
            ConsoleCommand toExecute = holder.Find(command);
            if(toExecute != null)
                toExecute.Do(command);
            else Console.WriteLine(holder);
        }
        Console.WriteLine("Disposing domain and stopping");
        Console.ReadKey();
    }
}

Each command "remembers" its prefixes, so the task of ConsoleCommandHolder is to find the appropriate command, which has the same prefix as in the entered string. In the main loop, we call holder.Find which is shown below:

public ConsoleCommand Find(string command)
{
     return unique.FirstOrDefault(x => x.Prefixes.Any(command.StartsWith));
}

Where unique is the collection of commands.

The output of the program is given further (entered: "pd", "add 2 3", "l", "psc", "stop", yes):

pd
first -> value 1
second -> value 2
add 2 3
OK
pd
first -> value 1
second -> value 2
2 -> 3
l
 
Stop application        Syntax: <stop>
Print first command     Syntax: <print first command><pfc>
Print second command    Syntax: <print second command><psc>
Print dictionary        Syntax: <print dictionary><pd>
Add string  to dictionary       Syntax: <add 'v1' 'v2'>
psc
Printing second counters
stop
Do you really want to stop? (type 'yes' to stop)
yes
Disposing domain and stopping

One might say that the program is still a bit messy. For some people, it is even easier to read a plain switch statement than a code with a bunch of constructor calls and lambda-expressions. But this is only a first impression. When the number of commands increases, this impression will vanish. The benefits we gain:

  • As I've just mentioned, the code becomes not so messy. It is much more easy to read the details inside the ConsoleCommand class, but not in the Main method.
  • Help is easier to maintain, you don't forget to provide help when adding new commands.
  • We can now develop the program in interesting ways:
    • Add a IsForbidden field into the ConsoleCommand class. And providing the procedure of login (again by means of a separate command), we can allow/forbid certain types of commands. It's easy to make so that forbidden commands are not shown in help.
    • Add log entry with time when each command is executed (you need only to change the base class, but not all offsprings).
    • Develop the type of command that might do opposite things. For example "add... " adds some data to Domain while "no add... " removes this object.
    • Use N ConsoleCommandHolder objects - which are filled with different sets of ConsoleCommand objects. Depending on the mode, you give the user different sets of commands.

And so on.

Note, all these changes affect the ConsoleCommand class in a not-so-messy way. Just imagine implementing these logics when you already have a switch with 20 "show" commands and 20 commands that need parsing.

Points of Interest

  • Describing the approach made me think of the Command pattern. The realization is not like the classical one: where each command is a separate object. In our example, only each command type is a separate object.
  • If you've got 2-5 commands, then the naive approach might be better for you.
  • I wonder if there is a standard approach to developing the auto-completion feature. Like if I press '?' - I've got all possible words I can enter, or if I press Tab - the word, I'm entering completes automatically.

History

  • 16.10.2012 - Got the idea while editing a switch statement and started writing the article
  • 18.10.2012 - First published

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here