Lets start this of with a good one. One thing that you guys propably have faced a number of times, often you take in a nuget package that does this for you, often you parse the string args in the main function of the program and delegate the actions. but i wanted to share a simple utility i wrote a number of years ago, that i’ve recently decided to put on github..

lets say we have a console application and we have a number of functions such as add, and divide.


public static class Program
{
   public static void Main(string[] args)
   {
       Add(args[0], args[1]);
   }

   public void static Add(int a, int b) {
      Console.WriteLine($"{a+b}");
   }
}

There are things with this that is just wrong on so many levels. espectally when you consider how the commandline arguments are put in, they have to be set up sequentially in the correct other as the developer wanted it.

if we wanted something else such as


program.exe add -a 5 -b 5

We’d have to do parsing and use reflection to inject the values where they needed to be, well that is actually easy.. The github repo i refrenced has been deployed to nuget, but you can also just copy the file in to your own project since its only 91 lines of c# .. yep, its that small .. lets have a look at it here.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace TCMD
{
    public static class TCMD
    {
        public static async Task Parse(params string[] args)
        {
            if (args.Length == 0)
                throw new ArgumentException("No parameters given.");

            var assembly = Assembly.GetEntryAssembly();
            var methods = assembly.GetTypes().SelectMany(t => t.GetMethods()).Where(m => m.GetCustomAttributes(typeof(CMD), false).Length > 0).ToArray();
            MethodInfo func = methods.FirstOrDefault(x => x.Name.ToLower() == args[0].ToLower());
            if (func == null) throw new ArgumentException($"Command {args[0]} not found.");
            List<object> newArgs = null;
            if (func.GetParameters().Any())
            {
                newArgs = new List<object>();
                int requiredParameters = func.GetParameters().Where(x => x.HasDefaultValue == false).Count();
                IEnumerable<string> GetArgument(string option) => args.SkipWhile(i => i.Substring(1, i.Length - 1).ToLower() != option.ToLower()).Skip(1).SelectArgChunks();
                foreach (var p in func.GetParameters())
                {
                    var paramValue = GetArgument(p.Name);
                    if (paramValue == null && p.DefaultValue != null)
                    {
                        throw new ArgumentException($"Parameter '{p.Name}' is required.");
                    }
                    else if (p.ParameterType.IsArray)
                    {

                        newArgs.Add(paramValue.ToArray());
                    }
                    else
                    {
                        var val = paramValue.FirstOrDefault();
                        if (int.TryParse(val, out var intVal))
                        {
                            newArgs.Add(intVal);
                        } else if(bool.TryParse(val, out var boolVal))
                        {
                            newArgs.Add(boolVal);
                        }
                        else
                        {
                            newArgs.Add(val);
                        }
                    }
                }
            }
            if (func.ReturnType == typeof(Task))
            {
                await (Task)func.Invoke(null, newArgs == null ? null : newArgs.ToArray());
            }
            else
            {
                await Task.Run(() => func.Invoke(null, newArgs == null ? null : newArgs.ToArray()));
            }
        }

        public static IEnumerable<string> SelectArgChunks(this IEnumerable<string> source)
        {
            foreach (var item in source)
            {
                if (item.StartsWith("-"))
                {
                    break;
                }
                else
                {
                    yield return item;
                }
            }
        }
    }

    public class CMD : Attribute
    {
        public string Alt { get; set; }

        public CMD(string Alt = "")
        {
            this.Alt = Alt;
        }
    }
}

what that does is allow us to add a simple attribute over the function we want to be execute from a shell / commandline and have a reflection parser inject the values and invoke the method. as such..


public static class Program
{
   public static void Main(string[] args)
   {
       TCMD.Parse(args);
   }

   [CMD]
   public void static Add(int a, int b) {
      Console.WriteLine($"{a+b}");
   }
}

It would be wise to try catch the parse method, to catch any exceptions that might be thrown top or lower in the code and actually handle the exceptions gracefully. but i guess that is a preference. The parse function will take the first argument as the function name, case insensitive and try to map any method arguments with the -[argumentName] [Value] as demonstated above with the commandline example.

Strings will be wrapped in a double quote, int, double, float .. what have you will be cast to their respective types as well. all handled with in those smal 91 lines. so it is up to you if you want to include the file in your source or install the nuget.

if you have any suggestions, please consider forking or contributing to the project. but keep in mine simpilicity is key and size means everything..

have fun..