commandline icon indicating copy to clipboard operation
commandline copied to clipboard

Usage of noun and verb with further parameters

Open ImfeldC opened this issue 1 year ago • 1 comments

Hi, Is it possible to use CommandLineParser with noun and verb syntax? I would like to setup a command line tool, which supports different nouns (in CommandLineParser terminology, this would be a verb)

As example:

  • Myapp.exe tpm init -v ....
  • Myapp.exe tpm show -v -l ....
  • MyApp.exe dongle show -l .... In the example above I have two levels of verbs, in the first level the noun and in the second level the verb.

Is this possible out-of-the-box? If not, are there ways to implement this?

ImfeldC avatar Jul 22 '24 12:07 ImfeldC

hey @ImfeldC I have the same problem and haven't found an easy way to do this. One solution might be to add a custom attribute like this:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)]
public class NounAttribute : Attribute
{
    public readonly string Name;
    
    public NounAttribute(string nounName)
    {
        Name = nounName;
    }
}

[Noun("tpm")]
[Verb("init")]
public class TpmInitArgs
{
    // ...
}

[Noun("tpm")]
[Verb("show")]
public class TpmShowArgs
{
    // ...
}

[Noun("dongle")]
[Verb("show")]
public class DongleShowArgs
{
    // ...
}

And add an extension method that handles the attribute:

public static class ParserExtensions
{
    public static ParserResult<object> ParseArgumentsWithNoun<T1, T2, T3>(this Parser parser, string[] args)
    {
        if (args.Length < 1)
        {
            // just return whatever as it will fail; there is no way to create NotParsed as it's sealed and ctor is internal
            return parser.ParseArguments(args, []);
        }

        var types = new[] { typeof(T1), typeof(T2), typeof(T3) };
        var nounTypesLookup = types
            .SelectMany(t => t.GetTypeInfo().GetCustomAttributes<NounAttribute>().Select(attr => new {Type = t, Attribute = attr}))
            .ToLookup(x => x.Attribute.Name, x => new {x.Type, x.Attribute});

        if (!nounTypesLookup.Contains(args[0]))
        {
            // just return whatever as it will fail; there is no way to create NotParsed as it's sealed and ctor is internal
            return parser.ParseArguments(args.Skip(1), types);
        }

        return parser.ParseArguments(args.Skip(1).ToArray(), nounTypesLookup[args[0]].Select(x => x.Type).ToArray());
    }
}

public static class Program
{
    public static void Main(string[] args)
    {
        Parser.Default.ParseArgumentsWithNoun<TpmInitArgs, TpmShowArgs, DongleShowArgs>(args)
            .WithParsed<TpmInitArgs>(arg => { Console.WriteLine("TpmInitArgs"); })
            .WithParsed<TpmShowArgs>(arg => { Console.WriteLine("TpmShowArgs"); })
            .WithParsed<DongleShowArgs>(arg => { Console.WriteLine("DongleShowArgs"); });
    }
}

Although this solution would be a bit problematic:

  1. you will have to add an extension method accepting the same number of generic parameters as there are possible ways to run the application (all noun-verb pairs). It will quickly turn out that there will be actually a lot of those actions and parameter types to parse.
  2. the built-in Program Usage display does not work

What would be really amazing is a way to declare a Noun attribute on a class, and have the Verb attribute declared per field/property of a class, like so:

[Noun("tpm")]
public class TpmArgs
{
    [Verb("init")]
    public TpmInitArgs TpmInitArgs;
    [Verb("show")
    public TpmShowArgs TpmShowArgs;
}

which can be declared as simply as

Parser.Default.ParseArguments<TpmArgs>(...)

and would eventually map to particular args type in WithParsed<T>

.WithParsed<TpmInitArgs>( ... )
.WithParsed<TpmShowArgs>(... )

(kinda resembling how routes are declared in controllers in ASP.NET WebApi)

piotrwcislo avatar Jan 31 '25 18:01 piotrwcislo