Click here to Skip to main content
15,892,809 members
Articles / Programming Languages / C#

Parsing Command Line Arguments

Rate me:
Please Sign up or sign in to vote.
4.71/5 (25 votes)
26 Apr 2009CPOL2 min read 62K   496   83  
The CommandLineParser library provides a simple way to define command line arguments and parse them in your application.
For applications that have one or two arguments, you could probably manage with some switches and ifs, but when there are more arguments, you could use a CommandLineParser library and thus make your code cleaner and more elegant.
using System;
using System.Collections;
using System.Collections.Generic;
using CommandLineParser.Arguments;
using System.Reflection;
using CommandLineParser.Exceptions;
using CommandLineParser.Validation;

namespace CommandLineParser
{
    /// <summary>
    /// CommandLineParser allows user to define command line arguments and then parse
    /// the arguments from the command line.
    /// </summary>
    /// <include file='Doc/CommandLineParser.xml' path='CommandLineParser/Parser/*' />
    public class CommandLineParser
    {
        #region property backing fields

        private List<Argument> arguments = new List<Argument>();

        private List<ArgumentCertification> certifications = new List<ArgumentCertification>();

        private string[] additionalArguments;

        private Dictionary<char, Argument> shortNameLookup;

        private Dictionary<string, Argument> longNameLookup;

        private string[] args;

        private bool acceptAdditionalArguments = true;

        private int requestedAdditionalArgumentsCount = 0;

        private bool showUsageOnEmptyCommandline = false;

        private bool checkMandatoryArguments = true;

        private bool checkArgumentCertifications = true; 

        private bool allowShortSwitchGrouping = true;

        #endregion

        /// <summary>
        /// Defined command line arguments
        /// </summary>
        public List<Argument> Arguments
        {
            get { return arguments; }
            set { arguments = value; }
        }

        /// <summary>
        /// Set of <see cref="ArgumentCertification">certifications</see> - certifications can be used to define 
        /// which argument combinations are allowed and such type of validations. 
        /// </summary>
        /// <seealso cref="CheckArgumentCertifications"/>
        /// <seealso cref="ArgumentCertification"/>
        /// <seealso cref="ArgumentGroupCertification"/>
        /// <seealso cref="DistinctGroupsCertification"/>
        public List<ArgumentCertification> Certifications
        {
            get { return certifications; }
            set { certifications = value; }
        }

        /// <summary>
        /// Collection of additional arguments that were found on the command line after <see cref="ParseCommandLine"/> call.
        /// </summary>
        /// <exception cref="CommandLineException">Field accessed before <see cref="ParseCommandLine"/> was called or 
        /// when <see cref="AdditionalArguments"/> is set to false</exception>
        /// <seealso cref="AcceptAdditionalArguments"/>
        public string[] AdditionalArguments
        { 
            get
            {
                if (acceptAdditionalArguments && additionalArguments != null)
                { 
                    return additionalArguments;
                }
                if (!acceptAdditionalArguments)
                {
                    throw new CommandLineException(Messages.EXC_ADDITONAL_ARGS_FORBIDDEN);
                }
                throw new CommandLineException(Messages.EXC_ADDITIONAL_ARGS_TOO_EARLY);
            }
        }

        /// <summary>
        /// Set RequestedAdditionalArgumentsCount to non-zero value if your application
        /// requires some additional arguments. 
        /// </summary>
        public int RequestedAdditionalArgumentsCount
        {
            get { return requestedAdditionalArgumentsCount; }
            set
            {
                if (value < 0) 
                    throw new ArgumentOutOfRangeException("The value must be non negative. ");
                requestedAdditionalArgumentsCount = value;
            }
        }

        /// <summary>
        /// When set to true (default), additional arguments are stored in <see cref="AdditionalArguments"/> collection when found on the 
        /// command line. When set to false, Exception is thrown when additional arguments are found by <see cref="ParseCommandLine"/> call.
        /// </summary>
        public bool AcceptAdditionalArguments
        {
            get { return acceptAdditionalArguments; }
            set { acceptAdditionalArguments = value; }
        }

        /// <summary>
        /// When set to true, usage help is printed on the console when command line is without arguments.
        /// Default is false. 
        /// </summary>
        public bool ShowUsageOnEmptyCommandline
        {
            get { return showUsageOnEmptyCommandline; }
            set { showUsageOnEmptyCommandline = value; }
        }

        /// <summary>
        /// When set to true, <see cref="MandatoryArgumentNotSetException"/> is thrown when some of the non-optional argument
        /// is not found on the command line. Default is true.
        /// See: <see cref="Argument.Optional"/>
        /// </summary>
        public bool CheckMandatoryArguments
        {
            get { return checkMandatoryArguments; }
            set { checkMandatoryArguments = value; }
        }

        /// <summary>
        /// When set to true, arguments are certified (using set of <see cref="Certifications"/>) after parsing. 
        /// Default is true.
        /// </summary>
        public bool CheckArgumentCertifications
        {
            get { return checkArgumentCertifications; }
            set { checkArgumentCertifications = value; }
        }

        /// <summary>
        /// When set to true (default) <see cref="SwitchArgument">switch arguments</see> can be grouped on the command line. 
        /// (e.g. -a -b -c can be written as -abc). When set to false and such a group is found, <see cref="CommandLineFormatException"/> is thrown.
        /// </summary>
        public bool AllowShortSwitchGrouping
        {
            get { return allowShortSwitchGrouping; }
            set { allowShortSwitchGrouping = value; }
        }

        /// <summary>
        /// Fills lookup dictionaries with arguments names and aliases 
        /// </summary>
        private void InitializeArgumentLookupDictionaries()
        {
            shortNameLookup = new Dictionary<char, Argument>();
            longNameLookup = new Dictionary<string, Argument>();
            foreach (Argument argument in arguments)
            {
                if (argument.ShortName != ' ')
                {
                    shortNameLookup.Add(argument.ShortName, argument);
                }
                //if (argument.shortAliases != null)
                //{
                foreach (char aliasChar in argument.ShortAliases)
                {
                    shortNameLookup.Add(aliasChar, argument);
                }
                if (!String.IsNullOrEmpty(argument.LongName))
                {
                    longNameLookup.Add(argument.LongName, argument);
                }
                //}
                //if (argument.longAliases != null)
                //{
                foreach (string aliasString in argument.LongAliases)
                {
                    longNameLookup.Add(aliasString, argument);
                } 
                //}
            }

        }

        /// <summary>
        /// Resolves arguments from the command line and calls <see cref="Argument.Parse"/> on each argument. 
        /// Additional arguments are stored in <see cref="AdditionalArguments"/> if <see cref="AcceptAdditionalArguments"/> is set to true. 
        /// </summary>
        /// <exception cref="CommandLineFormatException">Command line arguments are not in correct format</exception>
        /// <param name="args">Command line arguments</param>
        public void ParseCommandLine(string[] args)
        {
            arguments.ForEach(delegate(Argument a) { a.Init(); });
            List<string> args_list = new List<string>(args);
            InitializeArgumentLookupDictionaries();
            ExpandShortSwitches(args_list);
            additionalArguments = new string[0];

            if (args.Length > 0)
            {
                this.args = args;    
                int argIndex;
                for (argIndex = 0; argIndex < args_list.Count;)
                {
                    string curArg = args_list[argIndex];
                    Argument argument = ParseArgument(curArg);
                    if (argument == null)
                        break;

                    argument.Parse(args_list, ref argIndex);
                    argument.UpdateBoundObject();
                }

                ParseAdditionalArguments(args_list, argIndex);   
            }
            else if (ShowUsageOnEmptyCommandline)
                ShowUsage();

            PerformMandatoryArgumentsCheck();
            PerformCertificationCheck();
        }

        /// <summary>
        /// Searches <paramref name="parsingTarget"/> for fields with 
        /// <see cref="ArgumentAttribute">ArgumentAttributes</see> or some of its descendats. Adds new argument
        /// for each such a field and defines binding of the argument to the field. 
        /// Also adds <see cref="ArgumentCertification"/> object to <see cref="Certifications"/> collection 
        /// for each <see cref="ArgumentCertificationAttribute"/> of <paramref name="parsingTarget"/>.
        /// </summary>
        /// <seealso cref="Argument.Bind"/>
        /// <param name="parsingTarget">object where you with some ArgumentAttributes</param>
        public void ExtractArgumentAttributes(object parsingTarget)
        {
            Type targetType = parsingTarget.GetType();

            MemberInfo[] fields = targetType.GetFields();

            MemberInfo[] properties = targetType.GetProperties();

            List<MemberInfo> fieldAndProps = new List<MemberInfo>(fields);
            fieldAndProps.AddRange(properties);

            foreach (MemberInfo info in fieldAndProps)
            {
                object[] attrs = info.GetCustomAttributes(typeof(ArgumentAttribute), true);

                if (attrs.Length == 1)
                {
                    this.Arguments.Add((attrs[0] as ArgumentAttribute).Argument);
                    (attrs[0] as ArgumentAttribute).Argument.Bind = 
                        new FieldArgumentBind(parsingTarget, info.Name);
                }
            }

            object[] type_attrs = targetType.GetCustomAttributes(typeof(ArgumentCertificationAttribute), true);
            foreach (object certification_attr in type_attrs)
            {
                this.Certifications.Add((certification_attr as ArgumentCertificationAttribute).Certification);
            }
        }



        /// <summary>
        /// Parses one argument on the command line, lookups argument in <see cref="Arguments"/> using 
        /// lookup dictionaries.
        /// </summary>
        /// <param name="curArg">argument string (including '-' or '--' prefixes)</param>
        /// <returns>Look-uped Argument class</returns>
        /// <exception cref="CommandLineFormatException">Command line is in the wrong format</exception>
        /// <exception cref="UnknownArgumentException">Unknown argument found.</exception>
        private Argument ParseArgument(string curArg)
        {
            if (curArg[0] == '-')
            {
                string argName;
                if (curArg.Length > 1)
                {
                    if (curArg[1] == '-')
                    {
                        //long name
                        argName = curArg.Substring(2);
                        if (argName.Length == 1)
                        {
                            throw new CommandLineFormatException(String.Format(Messages.EXC_FORMAT_SHORTNAME_PREFIX, argName));
                        }

                    }
                    else
                    {
                        //short name
                        argName = curArg.Substring(1);
                        if (argName.Length != 1)
                        {
                            throw new CommandLineFormatException(
                                String.Format(Messages.EXC_FORMAT_LONGNAME_PREFIX, argName));
                        }
                    }
                    Argument argument = LookupArgument(argName);
                    if (argument != null) return argument;
                    else throw new UnknownArgumentException(string.Format(Messages.EXC_ARG_UNKNOWN, argName), argName);
                }
                else
                {
                    throw new CommandLineFormatException(Messages.EXC_FORMAT_SINGLEHYPHEN);
                }

            }
            else
                /*
                 * curArg does not start with '-' character and therefore it is considered additional argument.
                 * Argument parsing ends here.
                 */
                return null;
        }

        /// <summary>
        /// Checks whether or non-optional arguments were defined on the command line. 
        /// </summary>
        /// <exception cref="MandatoryArgumentNotSetException"><see cref="Argument.Optional">Non-optional</see> argument not defined.</exception>
        /// <seealso cref="CheckMandatoryArguments"/>, <seealso cref="Argument.Optional"/>
        private void PerformMandatoryArgumentsCheck()
        {
            arguments.ForEach(delegate (Argument arg)
                                  {
                                      if (!arg.Optional && !arg.Parsed)
                                          throw new MandatoryArgumentNotSetException("Argument {0} is not marked as optional and was not found on the commad line.", arg.Name);
                                  });                         
        }

        private void PerformCertificationCheck()
        {
            certifications.ForEach(delegate (ArgumentCertification certification)
                                       {
                                           certification.Certify(this);
                                       });
        }

        /// <summary>
        /// Parses the rest of the command line for additional arguments
        /// </summary>
        /// <param name="args_list">list of thearguments</param>
        /// <param name="i">index of the first additional argument in <paramref name="args_list"/></param>
        /// <exception cref="CommandLineFormatException">Additional arguments found, but they are 
        /// not <see cref="AcceptAdditionalArguments"> accepted</see></exception>
        private void ParseAdditionalArguments(List<string> args_list, int i)
        {
            if (acceptAdditionalArguments)
            {
                additionalArguments = new string[args_list.Count - i];
                if (i < args_list.Count)
                {
                    Array.Copy(args_list.ToArray(), i, additionalArguments, 0, args_list.Count - i);
                }
            }
            else
            {
                throw new CommandLineFormatException(
                    @"Additional arguments found and parser does not accept additional arguments. Set AcceptAdditionalArguments to true if you want to accept them. ");
            }
        }


        /// <summary>
        /// If <see cref="AllowShortSwitchGrouping"/> is set to true,  each group of switch arguments (e. g. -abcd) 
        /// is expanded into full format (-a -b -c -d) in the list.
        /// </summary>
        /// <exception cref="CommandLineFormatException">Argument of type differnt from SwitchArgument found in one of the groups. </exception>
        /// <param name="args_list">List of arguments</param>
        /// <exception cref="CommandLineFormatException">Arguments that are not <see cref="SwitchArgument">switches</see> found 
        /// in a group.</exception>
        /// <seealso cref="AllowShortSwitchGrouping"/>
        private void ExpandShortSwitches(IList<string> args_list)
        {
            if (allowShortSwitchGrouping)
            {
                for (int i = 0; i < args_list.Count; i++)
                {
                    string arg = args_list[i];
                    if (arg.Length > 2)
                    {
                        if (arg[0] == '-' && arg[1] != '-')
                        {
                            args_list.RemoveAt(i);
                            //arg ~ -xyz
                            foreach (char c in arg.Substring(1))
                            {
                                if (shortNameLookup.ContainsKey(c) && !(shortNameLookup[c] is SwitchArgument))
                                {
                                    throw new CommandLineFormatException(
                                        string.Format(Messages.EXC_BAD_ARG_IN_GROUP, c));
                                }

                                args_list.Insert(i, "-" + c);
                                i++;
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Returns argument of given name
        /// </summary>
        /// <param name="argName">Name of the argument (<see cref="Argument.ShortName"/>, <see cref="Argument.LongName"/>, or alias)</param>
        /// <returns>Found argument or null when argument is not present</returns>
        public Argument LookupArgument(string argName)
        {
            if (argName.Length == 1)
            {
                if (shortNameLookup.ContainsKey(argName[0]))
                {
                    return shortNameLookup[argName[0]];
                }
            }
            else
            {
                if (longNameLookup.ContainsKey(argName))
                {
                    return longNameLookup[argName];
                }
            }
            // argument not found anywhere
            return null;
        }

        /// <summary>
        /// Prints arguments information and usage information to the console. 
        /// </summary>
        public void ShowUsage()
        {
            Console.WriteLine(Messages.MSG_USAGE);

            foreach (Argument argument in arguments)
            {
                Console.Write("\t");
                bool comma = false;
                if (argument.ShortName != ' ')
                {
                    Console.Write("-" + argument.ShortName);
                    comma = true;
                }
                foreach (char c in argument.ShortAliases)
                {
                    if (comma)
                        Console.WriteLine(", ");
                    Console.Write("-" + c);
                    comma = true;
                }
                if (!String.IsNullOrEmpty(argument.LongName))
                {
                    if (comma)
                        Console.Write(", ");
                    Console.Write("--" + argument.LongName);
                    comma = true;
                }
                foreach (string str in argument.LongAliases)
                {
                    if (comma)
                        Console.Write(", ");
                    Console.Write("--" + str);
                    comma = true; 
                }

                if (argument.Optional)
                    Console.Write(Messages.MSG_OPTIONAL);
                Console.WriteLine("... {0}", argument.Description);
                if (!String.IsNullOrEmpty(argument.FullDescription))
                {
                    Console.WriteLine();
                    Console.WriteLine(argument.FullDescription);
                }
                Console.WriteLine();
            }
            
            if (Certifications.Count > 0)
            {
                Console.WriteLine(Messages.CERT_REMARKS);
                foreach (ArgumentCertification certification in Certifications)
                {
                    Console.WriteLine("\t" + certification.GetDescription);    
                }
                Console.WriteLine();
            }
        }

        /// <summary>
        /// Prints values of parsed arguments. Can be used for debugging. 
        /// </summary>
        public void ShowParsedArguments()
        {
            Console.WriteLine(Messages.MSG_PARSING_RESULTS);
            Console.WriteLine("\t" + Messages.MSG_COMMAND_LINE);    
            foreach (string arg in args)
            {
                Console.Write(arg);
                Console.Write(" ");
            }

            Console.WriteLine();
            Console.WriteLine();
            Console.WriteLine("\t" + Messages.MSG_PARSED_ARGUMENTS);
            foreach (Argument argument in arguments)
            {
                if (argument.Parsed)
                    argument.PrintValueInfo();
            }
            Console.WriteLine();
            if (acceptAdditionalArguments)
            {
                Console.WriteLine("\t" + Messages.MSG_ADDITIONAL_ARGUMENTS);

                foreach (string simpleArgument in AdditionalArguments)
                {
                    Console.Write(simpleArgument + " ");
                }

                Console.WriteLine();   
                Console.WriteLine();
            }
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Software Developer (Junior)
Czech Republic Czech Republic
I am a computer science student at Charles University in Prague. I work as a developer of CRM and informational systems.

Comments and Discussions