Skip to content

Multilevel Verb command in version 2 #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ericnewton76 opened this issue Nov 4, 2017 · 16 comments
Closed

Multilevel Verb command in version 2 #69

ericnewton76 opened this issue Nov 4, 2017 · 16 comments

Comments

@ericnewton76
Copy link
Member

Issue by SuhovDE
Friday Apr 22, 2016 at 12:34 GMT
Originally opened as gsscoder/commandline#309


Hello, I have a question.
Can somebody tell me how to implement multilevel verb command using version 2?
For example I need to implement the following commands:

Create DataArea --Name="Name" --Connection="Connection string"
Create App --Name="some name" --DataArea="Name"
Show App
Show DataArea
and so on.

For previous version it is discussed in #107. But in new version I was not able to find a solution.

Thank you.

@ericnewton76
Copy link
Member Author

Comment by nemec
Saturday Apr 23, 2016 at 20:23 GMT


Hi @SuhovDE, it hasn't been implemented in the new version either. One option I can think of is to combine them into one verb with a hyphen (e.g. Create-DataArea, Create-App). If you can manage to implement multilevel verbs in a backwards compatible way, I'd be happy to merge the pull request, but given the way they're configured now it may be difficult.

@ericnewton76
Copy link
Member Author

Comment by SuhovDE
Monday Apr 25, 2016 at 08:31 GMT


@nemec, thank you for answer.
Now I have about 20 verb commands, therefore I would like to implement multilevel verbs with 4 main commands (create, update, delete, show) and 5 subcommands by type of data.

I think it could be possible to implement it like nested classes, as variant something like this

[Verb]
public class VerbCommand
{
[Verb]
public class VerbSubcommand1
{
[Option]
string Option { get;set;}
}
[Verb]
public class VerbSubcommand2
{
[Option]
string Option2 { get;set;}
}
}

Or may be not possible, difficult to say, when you have never seen code of library

@ericnewton76
Copy link
Member Author

Comment by CreepyGnome
Wednesday Jun 15, 2016 at 16:02 GMT


I like this concept as well, and think that if a way to implement it can be found that it shouldn't be limited to just one level of child verbs, but if would be nice if i could be able to have as many as the developer would like in theory.

I think the nesting verb 'classes' is a good approach, my only concern with it would be code reuse. If we have child verbs we want to share across other parent verbs we would need to use create common base classes they all have to extend to have the same common child verb apply to more than one parent verb. Which means we would have a lot of boiler plate code being tested with the base classes outside of all the testing. This could be come a difficult code base to manage in a long run if you have to much child verbs you want to share.

Maybe another approach that could be considered would be to have a new property on verbs or a new attribute to add to a class that is a verb. If it was a property it could be named like ChildrenVerbs which is a collection of of the verb names that would be its children. If it was an attribute it could be something like ChildVerbAttribute, which takes a Collection of Verb names as well but is a list of the Parent verbs.

This way you do not have to nest anything, and you are just creating a hierarchy of verbs either from the Parent down, or form the Child up whichever method is preferable or easiest to implement. This hierarchy can then be used to organize how the HelpText is displayed and what Verbs and Options are created.

A Verb may then need to have a methods/properties that are set by the Parser of the Children and Parents. I would recommend both as you never know which way someone may want to go in based on their application. How to do this, either in requiring an IVerb interface or a VerbBase class, OR to stick with the attribute approach the developer creates any property they want that is of a specific type and dectorate it with a attribute like ParentVerb which would be a verb type, and could have ChildVerbs which would be a collection of Verbs.

Anyway, just spit balling some ideas, as I think this would be a great feature. However, I think it is very big, and even though it can probably be done in backwards compatible way with the current 2.0 beta, it may not making to 2.0 RTM release I would assume due to the size of the effort.

@ericnewton76
Copy link
Member Author

Comment by manytwo
Friday Jul 22, 2016 at 11:04 GMT


Hi,
it may be late but I post my actual pattern for multi verbs.
Goal is to first parse args[0] manually, and send to Parser args.Skip(1).
You can do a simple switch case on your args[0] or implement more generic with custom attributes.

You can also use nested Classes to order verbs and sub verbs etc.

A simplified example:

class Program
    {
        static void Main(string[] args)
        {
            if (args == null || args.Count() < 1)
            {
                // Help for create / show ...
                ShowCustomHelp();
            }
            else
            {
                switch (args[0].ToLowerInvariant())
                {
                    case "create":                        
                        ParseCreateVerbs(args);
                        break;
                    case "show":
                        ParseShowVerbs(args);
                        break;
                    default:
                        Console.WriteLine(args[0] + " tool do not exists.");
                        ShowCustomHelp();
                        break;
                }
            }
        }
        static void ParseCreateVerbs(string[] args)
        {
            //for example, help parsing is simplified, can do custom  attributes,
            //use  verbAttributes with custom parsing etc. 
            if(args.Count() < 2 || args[1].ToLowerInvariant() == "--help"){
                CreateOpt.ShowHelp();
            }
            // DO NOT FORGET Skip(1) for parsing!!
            Parser.Default
                .ParseArguments<CreateOpt.DataArea, CreateOpt.App,...>(args.Skip(1).ToArray()) 
            // do your stuff
        }
        static void ParseShowVerbs(string[] args)
        { 
            ... 
        }
     }
        //nestead class can be used to order verbs / subverbs
     class CreateOpt
     {
         [Verb("DataArea", HelpText="")]
         public class DataArea
         {
             ...
         }
         [Verb("App", HelpText = "")]
         public class DataArea
         {
             ...
         }
         public static ShowHelp(){ ... }
     }

There is probably many more better implementations, but I hope this simple one can help.

@ericnewton76
Copy link
Member Author

Comment by SuhovDE
Monday Jul 25, 2016 at 10:21 GMT


it may be late but I post my actual pattern for multi verbs.
Goal is to first parse args[0] manually, and send to Parser args.Skip(1).

@manytwo, it is very gut idea, I will follow it. Thank you.

@ericnewton76
Copy link
Member Author

Comment by EdonGashi
Tuesday Jul 26, 2016 at 00:52 GMT


Hello, I have also been struggling with the lack of this feature. I managed to fake it by eating the first token recursively and checking for subverbs before calling ParseArguments. The nice part is that auto help building works with each level of verbs.

[AttributeUsage(AttributeTargets.Class)]
public sealed class SubVerbsAttribute : Attribute
{
    public Type[] Types { get; }

    public SubVerbsAttribute(params Type[] types)
    {
        Types = types;
    }
}

public static class ParserVerbExtensions
{
    public static ParserResult<object> ParseVerbs(this Parser parser, string[] args, params Type[] types)
    {
        if (args.Length == 0 || args[0].StartsWith("-"))
        {
            return parser.ParseArguments(args, types);
        }

        var verb = args[0];
        foreach (var type in types)
        {
            var verbAttribute = type.GetCustomAttribute<VerbAttribute>();
            if (verbAttribute == null || verbAttribute.Name != verb)
            {
                continue;
            }

            var subVerbsAttribute = type.GetCustomAttribute<SubVerbsAttribute>();
            if (subVerbsAttribute != null)
            {
                return ParseVerbs(parser, args.Skip(1).ToArray(), subVerbsAttribute.Types);
            }

            break;
        }

        return parser.ParseArguments(args, types);
    }
}

Example:

[Verb("commit", HelpText = "Manage commits.")]
[SubVerbs(typeof(Commit.Log), typeof(Commit.Add))]
public class Commit
{
    [Verb("log", HelpText = "Display commits.")]
    [SubVerbs(typeof(Commit.Log.All), typeof(Commit.Log.Latest))]
    class Log
    {
        [Verb("all", HelpText = "Display all commits.")]
        class All { /* do work */ }

        [Verb("latest", HelpText = "Display latest commits.")]
        class Latest { /* do work */ }
    }

    [Verb("add", HelpText = "Add new commit.")]
    class Add { /* do work */ }
}

app commit add
app commit log all
app commit log latest
app commit --help
app commit log --help

@ericnewton76
Copy link
Member Author

Comment by jkodroff
Thursday Dec 01, 2016 at 17:41 GMT


@edongashi Could you add an example of Program.cs calling Parser.Default to your example above?

@ericnewton76
Copy link
Member Author

Comment by EdonGashi
Thursday Dec 01, 2016 at 19:32 GMT


@jkodroff Hello, here's a demo with the pattern that I use for command line apps. If you have any question feel free to ask here or add an issue in that repo

https://github.com/EdonGashi/CommandlineChildVerbs

@ericnewton76
Copy link
Member Author

Comment by nmklotas
Saturday Aug 19, 2017 at 09:44 GMT


It would be very nice to have multiple verbs support. dotnet.exe is a very good example of this:

For example dotnet add:

Usage: dotnet add [options] <PROJECT> [command]

Arguments:
  <PROJECT>   The project file to operate on. If a file is not specified, the command will search the current directory for one.

Options:
  -h, --help   Show help information.

Commands:
  package <PACKAGE_NAME>   .NET Add Package reference Command
  reference <args>         .NET Add Project to Project reference Command

@BenjaminHolland
Copy link

Has this gone anywhere? I couldn't find any mention of it in the wiki...

@jjzieve
Copy link

jjzieve commented Apr 28, 2020

Agreed, this is a very useful feature, it seems @edongashi 's approach would work as a PR.

@johnjaylward
Copy link
Contributor

looks like the same idea as #13

@johnjaylward
Copy link
Contributor

and #35

@R0boC0p
Copy link

R0boC0p commented Dec 15, 2021

Comment by EdonGashi Thursday Dec 01, 2016 at 19:32 GMT

@jkodroff Hello, here's a demo with the pattern that I use for command line apps. If you have any question feel free to ask here or add an issue in that repo

https://github.com/EdonGashi/CommandlineChildVerbs

Hi Eric, I was just coming along this here, and actually tried the Extension (that was the only file that I needed) from the github repo you posted. I was so close to write something similar, but then found this. I wanted to take the chance to say thanks a very lot!!! <3
The ParserExtension.cs was the only thing I needed, so I am wondering why this is not merged back already? Are there any caveats I am not aware of?

@aaronenberg
Copy link

aaronenberg commented Dec 27, 2021

The ParserExtension.cs was the only thing I needed, so I am wondering why this is not merged back already? Are there any caveats I am not aware of?

One caveat that I encountered while using this extension in my project is that it breaks the UnParserExtensions. This is the extension that converts a parsed verb back into string arguments. It breaks because it requires a class decorated with VerbAttribute inherit from a class also decorated with VerbAttribute. And due to an assumption in the unparsing logic, it's required that any given type has only a single VerbAttribute in its type hierarchy.

public static Maybe<VerbAttribute> GetVerbSpecification(this Type type)
{
return
(from attr in
type.FlattenHierarchy().SelectMany(x => x.GetTypeInfo().GetCustomAttributes(typeof(VerbAttribute), true))
let vattr = (VerbAttribute)attr
select vattr)
.SingleOrDefault()
.ToMaybe();
}

I'm not exactly sure why this assumption is made or the consequences of removing it and allowing multiple verbs in a type's hierarchy. There could be more breakages but this is the only one I've discovered.

@JordanW9232
Copy link

JordanW9232 commented Mar 30, 2025

Comment by EdonGashi Tuesday Jul 26, 2016 at 00:52 GMT

Hello, I have also been struggling with the lack of this feature. I managed to fake it by eating the first token recursively and checking for subverbs before calling ParseArguments. The nice part is that auto help building works with each level of verbs.

[AttributeUsage(AttributeTargets.Class)]
public sealed class SubVerbsAttribute : Attribute
{
public Type[] Types { get; }

public SubVerbsAttribute(params Type[] types)
{
    Types = types;
}

}

public static class ParserVerbExtensions
{
public static ParserResult ParseVerbs(this Parser parser, string[] args, params Type[] types)
{
if (args.Length == 0 || args[0].StartsWith("-"))
{
return parser.ParseArguments(args, types);
}

    var verb = args[0];
    foreach (var type in types)
    {
        var verbAttribute = type.GetCustomAttribute<VerbAttribute>();
        if (verbAttribute == null || verbAttribute.Name != verb)
        {
            continue;
        }

        var subVerbsAttribute = type.GetCustomAttribute<SubVerbsAttribute>();
        if (subVerbsAttribute != null)
        {
            return ParseVerbs(parser, args.Skip(1).ToArray(), subVerbsAttribute.Types);
        }

        break;
    }

    return parser.ParseArguments(args, types);
}

}
Example:

[Verb("commit", HelpText = "Manage commits.")]
[SubVerbs(typeof(Commit.Log), typeof(Commit.Add))]
public class Commit
{
[Verb("log", HelpText = "Display commits.")]
[SubVerbs(typeof(Commit.Log.All), typeof(Commit.Log.Latest))]
class Log
{
[Verb("all", HelpText = "Display all commits.")]
class All { /* do work */ }

    [Verb("latest", HelpText = "Display latest commits.")]
    class Latest { /* do work */ }
}

[Verb("add", HelpText = "Add new commit.")]
class Add { /* do work */ }

}

app commit add
app commit log all
app commit log latest
app commit --help
app commit log --help

It is 8 years later and this is by far the best answer I could find online. Thank you sir.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants