Click here to Skip to main content
15,887,083 members
Articles / Programming Languages / C#
Tip/Trick

One Function Command Line Argument Parsing in C#

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
23 Jan 2024CPOL2 min read 5.4K   16   6
Easily crack your command line arguments into a dictionary with this code
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:

/// <summary>
/// Cracks command line arguments
/// </summary>
/// <param name="defaultname">The key name of the default parameter</param>
/// <param name="args">The args passed to the entry point</param>
/// <param name="required">A collection of strings that indicate the required arguments
/// </param>
/// <param name="arguments">A dictionary of arguments, 
///  with empty instances of various types for the values 
///  which specify the "type" of the argument</param>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidProgramException"></exception>
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) /*|| 
           ((o is bool) && (!(bool)o))*/)
            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.

C#
/// <summary>
/// Cracks command line arguments
/// </summary>
/// <param name="defaultname">The key name of the default parameter</param>
/// <param name="args">The args passed to the entry point</param>
/// <param name="required">A collection of strings that indicate the 
///  required arguments</param>
/// <param name="arguments">A dictionary of arguments, 
///  with empty instances of various types for the values 
///  which specify the "type" of the argument</param>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidProgramException"></exception>
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) /*|| 
           ((o is bool) && (!(bool)o))*/)
            throw new ArgumentException(string.Format
                    ("Missing required switch /{0}", arg));
    }
}

Using the Code

Simple Routine Command Line:

BAT
example.exe foo.txt bar.txt /output foobar.cs /ifstale

Advanced Routine Command Line

BAT
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
C#
static int Main(string[] args)
{
    var arguments = new Dictionary<string, object>();
    arguments.Add("inputs", new string[0]); // the input files (can be a List<string>)
    arguments.Add("output", ""); // the output file
    arguments.Add("ifstale", false);
    // following is for advanced routine
    //arguments.Add("id", Guid.Empty);
    //arguments.Add("ips", new List<IPAddress>());
    //arguments.Add("count", 0);
    //arguments.Add("indices", new List<int>());
    //arguments.Add("enum", System.Reflection.BindingFlags.Instance);
    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Just a shiny lil monster. Casts spells in C++. Mostly harmless.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Carl Edwards In SA24-Jan-24 0:17
professionalCarl Edwards In SA24-Jan-24 0:17 
GeneralRe: My vote of 5 Pin
honey the codewitch24-Jan-24 0:20
mvahoney the codewitch24-Jan-24 0:20 
Praise5 vote, as usual Pin
Daniele Alberto Galliano23-Jan-24 23:20
professionalDaniele Alberto Galliano23-Jan-24 23:20 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA23-Jan-24 8:41
professionalȘtefan-Mihai MOGA23-Jan-24 8:41 
GeneralAnother 5! Pin
Mircea Neacsu23-Jan-24 8:03
Mircea Neacsu23-Jan-24 8:03 
GeneralMy vote of 5 Pin
LightTempler23-Jan-24 6:38
LightTempler23-Jan-24 6:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.