commandline
commandline copied to clipboard
Subverbs
Does this library support subverbs? Based on my current research it doesn't. For example, let's assume git is the top level verb, And it has several layers of subverbs.
git/commit git/push/folder --> (file and folder are verbs as well) git/push/file git/pull/folder git/pull/file
Is there anyway to write this kind of command line interface with multiple layers of subcommands/subverbs? Thanks
It does not. There was a comment in one of the issues where someone mentioned they remove the first item from args
each time a verb is hit and then run the parser over it again, but that seems kind of fragile.
Psuedocode Example:
var args = new List<string>{"git", "push", "--help"}; // this comes from main
while(true)
{
var result = Parser.Default.ParseArguments<GitVerb, PushVerb, PullVerb>(args);
if(/* result is gitverb */)
{
args.Remove(0);
continue;
}
RunProgram(result);
}
@nemec What if we treate the subverbs as values and after parsing we can use switch statement to determine what value(subcommand) it is and perform things accordingly. That might be a hack as well right?
[Verb("git"), HelpText = "git verb")]
class GitOptions
{
[Option("foo", Required = true, HelpText = "foo")]
public string Name { get; set; }
[Value(0)]
public string SubVerb { get; set; }
[Value(1)]
public string NextVerb { get; set; }
}
If we run $git push file --foo "bar"
, opts.SubVerb is "push", opts.NextVerb is "file" and opts.foo is "bar".
nemecs comment seems legit. I would use that functionality at least for the time being.
frankcchen, your solution will definitely get you in trouble because your sub verbs then wont get as good of handling the deeper you go.
Verbs that "consume" the args actually is an elegant solution for the problem until subverbs can be formally accounted for in the library. (Frankly this is probably how the library would handle sub verbs!... Lol)
How would you handle the automatic generation of help for subverbs with this approach?
@simonech I recently needed to implement subverbs for a commandline tool I made so I put my solution on GitHub.
The code to use it is a bit more complicated than the pseudo code provided by @nemec but it should handle the auto help/version well enough.
It also does not allow a user to pass, for example:
git git git git push --help
~~The second git
would trigger an error to display the subverbs available under the git
verb.~~
Unless you code it to parse the git
verb multiple levels deep of course.
Edit: I was wrong sorry, the above would display an error that the verb was not recognized but does not show the available verbs under git
.
Instead of subverbs, just use hyphenation on your verbs.
mycmd pull-folder
mycmd pull-file
mycmd verb-subverb
I would not recommend subverbs due to the complexity, and I would classify it as a commandline smell (like a code smell but on the command line)
@ericnewton76 I completely agree that hyphenation should be considered a better solution most of the time.
In the program I had to make my subverb extension project for however, the amount of commands that are crammed in far exceeds what MapResult
is capable of handling.
Most of those commands now have been grouped in five subverb categories which in my opinion made it a much nicer tool to work with.
One could argue that having that many commands/verbs available in a single command line app is a code/commandline smell in and of itself. I would definitely agree with that as well but in this particular case the decision to break the app up into five different ones is not mine to make. So having the option to use subverbs still would've been nice for me in this situation.
I actually tried implementing this feature in this project first, since it was marked as good first issue
, but as you said it is quite complex and in my opinion could benefit from some more design discussion if you're still interested in making subverbs part of the library.
We're always interested in new ideas. If you have a different approach, I'd love to discuss it! They are always welcome.
Personally, I have an approach that I wanted to implement, similar to node commander package. Instead of using classes to dictate the command line options (and verbs), you use a fluent interface. In the end you end up with a "program" (or whatever you wanna call it) object that contains the values or undefined or defaults. Its so different from the current implementation that it would practically be a very different class library itself. Which makes it somewhat off-topic here but I wanted to mention it.
Even gits "sub-verbs" are actually separate programs, just using a convention that the main git.exe understands to make the subprocess call. For example, git svn
(I believe) is actually git-svn.exe
installed alongside git.exe
and a few other "git staples" are actually python scripts named git-function.py
I realize this isn't a precise analogy given the main topic though. You'll notice, if you subscrribe to the git mailing list, that some of these get promoted into the core git, and rewritten in C/C++ to speed them up.
Sorry for the late reply. I wanted to take another look at the code base and understand it a bit better before submitting an approach but did not get around to doing so until now.
Your idea of using a fluent interface to build a sort of program sounds pretty interesting. I'm having trouble imagining how one would implement that without resorting to "cheating" by using dynamic
though.
My experience is mostly with C# so my best guess would be that you'd still define an interface and then fluently built an implementation for that interface using something like Castle Core's DynamicProxy. Or perhaps something like a type provider library in F# could handle it? I don't have a lot of experience with that but I came across Rezoom.SQL once which does some cool/funky stuff using database schema scripts and queries. I guess you could do something similar by treating the help text of the command line app as the schema and then generate a type/program from that. If it isn't considered to be to much off-topic I'd like to hear what kind of implementation you came up with.
As for my current suggested approach to implement the subverb feature, after looking at the code base again I believe the following could work well enough.
Consumer side
My first thought/attempt at implementing this was to simply allow verbs to define properties of types that are also decorated with the Verb
attribute.
[Verb("main")]
public class MainVerb
{
public SubVerb Sub { get; set; }
}
[Verb("sub")]
public class SubVerb
{
[Value(0)]
public string SomeValue { get; set; }
}
This didn't go very well for a number of reasons, the main one being that verbs, options and values could be mixed in strange ways.
// While technically possible it would be strange to allow for
// 'program.exe main -s sub ...' to be parsed in my opinion.
[Verb("main")]
public class MainVerb
{
[Option('s')]
public bool SomeSwitch { get; set; }
public SubVerb Sub { get; set; }
}
[Verb("sub")]
public class SubVerb
{
// opts/values
}
So following that failure my suggestion now on how to implement this feature would be to define another attribute called a VerbSet. This attribute would (currently at least) have the same properties as the Verb attribute but instead of Option and Value properties only types that are decorated with either a Verb or VerbSet would be allowed.
// Same attribute props as Verb.
[VerbSet("set1", HelpText = "Verb set one.", Hidden = false)]
public class VerbSet1
{
// No [Verb] attribute needed since the Verb1 type should already be decorated with it.
public Verb1 VerbOne { get; }
// Further nesting can be done by adding a property for a type decorated with [VerbSet].
public VerbSet2 VerbSetTwo { get; }
// Option and Value props aren't even considered when parsing or building help.
[Option('o')]
public string IgnoredOption { get; set; }
[Value(0)]
public string IgnoredValue { get; set; }
}
[VerbSet("set2", HelpText = "Verb set two.")]
public class VerbSet2
{
public Verb2 VerbTwo { get; }
}
[Verb("verb0")]
public Verb0
{
[Option('x')]
public bool SomeSwitch { get; set; }
}
[Verb("verb1")]
public Verb1
{
[Option('s')]
public bool SomeSwitch { get; set; }
}
[Verb("verb2")]
public Verb2
{
[Value(0)]
public string SomeValue { get; set; }
}
Using the above verbs and sets some of the available paths in the program would be:
program.exe verb0 -x
program.exe set1 verb1 -s
program.exe set1 set2 verb2 somevaluestring
You may have noticed that the verbset properties in the example only have a getter. This is to illustrate that set classes are only used to define the schema of the program and thus are never instantiated.
To me it seemed unnecessary to create them since the Verb classes hold all the actual information used to run a command.
The VerbSet attribute's target could also be set to AttributeTargets.Interface
but I don't know if/how that would affect usage in F#.
Following that logic, usage with a Parser would be like this:
ParserResult<object> result = Parser.Default.ParseArguments<VerbSet1, Verb0>(args);
// Only Verb classes are returned as a Parsed<T>.
result.MapResult(
(Verb0 verbZero) => ...,
(Verb1 verbOne) => ...,
(Verb2 verbTwo) => ...,
(Errors _) => ...);
// The following could be an edge case since VerbSet classes shouldn't be created.
ParserResult<VerbSet1> result = Parser.Default.ParseArguments<VerbSet1>(args);
// But the above would be a bit strange for a developer to do since sets only contain info
// about other VerbSet and Verb types.
// So parsing like this would create a needless extra value between the actual sets/verbs
// that should be parsed.
// If the developer really does want to do this, for whatever reason, a workaround would be:
ParserResult<object> result = Parser.Default.ParseArguments(args, typeof(VerbSet1));
Backend
To get the parsing logic working I suggest modifying CommandLine.Core.Verb
to hold extra info so it can also be used to represent a VerbSet.
sealed class Verb
{
public Verb(string name, string helpText, bool hidden, IEnumerable<Type> subverbTypes
{
// omitted.
this.SubverbTypes = subverbTypes;
}
// omitted.
public IEnumerable<Type> SubverbTypes { get; }
public bool IsVerbSet => this.SubverbTypes.Any();
public static Verb FromAttribute(VerbAttribute attribute)
{
return new Verb(
attribute.Name,
attribute.HelpText,
attribute.Hidden,
Enumerable.Empty<Type>() // Regular verbs cannot hold verb/set types.
);
}
public static Verb FromAttribute(VerbSetAttribute attribute, Type type)
{
var subVerbTypes = // get verb/set types.
return new Verb(
attribute.Name,
attribute.HelpText,
attribute.Hidden,
subVerbTypes
);
}
public static IEnumerable<Tuple<Verb, Type>> SelectFromTypes(IEnumerable<Type> types)
{
var verbsets = // get VerbSetAttribute Verbs.
var verbs = // get VerbAttribute Verbs.
// Return both combined.
return verbsets.Concat(verbs);
}
}
By representing both attributes as CommandLine.Core.Verb
InstanceChooser.MatchVerb
can be modified to recursively call back to InstanceChooser.Choose
whenever a verb is detected to be a set (with the help of Verb.IsVerbSet
). This would achieve the desired effect of re-parsing the arguments without the currently detected verbset name. In the end of the chain an actual verb should have been detected and parsing would continue into InstanceBuilder.Build
like it does already.
Another reason to treat both attributes as a CommandLine.Core.Verb
is that it should make it much easier to get AutoHelp working since HelpText
already has logic to convert that class.
I think that covers at least most of what would need to be implemented. Please let me know if this way of implementing the feature would be acceptable or if I forgot to account for some features of the library.
I think easiest solution would be add attribute, to capture all items after verb to IEnumerable
[Verb("add", HelpText = "Add item to the project.")]
internal class AddVerb
{
[Everything]
public IEnumerable<string> Parameters{ get; set; }
}
Parser.Default.ParseArguments(args, typeof(AddVerb))
.WithParsed<AddVerb>(v => AddItem(v));
private static void AddItem(AddVerb verb)
{
var parser = new Parser(with => with.EnableDashDash = true);
var result = parser.ParseArguments<SubVerbs....>(verb.Parameters);
result.WithParsed<SubVerb>(v => OnSubVerb(v));
}
But AFAIK no such attribute exist now.
Help generation is another question, but it also can be handled by subparser, somehow.
see also #13 #35
and #69
any update on this?
Any update on this?
One solution to this is to leave CommandLineParser alone and write a new class CommandTree that allows one to "mount" command line parsers on different "verb paths" like "git/push/folder". Each mount path points to a command line parser. When main is called, this command tree would use its registry of command line parsers to find and use the right command parser to parse the arguments after the mount point. Problem solved.