Giter Site home page Giter Site logo

haath / commander.net Goto Github PK

View Code? Open in Web Editor NEW
16.0 3.0 2.0 99 KB

C# command-line argument parsing library, supporting deserialization, commands and help text generation.

License: MIT License

C# 100.00%
positional-parameters argument-parsing serialization nuget regular-expression cli

commander.net's Introduction

Commander.NET

C# command-line argument parsing and serialization via attributes. Inspired by JCommander.

If you would like to expand this library, please include examples in your PR or issue, of widely-used CLI tools that support your suggested practice or format.

Installation

Install from NuGet.

Usage

First you need to create a non-abstract class, with a public parameterless constructor, which will describe the parameters.

using Commander.NET.Attributes;

class Options
{
	[Parameter("-i", Description = "The ID")]
	public int ID = 42;

	[Parameter("-n", "--name")]
	public string Name;

	[Parameter("-h", "--help", Description = "Print this message and exit.")]
	public bool Help;
}

Then simply parse the command line arguments

string[] args = { "-i", "123", "--name", "john"};

CommanderParser<Options> parser = new CommanderParser<Options>();

Options options = parser.Add(args)
                        .Parse();

Required parameters

You may specify which of the parameters are required by setting the Required property of the attributes.

[Parameter("-r", Required = Required.Yes)]
public int RequiredParameter;

[Parameter("--lorem", Required = Required.No)]
public bool NotRequiredParameter;

[Parameter("--ipsum")]	// Required.Default
public string ThisOneIsRequired;

[Parameter("--dolor")]	// Required.Default
public string ThisOneIsNotRequired = "Because it has a default value";

As seen in the example above, the Required enum can have one of three values:

  • Required.Yes
    • This parameter is required and omitting it will throw a ParameterMissingException during parsing.
  • Required.No
    • This parameter is optional. If it is not set, the relevant field will maintain its default value.
  • Required.Default
    • The parameter will be considered to be required only if the default value of the relevant field is null.

Generate a usage summary

CommanderParser<Options> parser = new CommanderParser<Options>();

Console.WriteLine(parser.Usage());

Will print out the following.

Usage: <exe> [options]
Options:
      -h, --help
        Print this message and exit.
        Default: False
      -i
        The ID
        Default: 42
    * -n, --name

An asterisk prefix implies that the option is required.

Exceptions

using Commander.NET.Exceptions;

try
{
	Options opts = CommanderParser.Parse<Options>(args);
}
catch (ParameterMissingException ex)
{
	// A required parameter was missing
	Console.WriteLine("Missing parameter: " + ex.ParameterName);
}
catch (ParameterFormatException ex)
{
	/*
	*	A string-parsing method raised a FormatException
	*	ex.ParameterName
	*	ex.Value
	*	ex.RequiredType
	*/
	Console.WriteLine(ex.Message);
}

Positional parameters

You can define positional parameters using the PositionalParameter attribute.

[PositionalParameter(0, "operation", Description = "The operation to perform.")]
public string Operation;

[PositionalParameter(1, "target", Description = "The host to connect to.")]
public string Host = "127.0.0.1";

When printing out the usage, positional parameters will be shown like the example below. However, they can be passed in any order in relation to the options.

Usage: Tests.exe [options] <operation> [target]
    operation: The operation to perform.
    target: The host to connect to.
Options:
    ...

Whether a parameter is required or not is defined exactly as shown above. This leaves room for error though, since for example the first positional parameter can be specified as optional and the second as required, which would leave you with a counter-intuitive interface design. The library does not currently enforce a good practice here - until a proper method of doing so is decided - so this is entirely up to the user.

In general, any argument passed that is neither the name of the parameter, nor the value of a non-boolean named parameter will be considered a positional parameter and assigned to the appropriate index.

You may also get all the positional parameters that were passed, using the `PositionalParameterList' attribute.

[PositionalParameterList]
public string[] Parameters;

Key-Value separators

By default, the parser will only consider key-value pairs that are separated by a space. You can change that by setting the Separators flags of the parser. The example below will parse both the --key value format and the --key=value format.

CommanderParser<Options> parser = new CommanderParser<Options>();
Options options = parser.Add(args)
                        .Separators(Separators.Space | Separators.Equals)
                        .Parse();

Currently available separators:

  • Separators.Space
  • Separators.Equals
  • Separators.Colon
  • Separators.All

Value validation

This section lists the ways with which you can validate, format or even convert the values that are passed to your parameters. Each individual value, goes through the following steps in that order:

  1. Regex validation
  2. Method validation
  3. Formatting

Regular Expression validation

You can validate the values of a parameter through a regular expression, by setting the Regex property.

[Parameter("-n", "--name", Regex = "^john|mary$")]
public string Name;

If the regex match failes, a ParameterMatchException is thrown, as it shown below.

string[] args = { "-n", "james" };

try
{
	Options opts = CommanderParser.Parse<Options>(args);
	Console.WriteLine(opts.ToString());
}
catch (ParameterMatchException ex)
{
	// Parameter -n: value "james" did not match the regular expression "^john|mary$"
	Console.WriteLine(ex.Message);
}

Method validation

You can also use your own validation methods for values that are passed to a specific parameter by implementing the IParameterValidator interface.

If the Validate() method returns false, then a ParameterValidationException is thrown by the parser. Alternatively, you can use this method to throw your own exceptions, and they will rise to where you called you called the parser from.

The following basic example makes sure that the argument passed to the Age parameter is a positive integer.

using Commander.NET.Interfaces;

class PositiveInteger : IParameterValidator
{
	bool IParameterValidator.Validate(string name, string value)
	{
		int intVal;
		return int.TryParse(value, out intVal) && intVal > 0;
	}
}
[Parameter("-a", "--age", ValidateWith = typeof(PositiveInteger))]
public int Age;

Value formatting

Similar to method validation, you may want to declare your own methods for formatting - or event converting - certain parameter values. You may do this by implementing the IParameterFormatter interface.

The object returned by the Format() method will be directly set to the parameter with no other formatting or type conversion.

The following basic example, converts whatever value is passed to the --ascii parameter into a byte array.

using Commander.NET.Interfaces;

class StringToBytes : IParameterFormatter
{
	object IParameterFormatter.Format(string name, string value)
	{
		return Encoding.ASCII.GetBytes(value);
	}
}
[Parameter("--ascii", FormatWith = typeof(StringToBytes))]
public byte[] ascii;

Commands

Commands are usually a very important method for separating the different functionalities of a CLI program. Consider the following example, based on the popular git tool.

class Git
{
	[Command("commit")]
	public Commit Commit;

	[Command("push")]
	public Push Push;
}

class Commit
{
	[Parameter("-m")]
	public string Message;
}

class Push
{
	[PositionalParameter(0, "remote")]
	public string Remote;

	[PositionalParameter(1, "branch")]
	public string Branch;
}
Git git = new CommanderParser<Git>()
                         .Parse(args);

if (git.Commit != null)
{
	Console.WriteLine("Commiting: " + git.Commit.Message);
}
else if (git.Push != null)
{
	Console.WriteLine("Pushing to: " + git.Push.Remote + " " + git.Push.Branch);
}
  • If at least one command is specified, but no command name is found in the arguments, the parser will raise a CommandMissingException
  • Any arguments passed after the name of the command, are parsed and serialized into that command.

Command handlers

In the example above, to find out which command was issued we were comparing each command variable with null to find out which had been instantiated. Alternatively though, you can avoid any manual checks by using custom handlers for commands.

When the parsing and serialization of the arguments of an object has completed, the type of the command variable is checked for the ICommand interface. If the command type implements said interface, then the ICommand.Execute() is called on that object.

The object parent argument that is passed to this method, is the - already initialized - object, through which this command was called. In our example, this argument will contain the Git object.

class Commit : ICommand
{
	[Parameter("-m")]
	public string Message;

	void ICommand.Execute(object parent)
	{
		// The "commit" command was issued
	}
}

After the above callback, the base class is then checked for methods with the CommandHandler attribute. If any such method in the class has exactly one parameter and that parameter is of the same type as the command that is being executed, the method will be invoked.

class Git
{
	[Command("commit")]
	public Commit Commit;

	[Command("push")]
	public Push Push;

	[CommandHandler]
	public void PushCommand(Push push)
	{
		// The "push" command was issued
	}

	[CommandHandler]
	public void CommitCommand(Commit commit)
	{
		// The "commit" command was issued
	}
}

Private fields

By default, private and static fields, properties and methods are visible to the parser, even if their respective accessors are not public. You can change this behavior by manually passing the System.Reflection.BindingFlags flags to the parser.

CommanderParser<Options> parser = new CommanderParser<Options>();

parser.Bindings(BindingFlags.Public | BindingFlags.Instance);

//TODO

  • Reverse positional indexing
  • Specifying possible values. (f.e bacon|onions|tomatoes) Will be doable by default with regex, but enum support will be nice.
  • Value type validation within the library with custom errors
  • Recursive help flag for commands
  • Support for appending the same parameter into collections
  • Better support for mixing command parameters with parent parameters

commander.net's People

Contributors

haath avatar

Stargazers

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

Watchers

 avatar  avatar  avatar

commander.net's Issues

ParameterMissingException when parsing equal-separated key-value pair args

Commander.NET throws ParameterMissingException when parsing equal-separated key-value pair args.

Steps to Reproduce

Step 1: NginxRunConfig.cs

namespace NginxGUI
{
    internal class NginxRunConfig
    {
        [Parameter("--command", Description = "Path of the nginx command")]
        public string Command;

        [Parameter("--prefix")]
        public string Prefix;

        [Parameter("--pid-path")]
        public string PidPath;

        [Parameter("--error-log-path")]
        public string ErrorLogPath;
    }
}

Step 2: Program.cs

namespace NginxGUI
{
    internal static class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            NginxRunConfig rc = null;
            CommanderParser<NginxRunConfig> parser = new CommanderParser<NginxRunConfig>();
            parser.Add(args)
                .Separators(Separators.Equals)
                .Bindings(BindingFlags.Public | BindingFlags.Instance);
            try
            {
                rc = parser.Parse();
            }
            catch (ParameterMissingException ex)
            {
                Console.WriteLine("Missing parameter: " + ex.ParameterName);
            }
            catch (ParameterFormatException ex)
            {
                Console.WriteLine(ex.Message);
            }
            // ...
        }
    }
}

Step 3: Debug CLI Arguments

--command=C:\Library\nginx-1.22.0\nginx.exe --prefix=C:\Library\nginx-1.22.0 --pid-path=logs\nginx.pid --error-log-path=logs\error.log

Expected Result

As [doucmentation](https://github.com/gmantaos/Commander.NET#key-value-separators) says

Currently available separators:

  • Separators.Space
  • Separators.Equals
  • Separators.Colon
  • Separators.All

since separator flags of key-value pair were set to Separators.Equals, so Arg --command=C:\Library\nginx-1.22.0\nginx.exe should be accepted by the parser.

Actual Result

A ParameterFormatException occurred:

Missing parameter: --command

If I change separators to Separators.Space and change CLI Arguments to space-separated key-value pair, like--command C:\Library\nginx-1.22.0\nginx.exe, then Args can to parsed successfully.

Additional Information

  • Commander.NET 1.1.11
  • .Net Framework 4.8
  • Windows 10.0.19044

Have I missed something?

Can't handle "&" in value part

Hi G. Mantaos,

your Commander.NET can't handle "&" (maybe spaces as well) in the parameter value part.

For example:

MyApp -p abc&def

Turns into:

MyApp -p abc & def

This way def is recognized as a command. 0o

Okay, so i tried to put the value in quotes (as usual in console applications):

MyApp -p "abc&def"

But then the quotes will not be removed.

So I have to deal with them. :-/

...do I really have to??? ...or is there a secret parser option?

The quotes only should make sure that the complete value will be read (e.g. a path with spaces), but they should not be included i think, don't you?

Thanks & best regards,
Chris

Boolean arguments on Parent class are not set

I have the following setup:

public class CommandArgs
{
    [Parameter("verbose", Required = Required.No)
    public bool Verbose { get; set; }

    [Command("go")]
    public GoCommand GoCmd { get; set; }
}

public class GoCommand : ICommand
{
     [Parameter("x")]
     public string X { get; set; }

     public void Execute(object parent)
     {
         var globalArgs = (CommandArgs)parent;
         
         Console.Writeline(globalArgs.Verbose); // <-- always false
     }
}

Expected: the value of the CommandArgs.Verbose should be set to true when --verbose is supplied on the command-line.
Actual: the value is ignored.

If I change this code and move the Verbose flag to the GoCommand class, it does get picked up correctly.

I'm using .NET Core 5 in Visual Studio 2022.

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.