Introduction
Obviously, a program starts with Main(string[] args)
where args
usually has to be somehow parsed and evaluated. There are many solutions of this simple but sometimes boring and non-creative task. Part 1 and 2 of this article propose two more different solutions. The peculiarity is that they do not parse anything themselves, they are entirely based on C# and .NET capabilities.
In Part 1, a command line argument is treated as a piece of C# code accessing program stuff. A command line becomes a real part of the program and it is written in C#, like a program. This approach is very powerful for developers, especially for debugging and testing. But for an end user, this may be inconvenient and even unsafe.
Part 2 proposes an approach designed exactly for an end user. A command line looks almost like a traditional one. How does more or less "traditional" command line look like? Let say, it has a number of required positional arguments and a number of optional named arguments. It reminds of a C# attribute argument list and leads to the idea: let a command line consists of one argument, which in fact is a list of arguments of an attribute. Our aim is to access the data with minimal programming effort.
Step 1: Input data design
Let's talk in terms of a hypothetical example program (source code is attached). Suppose our program expects the following input data:
- Required string value (for example, some directory name);
- Required bit field (for example, flags of some jobs to do);
- Some optional
int
value;
- Some optional
bool
value;
- Some optional
double
value.
We are going to use an attribute arguments style for the args[0]
value in the Main()
.
Example of args[0]
values:
null, Jobs.Job1|Jobs.Job2
@"C:\Temp", Jobs.Job2|Jobs.Job3, MyDouble = 3.14
@"C:\Temp", Jobs.Job3, MyBool = true, MyDouble = 3.14, MyInt = 123
Note: the command line consists of the only argument, which has to be entirely enclosed in quotation marks (") and any inner quotation mark has to be preceded by (\). These requirements come from the command line rules.
Command lines corresponding to args[0]
above:
"null, Jobs.Job1|Jobs.Job2"
"@\"C:\Temp\", Jobs.Job2|Jobs.Job3, MyDouble = 3.14"
"@\"C:\Temp\", Jobs.Job3, MyBool = true, MyDouble = 3.14, MyInt = 123"
Step 2: Input data class
Having designed input data, we create a class to keep them. The class InputAttribute
is derived from System.Attribute
. Required data (directory name and job flags) are implemented as read only properties and a constructor taking them as parameters is added to the class. Optional data are implemented as read/write properties; their names in a real program have to be well thought-out to describe the meaning well enough. Exactly these names are used in a command line by a user. Finally, we define enum Jobs
whose values are also used in a command line.
namespace Test
{
[FlagsAttribute()]
public enum Jobs { Job1 = 1, Job2 = 2, Job3 = 4 }
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class InputAttribute : System.Attribute
{
public InputAttribute(string directory, Jobs jobs)
{
this.directory = directory;
this.jobs = jobs;
}
public string Directory
{
get { return directory; }
}
public Jobs Jobs
{
get { return jobs; }
}
public int MyInt
{
get { return myInt; }
set { myInt = value; }
}
public bool MyBool
{
get { return myBool; }
set { myBool = value; }
}
public double MyDouble
{
get { return myDouble; }
set { myDouble = value; }
}
private string directory;
private Jobs jobs;
private int myInt = 0;
private bool myBool = false;
private double myDouble = 0.0;
}
}
Step 3: Input data object
Now we are ready to write the only code statement to get the command line data:
InputAttribute input = (InputAttribute)Attributer.Evaluate(
typeof(InputAttribute), args[0], "using Test;");
Where:
typeof(InputAttribute)
is the type of our attribute class;
args[0]
is the command line argument of our program;
"using Test;"
is used for brevity to allow writing "Jobs.Job1"
instead of "Test.Jobs.Job1"
in the command line.
The code
The class Absolute.Attributer
is an attribute evaluator (source code is attached). It is a "static class", it has the only a static
method Evaluate()
. This method performs following three small steps:
- An assembly source code is generated. It contains
using
directives (if needed), an assembly attribute and no other code. An example of the generated source code: using Test;
[assembly:InputAttribute(@"C:\Temp", Jobs.Job1,
MyDouble = 3.14, MyBool = true)]
- The assembly source is compiled and the assembly is created in memory. This assembly contains nothing but our attribute
InputAttribute
.
- An instance of the
InputAttribute
is requested and returned. This instance contains all our data evaluated and ready to use.
A word about safety
In contrast with Part 1 of the article (execution of a piece of C# code coming from a command line), this way seems safe for an end user. By definition, attribute arguments cannot be non-constant expressions. If a careless or malicious non-constant expression is written as an attribute argument, then it cannot be compiled. Step 2 (see Background) will fail at compile time and an exception will be thrown. You can try to write different "bad" command lines for our example program, run it and see what happens. You will get an error message and the program will stop. Nevertheless, could someone find the joint in the armor?
Summary
The attached project contains two files: Attributer.cs (utility class) and Class1.cs (example program described here). I hope this simple utility will help someone to avoid non-creative command lines parsing work in his own programs. Any remarks are welcome.