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:
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