Code Monkey home page Code Monkey logo

utility.commandline.arguments's Introduction

Utility.CommandLine.Arguments

Build status Build Status codecov Quality Gate Status NuGet version License: MIT

A C# .NET Class Library containing tools for parsing the command line arguments of console applications.

Why?

I needed a solution for parsing command line arguments and didn't like the existing options.

Installation

Install from the NuGet gallery GUI or with the Package Manager Console using the following command:

Install-Package Utility.CommandLine.Arguments

The code is also designed to be incorporated into your project as a single source file (Arguments.cs).

Quick Start

Create private static properties in the class containing your Main() and mark them with the Argument attribute, assigning short and long names. Invoke the Arguments.Populate() method within Main(), then implement the rest of your logic.

The library will populate your properties with the values specified in the command line arguments.

internal class Program
{
    // [Argument(short name (char), long name (string), help text)]
    [Argument('b', "myBool", "a boolean value")]
    private static bool Bool { get; set; }

    [Argument('f', "someFloat")]
    private static double Double { get; set; }

    [Argument('i', "anInteger")]
    private static int Int { get; set; }

    [Argument('s', "aString")]
    private static string[] String { get; set; }

    [Operands]
    private static string[] Operands { get; set; }

    private static void Main(string[] args)
    {
        Arguments.Populate();

        Console.WriteLine("Bool: " + Bool);
        Console.WriteLine("Int: " + Int);
        Console.WriteLine("Double: " + Double);

        foreach (string s in String)
        {
            Console.WriteLine("String: " + s);
        }

        foreach (string operand in Operands) 
        {
            Console.WriteLine("\r\n Operand:" + operand);
        }
    }
}

Grammar

The grammar supported by this library is designed to follow the guidelines set forth in the publication The Open Group Base Specifications Issue 7, specifically the content of Chapter 12, Utility Conventions, located here.

Each argument is treated as a key-value pair, regardless of whether a value is present. The general format is as follows:

<-|--|/>argument-name<=|:| >["|']value['|"] [--] [operand] ... [operand]

The key-value pair may begin with a single hyphen, a pair of hyphen, or a forward slash. Single and double dashes indicate the use of short or long names, respectively, which are covered below. The forward slash may represent either a sort or long name but does not allow for the grouping of parameterless arguments (e.g. /abc is not equivalent to -abc, but rather --abc).

The argument name may be a single character when using short names, or any alphanumeric sequence not including spaces if using long names.

The value delimiter may be an equals sign, a colon, or a space.

Values may be any alphanumeric sequence, however if a value contains a space it must be enclosed in either single or double quotes.

Any word, or phrase enclosed in single or double quotes, will be parsed as an operand. The official specification requires operands to appear last, however this library will parse them in any position.

A double-hyphen -- not enclosed in single or double quotes and appearing with whitespace on either side designates the end of the argument list and beginning of the explicit operand list. Anything appearing after this delimiter is treated as an operand, even if it begins with a hyphen, double-hyphen or forward slash.

Short Names

Short names consist of a single character, and arguments without parameters may be grouped. A grouping may be terminated with a single argument containing a parameter. Arguments using short names must be preceded by a single dash.

Examples

Single argument with a parameter: -a foo

Key Value
a foo

Two parameterless arguments: -ab

Key Value
a
b

Three arguments; two parameterless followed by a third argument with a parameter: -abc bar

Key Value
a
b
c bar

Long Names

Long names can consist of any alphanumeric string not containing a space. Arguments using long names must be preceded by two dashes.

Examples

Single argument with a parameter: --foo bar

Key Value
foo bar

Two parameterless arguments: --foo --bar

Key Value
foo
bar

Two arguments with parameters: --foo bar --hello world

Key Value
foo bar
hello world

Mixed Naming

Any combination of short and long names are supported.

Example

-abc foo --hello world /new="slashes are ok too"

Key Value
a
b
c foo
hello world
new slashes are ok too

Multiple Values

Arguments can accept multiple values, and when parsed a List<object> is returned if more than one value is specified. When using the Populate() method the underlying property for an argument accepting multiple values must be an array or List, otherwise an InvalidCastException is thrown.

Example

--list 1 --list 2 --list 3

Key Value
list 1,2,3

Operands

Any text in the string that doesn't match the argument-value format is considered an operand. Any text which appears after a double-hyphen -- not enclosed in single or double quotes and with spaces on either side is treated as an operand regardless of whether it matches the argument-value format.

Example

-a foo bar "hello world" -b -- -explicit operand

Key Value
a foo
b

Operands

  1. bar
  2. "hello world"
  3. -explicit
  4. operand

Parsing

Argument key-value pairs can be parsed from any string using the Parse(string) method. This method returns a Dictionary<string, object> containing all argument-value pairs.

If the string parameter is omitted, the value of Environment.CommandLine is used.

Note that passing the args variable, or the result of String.Join() on args, will prevent the library from property handling quoted strings. There are generally very few instance in which Environment.CommandLine should not be used.

Example

Dictionary<string, object> args = Arguments.Parse("-ab --foo bar");

The example above would result in a dictionary args containing:

Key Value
a
b
foo bar

Note that boolean values should be checked with Dictionary.ContainsKey("name"); the result will indicate whether the argument was encountered in the command line arguments. All other values are retrieved with Dictionary["key"].

Populating

The Populate() method uses reflection to populate private static properties in the target Type with the argument values matching properties marked with the Argument attribute.

The list of operands is placed into a single property marked with the Operands attribute. This property must be of type string[] or List<string>.

Creating Target Properties

Use the Argument attribute to designate properties to be populated. The constructor of Argument accepts a char, a string, and an additional string, representing the short and long names of the argument, and help text, respectively.

Note that the name of the property doesn't matter; only the attribute values are used to match an argument key to a property.

The Type of the property does matter; the code attempts to convert argument values from string to the specified Type, and if the conversion fails an ArgumentException is thrown.

Properties can accept lists of parameters, as long as they are backed by an array or List<>. Specifying multiple parameters for an argument backed by an atomic Type (e.g. not an array or List) will result in an InvalidCastException.

The Operands property accepts no parameters. If the property type is not string[] or List<string>, an InvalidCastException will be thrown.

Examples

[Argument('f', "foo", "some help text")]
private static string Foo { get; set; }

[Argument('n', "number")]
private static integer MyNumber { get; set; }

[Argument('b', "bool")]
private static bool TrueOrFalse { get; set; }

[Argument('l', "list")]
private static string[] List { get; set; }

[Operands]
private static List<string> Operands { get; set; }

Given the argument string -bf "bar" --number=5 --list 1 --list 2, the resulting property values would be as follows:

Property Value
Foo bar
MyNumber 5
TrueOrFalse true
List 1,2

Obtaining Help

A collection of ArgumentHelp containing the short and long names and help text for each argument property can be fetched with GetArgumentHelp():

private static void ShowHelp()
{
    var helpAttributes = Arguments.GetArgumentInfo(typeof(Program));

    var maxLen = helpAttributes.Select(a => a.Property.Name).OrderByDescending(s => s.Length).FirstOrDefault()!.Length;

    Console.WriteLine($"Short\tLong\t{"Type".PadRight(maxLen)}\tFunction");
    Console.WriteLine($"-----\t----\t{"----".PadRight(maxLen)}\t--------");

    foreach (var item in helpAttributes)
    {
        var result = item.ShortName + "\t" + item.LongName + "\t" + item.Property.PropertyType.ToColloquialString().PadRight(maxLen) + "\t" + item.HelpText;
        Console.WriteLine(result);
    }
}

utility.commandline.arguments's People

Contributors

branc116 avatar crahungit avatar dependabot[bot] avatar jpdillingham avatar juliogold avatar matthiasjentsch avatar montgomerybc avatar wburklund avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

utility.commandline.arguments's Issues

Breaking changes for v4.0.0

  • Argument values beginning with a forward slash (/) must now be enclosed in quotes. Unquoted values are interpreted as additional arguments.

Breaking changes for v3.0.0

  • GetArgumentHelp() and ArgumentHelp are deprecated in favor of GetArgumentInfo() and ArgumentInfo. The new functionality retains everything and adds Property to return the backing PropertyInfo.
  • The ArgumentDictionary property of Arguments now combines repeated short and long argument names into a single key of the name which appears first, but only if a Type is supplied to Parse().
    • For example, if 'a' and "abc" were the short and long names for an argument, and if the backing Type for this argument was a collection, and "-a foo --abc bar" were supplied on the command line, the resulting dictionary entry would have key "a" and values "foo, bar". If the first argument was "abc" instead of "a", the dictionary key would be "abc".
    • For repeated arguments not backed by a collection Type, the value of the dictionary entry will be the last value appearing in the command line string.
    • If a Type is not supplied to Parse(), all arguments are assumed to be single value (non-collection) Types, and combining multiple arguments of the same name will result in only the last value being retained.
  • The ArgumentList property has been added to Arguments to provide the full, individual list of parsed arguments with order preserved.

Subcommad support

Very crisp package. Thanks.

Wondering if there is a straight forward way to support subcommands as in:

git clone
git pull
git push

I tried to use the Operand feature but it appears that:

myprog begin start end -f 3.4

does not recognize "begin" as an operand.

However changing the usage to:

myprog -f 3.4 begin start end

does recognize begin as an operand.

With .NET Core the executable is identified as an operand

The proper way to execute a .NET Core console application with application arguments is as so:

dotnet run [dotnet args] -- [application args]

The code should begin parsing after the first explicit operand --, but somehow the name of the executable file is being added as an application operand. Investigate and suppress this.

Improper value propagation when a boolean flag is followed by an operand

        [Argument('d', "duplicates")]
        private static bool AllowDuplicates { get; set; }

-a text

Results in an ArgumentException at runtime. The Parse() method correctly treats -a text as an argument pair because it is agnostic about the type of a, however when propagating the argument dictionary to properties the code should check the type and split this pair into a boolean and an operand.

Argument values starting with non-word-characters are treated as operands

If an Argument's value start with (e.g.) a period or backslash, like a path would, the value is not parsed correctly.

Example:

using System;
using System.Linq;
using Utility.CommandLine;

namespace Foo
{
    class Program
    {
        [Argument('f', "folder")]
        private static string Folder { get; set; }

        [Operands]
        private static string[] Operands { get; set; }

        static void Main(string[] args)
        {
            Arguments.Populate();

            Console.WriteLine($"Folder='{Folder}'");
            Console.WriteLine($"Operands='{String.Join(';', Operands.Skip(1))}'");
        }
    }
}
PM> .\foo.exe
Folder=''
Operands=''

PM> .\foo.exe -f
Folder=''
Operands=''

PM> .\foo.exe -f a.txt
Folder='a.txt'
Operands=''

PM> .\foo.exe -f .\a.txt
Folder=''
Operands='.\a.txt'

PM> .\foo.exe -f ..\a.txt
Folder=''
Operands='..\a.txt'

PM> .\foo.exe -f \path\to\a.txt
Folder=''
Operands='\path\to\a.txt'

PM> .\foo.exe -f \\server\path\to\a.txt
Folder=''
Operands='\\server\path\to\a.txt'

.net core support

Gonna take a whack at this by upgrading the lib to .net standard and creating a core and non core app to prove interop.

Breaking changes for v6.0.0

The existing Parse() has been replaced with three overloads.

Old:

public static Arguments Parse(string commandLineString = default(string), Type type = null, [CallerMemberName] string caller = default(string)) {}

New:

public static Arguments Parse(Action<ArgumentParseOptions> configure = null) {}
public static Arguments Parse(string commandLineString, Action<ArgumentParseOptions> configure = null) {}
public static Arguments Parse(string commandLineString, ArgumentParseOptions options) {}

The caller argument wasn't doing anything in the first place, and the type argument is now specified through the TargetType property of ArgumentParseOptions.

ArgumentParseOptions contains the following properties:

  • TargetType -- same functionality as the previous type parameter
  • CombineAllMultiples -- combines repeated argument values into a list
  • CombinableArguments -- if CombineAllMultiples is false, combines the specified arguments into a list
    If TargetType is supplied and repeated arguments match a property in the type, the behavior remains the same; if the backing type of the property is a collection, values are combined, otherwise they are overwritten.

If TargetType is not supplied, or repeated arguments don't match a property in the type, the behavior depends on the other options for multiples, either everything or just those specified.

Properties backing arguments with multiple values not populated properly if short and long argument names are specified

This issue will require a fairly significant rewrite of the Populate() method, as well as the GetArgumentProperties() method. The Parse() method should remain unchanged; it is designed to parse arguments as presented and has no concept of short or long argument names.

Arguments requiring multiple values are an edge case, and arguments requiring multiple values while also allowing the user to freely switch between short and long names in the same argument string are an extreme edge case.

I have no plans to implement this functionality at this time, however if someone would like to take it on I've included failing tests for array and List backed properties.

Failing Tests

TestClassWithListProperty

/// <summary>
///     Tests the <see cref="Utility.CommandLine.Arguments.Populate(Type, string)"/> method with an explicit string
///     containing multiple instances of a list-backed argument, and containing a change from short to long names and back.
/// </summary>
[Fact]
public void PopulateWithNameChange()
{
    Exception ex = Record.Exception(() => CommandLine.Arguments.Populate(GetType(), "-l one --list two -l three"));

    Assert.Null(ex);
    Assert.Equal(3, List.Count);
    Assert.Equal("one", List[0]);
    Assert.Equal("two", List[1]);
    Assert.Equal("three", List[2]);
}

TestClassWithArrayProperty

/// <summary>
///     Tests the <see cref="Utility.CommandLine.Arguments.Populate(Type, string)"/> method with an explicit string
///     containing multiple instances of an array-backed argument, and containing a change from short to long names and back.
/// </summary>
[Fact]
public void PopulateWithNameChange()
{
    Exception ex = Record.Exception(() => CommandLine.Arguments.Populate(GetType(), "-a one --array two -a three"));

    Assert.Null(ex);
    Assert.Equal(3, Array.Length);
    Assert.Equal("one", Array[0]);
    Assert.Equal("two", Array[1]);
    Assert.Equal("three", Array[2]);
}

Argument starting with tilde (~) are not captured and left blank

Version: 6.0.0

When passing an argument that begins with a tilde, the argument is not populated to the attributed variable. This would be a common case in non-windows environments to signal relative-to-user paths.

Ex: dotnet run -file "~/data/path/to/file"

Breaking changes for v5.0.0

  • Forward slash (/) as an argument delimiter is disabled by default due to cross-platform compatibility issues. Projects needing to retain the previous behavior should set Arguments.EnableForwardSlash = true prior to invoking Populate() or Parse().

Trying to use negative double value cause Exception

System.ArgumentException: 'Specified value '' for argument 'max' (expected type: System.Double). See inner exception for details.'
Inner exception:
FormatException: Input string was not in a correct format.

        [Argument('1', "max", "max")]
        private static double max { get; set; }

        [Argument('2', "max_minus", "max minus")]
        private static double max_minus { get; set; }

        [Argument('3', "min", "min")]
        private static double min { get; set; }

        [Argument('4', "min_minus", "min minus")]
        private static double min_minus { get; set; }

Support enums

Consider adding a case insensitive enum parse for arguments backed by enum typed properties.

Add support for default values

Currently, Populate() sets all argument properties to null prior to populating values from arguments. This prevents default values from being specified like so:

        /// <summary>
        ///     Gets or sets the value of the Int argument.
        /// </summary>
        [Argument('i', "integer", "Gets or sets the value of the Int argument.")]
        private static int Int { get; set; } = 42

It does this by invoking ClearProperties() as the first step, which sets the value of each argument property to null via reflection (which, as a separate issue, likely doesn't work as intended for non-nullable types).

A simple fix which avoids a breaking change to the api is to add an optional parameter to Populate() which controls whether ClearProperties() is invoked prior to population. This new parameter would be added to the various overloads as optional with the name clearExistingValues and defaulted to false.

Null reference error

In release 1.1.1 there is a null reference error. This was working in release 1.0.1.
The Method GetOperandsProperty fails if there are no Operands variables defined.

Arguments starting with non-Alpha characters followed by Forward slashes are dropped completely

I believe #55 introduced an additional bug where arguments that begin with a non-alpha character .~ followed by a forward slash / are not captured and are parsed as empty strings.

This does not change whether quoted or not.

Given an Argument like:

[Argument('f', "folder", "The folder location for some thing.")]
private static string FolderPath { get; set; }

These will fail to populate the FolderPath property:
dotnet someapp.dll -f "../../path/in/parent"
dotnet someapp.dll -f ../../path/in/parent
dotnet someapp.dll -f "~/path/from/root"
dotnet someapp.dll -f ~/path/from/root

Environment Details:
.NET version: dotnet core 2.2.207
Repro OS: macOS 10.14.6
PackageVersion: 3.0.1

Documentation oversight

I see it is possible to have arguments like
-a
with no value, but how does the code know the input is -a with no value?

Arguments using only long name?

I'm writing a program that uses a lot of arguments and I can't map a short argument name to each of them.

If I try to define multiple arguments like this

[Argument(' ', "mykey", "Description")]

the parser seems to only store the first argument. Is there something I'm missing or is this not implemented?

Remove support for unquoted forward slashes as values

Presently, something like foo /bar.txt is supported. This breaks when parsing /a /b /c into three boolean parameters, so support for this needs to be removed from the regular expression.

Following this change, any values beginning with a forward slash will need to be quoted.

Allow properties to be populated from environment variables

Configuration might look like this:

        [Argument('p', "password", enVar: "MYAPP_PASSWORD")]
        private static string Password { get; set; }

Alternatively a breaking change can be introduced to insert the enVar argument before helpText, which would be more natural.

Environment variable values would be populated only if the argument is not present in the command line arguments.

Parse() dos not capture multiple values for repeated arguments

Per the documentation, this is supposed to work, but it doesn't.

Here's a failing test:

        [Fact]
        public void ParseMultiples()
        {
            Dictionary<string, object> test = Arguments.Parse("--test 1 --test 2").ArgumentDictionary;

            Assert.NotEmpty(test);
            Assert.Single(test);
            Assert.Equal("1,2", test["test"]);
        }

This is working as expected when you consider this line. It needs to be changed to append additional values if the key is already present.

I'll have to think about whether this will cause any breaking changes.

Rename Extensions

Extensions is a common name, and will cause conflicts when added to source directly. Rename to ArgumentExtensions or similar.

Populate() does not work with .NET Core 2.x

The introduction of async Main() has changed the makeup of the stack, which the application relies on to find the target Type when one is not specified in Populate().

As a temporary workaround, use Populate(typeof(<class containing main()>))

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.