Presenting a simple, yet reasonably featured command line argument parsing function in C#
Introduction
Update 2: For a more complete solution with a drop in code file, please see this linked article.
Update: I found and fixed a small bug, so if you copied this before, you may want to do so again, with my apologies.
I don't like having to rely on libraries that require a lot of buy in for what they do. Command line argument processing should be as simple as it can be, and no simpler.
With that in mind, I've created a single function that takes a dictionary preloaded with a sort of specification it uses to generate a command line parser for those arguments, such that it can take named arguments with "typed" parameters (on the command line for instance, the first argument could be an unnamed series of strings) or you can have a /bool
switch in there. Whatever.
It's not perfect, and it doesn't do wizardry like creating a "using screen" for you. What it is, is something that satisfies the 80/20 rule and speeds up development for command line tools.
The Routine (Simpler Version)
It can take arguments of string[]
/ICollection<string>
, bool
, or string
. You add empty instances to the arguments dictionary to indicate the above type.
This can be copied wholesale into your Program
class in your code:
public static void CrackArguments(string defaultname, string[] args,
ICollection<string> required, IDictionary<string, object> arguments)
{
var argi = 0;
if (!string.IsNullOrEmpty(defaultname))
{
if (args.Length == 0 || args[0][0] == '/')
{
if (required.Contains(defaultname))
throw new ArgumentException(string.Format
("<{0}> must be specified.", defaultname));
}
else
{
var o = arguments[defaultname];
var isarr = o is string[];
var iscol = o is ICollection<string>;
if (!isarr && !iscol && !(o is string))
throw new InvalidProgramException(string.Format
("Type for {0} must be string or a string collection or array",
defaultname));
for (; argi < args.Length; ++argi)
{
var arg = args[argi];
if (arg[0] == '/') break;
if (isarr)
{
var sa = new string[((string[])o).Length + 1];
Array.Copy((string[])o, sa, sa.Length - 1);
sa[sa.Length - 1] = arg;
arguments[defaultname] = sa;
o = sa;
}
else if (iscol)
{
((ICollection<string>)o).Add(arg);
}
else if ("" == (string)o)
{
arguments[defaultname] = arg;
}
else
throw new ArgumentException(string.Format
("Only one <{0}> value may be specified.", defaultname));
}
}
}
for (; argi < args.Length; ++argi)
{
var arg = args[argi];
if (string.IsNullOrWhiteSpace(arg) || arg[0] != '/')
{
throw new ArgumentException(string.Format
("Expected switch instead of {0}", arg));
}
arg = arg.Substring(1);
if (!char.IsLetterOrDigit(arg, 0))
throw new ArgumentException("Invalid switch /{0}", arg);
object o;
if (!arguments.TryGetValue(arg, out o))
{
throw new InvalidProgramException
(string.Format("Unknown switch /{0}", arg));
}
var isarr = o is string[];
var iscol = o is ICollection<string>;
var isbool = o is bool;
var isstr = o is string;
if (isarr || iscol)
{
while (++argi < args.Length)
{
var sarg = args[argi];
if (sarg[0] == '/')
break;
if (isarr)
{
var sa = new string[((string[])o).Length + 1];
Array.Copy((string[])o, sa, sa.Length - 1);
sa[sa.Length - 1] = sarg;
arguments[arg] = sa;
o=sa;
}
else if (iscol)
{
((ICollection<string>)o).Add(sarg);
}
}
}
else if (isstr)
{
if (argi == args.Length - 1)
throw new ArgumentException
(string.Format("Missing value for /{0}", arg));
var sarg = args[++argi];
if ("" == (string)o)
{
arguments[arg] = sarg;
}
else
throw new ArgumentException
(string.Format("Only one <{0}> value may be specified.", arg));
}
else if (isbool)
{
if ((bool)o)
{
throw new ArgumentException
(string.Format("Only one /{0} switch may be specified.", arg));
}
arguments[arg] = true;
}
else
throw new InvalidProgramException(string.Format("Type for {0}
must be a boolean, a string, a string collection or a string array", arg));
}
foreach (var arg in required)
{
if (!arguments.ContainsKey(arg))
{
throw new ArgumentException(string.Format
("Missing required switch /{0}", arg));
}
var o = arguments[arg];
if (null == o || ((o is string) && ((string)o) == "") ||
((o is System.Collections.ICollection) &&
((System.Collections.ICollection)o).Count == 0) )
throw new ArgumentException
(string.Format("Missing required switch /{0}", arg));
}
}
The Longer, More Capable Routine
It can take arguments of string[]
/ICollection<string>
, bool
, or string
. In addition, it can take arguments that are convertible from a string
(via a TypeConverter
or static
Parse()
method). You add empty instances to the arguments dictionary to indicate the above type.
public static void CrackArguments(string defaultname,
string[] args, ICollection<string> required, IDictionary<string, object> arguments)
{
var argi = 0;
if (!string.IsNullOrEmpty(defaultname))
{
if (args.Length == 0 || args[0][0] == '/')
{
if (required.Contains(defaultname))
throw new ArgumentException(string.Format
("<{0}> must be specified.", defaultname));
}
else
{
var o = arguments[defaultname];
Type et = o.GetType();
var isarr = et.IsArray;
MethodInfo coladd = null;
MethodInfo parse = null;
if (isarr)
{
et = et.GetElementType();
}
else
{
foreach (var it in et.GetInterfaces())
{
if (!it.IsGenericType) continue;
var tdef = it.GetGenericTypeDefinition();
if (typeof(ICollection<>) == tdef)
{
et = et.GenericTypeArguments[0];
coladd = it.GetMethod("Add",
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance,
new Type[] { et });
}
}
}
TypeConverter conv = TypeDescriptor.GetConverter(et);
if (conv != null)
{
if (!conv.CanConvertFrom(typeof(string)))
{
conv = null;
}
}
if (conv == null && !isarr && coladd == null)
{
var bt = et;
while (parse == null && bt != null)
{
try
{
parse = bt.GetMethod("Parse", BindingFlags.Static |
BindingFlags.Public);
}
catch (AmbiguousMatchException)
{
parse = bt.GetMethod("Parse", BindingFlags.Static |
BindingFlags.Public, new Type[] { typeof(string) });
}
bt = bt.BaseType;
}
}
if (!isarr && coladd == null && !(o is string) && conv == null)
throw new InvalidProgramException(string.Format
("Type for {0} must be string or a collection,
array or convertible type", defaultname));
for (; argi < args.Length; ++argi)
{
var arg = args[argi];
if (arg[0] == '/') break;
if (isarr)
{
var arr = (Array)o;
var newArr = Array.CreateInstance(et, arr.Length + 1);
Array.Copy(arr, newArr, newArr.Length - 1);
object v;
v = arg;
if (conv == null)
{
if (parse != null)
{
v = parse.Invoke(null, new object[] { arg });
}
}
else
{
v = conv.ConvertFromInvariantString(arg);
}
newArr.SetValue(v, newArr.Length - 1);
arguments[defaultname] = newArr;
o = newArr;
}
else if (coladd != null)
{
object v;
v = arg;
if (conv == null)
{
if (parse != null)
{
v = parse.Invoke(null, new object[] { arg });
}
}
else
{
v = conv.ConvertFromInvariantString(arg);
}
coladd.Invoke(o, new object[] { v });
}
else if ("" == (string)o)
{
arguments[defaultname] = arg;
}
else if (conv != null)
{
arguments[defaultname] = conv.ConvertFromInvariantString(arg);
}
else if (parse != null)
{
arguments[defaultname] = parse.Invoke(null, new object[] { arg });
}
else
throw new ArgumentException(string.Format
("Only one <{0}> value may be specified.", defaultname));
}
}
}
for (; argi < args.Length; ++argi)
{
var arg = args[argi];
if (string.IsNullOrWhiteSpace(arg) || arg[0] != '/')
{
throw new ArgumentException(string.Format
("Expected switch instead of {0}", arg));
}
arg = arg.Substring(1);
if (!char.IsLetterOrDigit(arg, 0))
throw new ArgumentException("Invalid switch /{0}", arg);
object o;
if (!arguments.TryGetValue(arg, out o))
{
throw new InvalidProgramException(string.Format("Unknown switch /{0}", arg));
}
Type et = o.GetType();
var isarr = et.IsArray;
MethodInfo coladd = null;
MethodInfo parse = null;
var isbool = o is bool;
var isstr = o is string;
if (isarr)
{
et = et.GetElementType();
}
else
{
foreach (var it in et.GetInterfaces())
{
if (!it.IsGenericType) continue;
var tdef = it.GetGenericTypeDefinition();
if (typeof(ICollection<>) == tdef)
{
et = et.GenericTypeArguments[0];
coladd = it.GetMethod("Add", System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance, new Type[] { et });
break;
}
}
}
TypeConverter conv = TypeDescriptor.GetConverter(et);
if (conv != null)
{
if (!conv.CanConvertFrom(typeof(string)))
{
conv = null;
}
}
if (conv == null)
{
var bt = et;
while (parse == null && bt != null)
{
try
{
parse = bt.GetMethod("Parse", BindingFlags.Static |
BindingFlags.Public);
}
catch (AmbiguousMatchException)
{
parse = bt.GetMethod("Parse", BindingFlags.Static |
BindingFlags.Public, new Type[] { typeof(string) });
}
bt = bt.BaseType;
}
}
if (isarr || coladd != null)
{
while (++argi < args.Length)
{
var sarg = args[argi];
if (sarg[0] == '/')
break;
if (isarr)
{
var arr = (Array)o;
var newArr = Array.CreateInstance(et, arr.Length + 1);
Array.Copy(newArr, arr, arr.Length - 1);
object v=sarg;
if (conv == null)
{
if (parse != null)
{
v = parse.Invoke(null, new object[] { sarg });
}
}
else
{
v = conv.ConvertFromInvariantString(sarg);
}
newArr.SetValue(v, arr.Length - 1);
}
else if (coladd != null)
{
object v=sarg;
if (conv == null)
{
if (parse != null)
{
v = parse.Invoke(null, new object[] { sarg });
}
}
else
{
v = conv.ConvertFromInvariantString(sarg);
}
coladd.Invoke(o, new object[] { v });
}
}
}
else if (isstr)
{
if (argi == args.Length - 1)
throw new ArgumentException(string.Format("Missing value for /{0}", arg));
var sarg = args[++argi];
if ("" == (string)o)
{
arguments[arg] = sarg;
}
else
throw new ArgumentException(string.Format
("Only one <{0}> value may be specified.", arg));
}
else if (isbool)
{
if ((bool)o)
{
throw new ArgumentException(string.Format
("Only one /{0} switch may be specified.", arg));
}
arguments[arg] = true;
}
else if (conv != null)
{
if (argi == args.Length - 1)
throw new ArgumentException(string.Format("Missing value for /{0}", arg));
arguments[arg] = conv.ConvertFromInvariantString(args[++argi]);
}
else if (parse != null)
{
arguments[arg] = parse.Invoke(o, new object[] { args[++argi] });
}
else
throw new InvalidProgramException(string.Format
("Type for {0} must be a boolean, a string, a string collection,
a string array, or a convertible type", arg));
}
foreach (var arg in required)
{
if (!arguments.ContainsKey(arg))
{
throw new ArgumentException(string.Format
("Missing required switch /{0}", arg));
}
var o = arguments[arg];
if (null == o || ((o is string) && ((string)o) == "") ||
((o is System.Collections.ICollection) &&
((System.Collections.ICollection)o).Count == 0) )
throw new ArgumentException(string.Format
("Missing required switch /{0}", arg));
}
}
Using the Code
Simple Routine Command Line:
example.exe foo.txt bar.txt /output foobar.cs /ifstale
Advanced Routine Command Line
example.exe foo.txt bar.txt /output foobar.cs /id 5860F36D-6207-47F9-9909-62F2B403BBA8
/ips 192.168.0.104 192.168.0.200 /ifstale /count 5 /enum static /indices 5 6 7 8
static int Main(string[] args)
{
var arguments = new Dictionary<string, object>();
arguments.Add("inputs", new string[0]);
arguments.Add("output", "");
arguments.Add("ifstale", false);
CrackArguments("inputs", args, new string[] { "inputs" }, arguments);
foreach (var entry in arguments)
{
Console.Write(entry.Key + ": ");
var v = entry.Value;
if (v is string)
{
Console.WriteLine((string)v);
}
else
if (v is System.Collections.IEnumerable)
{
var e = (System.Collections.IEnumerable)v;
Console.Write("Type: {0}: -> ", v.GetType());
var delim = "";
foreach (var item in e)
{
Console.Write(delim);
Console.Write("Type {0}: ", item?.GetType());
Console.Write(item);
delim = ", ";
}
Console.WriteLine();
}
else
{
Console.Write("Type {0}: ", v?.GetType());
Console.WriteLine(v);
}
}
return 0;
}
That's all there is to it! It supports TypeConverter
and Parse()
now but you can still extend it as you like.
History
- 23rd January, 2024 - Initial submission
- 23rd January, 2024 - Bugfix in multiple args after default
- 23rd January, 2024 - Added advanced routine using type converters and
Parse()
methods