Skip to content
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

Support for nesting commands #24

Closed
Gbury opened this issue Mar 6, 2015 · 31 comments
Closed

Support for nesting commands #24

Gbury opened this issue Mar 6, 2015 · 31 comments

Comments

@Gbury
Copy link

Gbury commented Mar 6, 2015

Commands can be specified using the Term.eval_choice function, however there doesn't seem to be any way to specify sub commands, to achieve results such as opam pin add, where add is a sub command of pin.

More generally, it would be useful to be able to define arbitrary nesting of commands, possible through the use of a function with the same semantics as Term.eval_choice, but with the following signature:

val group : ('a Term.t * Term.info) list -> 'a Term.t
@dbuenzli
Copy link
Owner

dbuenzli commented Mar 7, 2015

Something could be done to improve these use cases at least one level further. What I missed at the time is that often tools having commands also have:

tool OBJECT VERB ...

synopses.

However from a usability perspective I don't think it is a good thing to allow arbitrary nesting. Besides the bulk of the work here is not to define a grouping mechanism but rather how to handle and present the resulting automatic documentation in a sensible way (e.g. where do you put the documentation of the options for each subcommand, etc.).

@Gbury
Copy link
Author

Gbury commented Mar 9, 2015

I think it's reasonable to assume that sub commands of the the same command would have some (if not all) options in common. In that case, each command could have its documentation page, printed when using tool OBJECT --help, listing its sub commands in a section and options in other sections (maybe with a section for each sub commands with specific options).

@c-cube
Copy link

c-cube commented Apr 7, 2015

I think having a sub-tree of commands would be useful, and w.r.t. documentation it would require, imho, two things:

  • in the documentation of any node of the tree, the whole subtree of commands should be mentioned. Currently it's already the case, since the root documentation mentions the available sub-commands; it should also mention sub-sub-commands, and so on, to summarize the structure of the tree.
  • in the documentation of any node of the tree, all options that are specific to this node (program cmd1 cmd2 cmd3 ...) should be listed, as well as options specific to prefixes of the tree (the options for program, for program cmd1, and for program cmd1 cmd2. This problem is common in the documentation of object-oriented programs, in which the doc of each class lists both the methods of the class and the methods inherited from parents.

@Gbury
Copy link
Author

Gbury commented Apr 7, 2015

One more thing I just noticed and that could be an issue by itself, but is closely related to this issue, is that when you use Term.eval_choice, the only synopsis in the main help page is the one where you call one of the commands, while the synopsis of the default term appears to be discarded.
When the default term simply prints the help page, it doesn't really matter, but as soon as the default term has positional arguments, it is troubling that its synopsis doesn't appear.

@dbuenzli
Copy link
Owner

dbuenzli commented Apr 7, 2015

Le mardi, 7 avril 2015 à 18:40, Guillaume Bury a écrit :

When the default term simply prints the help page, it doesn't really matter, but as soon as the default term has positional arguments, it is troubling that its synopsis doesn't appear.

Could you please open an issue with a repro case please.

Daniel

@Gbury
Copy link
Author

Gbury commented Apr 7, 2015

Done in issue#28

@samoht
Copy link

samoht commented Oct 2, 2015

opam has an encoding of subcommands which is not too bad. See https://github.com/ocaml/opam/blob/master/src/client/opamArg.mli#L147-L173

@ihodes
Copy link

ihodes commented Apr 19, 2016

I think it's reasonable to assume that sub commands of the the same command would have some (if not all) options in common. In that case, each command could have its documentation page, printed when using tool OBJECT --help, listing its sub commands in a section and options in other sections (maybe with a section for each sub commands with specific options).

This is how opam handles the documentation of subcommands. This seems like a sensible and scalable way to handle arbitrary nesting of subcommands.

@shonfeder
Copy link

As noted in ocaml/dune#1482, dune would like to make use of nested subcommands. I would be interested in helping build out this functionality, if you would be open to contributions to realize this feature. Would you be open to some PRs in that direction, @dbuenzli?

@dbuenzli
Copy link
Owner

dbuenzli commented Mar 1, 2019

@shonfeder come up with a design at the API level first.

@dbuenzli
Copy link
Owner

dbuenzli commented Mar 1, 2019

Also so far I have personally not been too annoyed by the lack of it. So you might simply consider an encoding. See for example here.

@shonfeder
Copy link

I'll do as you advise re: API design and report back as soon as I have something sensible. Thanks :)

I'm currently using a pattern exactly along the lines you suggest, and I agree it works fine. Functionally speaking, however, we do miss

  • help documentation specific to each nested subcommand, and
  • specialization of options and flags to relevant subcommands, without requiring writing special purpose validators and handlers

I'll be in touch soon.

@ghost
Copy link

ghost commented May 22, 2019

@shonfeder, have you made progress on this by any chance? We were talking about it in the context of dune

@shonfeder
Copy link

@diml Unfortunately, I haven't had time to work on this at all. If someone else has the bandwidth and interest to tackle it, they should feel free to do so.

If not, I'm happy to turn my attention here after I get ocaml/dune#2185 through (tho I can't guarantee how swift I'll be able to move).

@ghost
Copy link

ghost commented May 22, 2019

Ok. I haven't followed dune init too closely but I see that the title of ocaml/dune#2185 mentions "subcommand", does that mean that we need the current PR first?

BTW, API-wise Core.Command could be a source of inspiration since it has supported arbitrary deep sub-tree of commands for a long time. It is also the second revision of the API, so I expect it to have reached a good level of maturity by now.

@dbuenzli
Copy link
Owner

As I already mentioned, I don't think arbitrary sub-tree of commands scale both from a documentation and end-user point of view. It should be sufficient to add support for one more layer.

@aalekseyev
Copy link

As an end-user of many command line interfaces that use Core.Command, I like command groups a lot and arbitrary nesting is a large part of it.

From the usability perspective, the way they work is that you need to specify full command name (get to the leaf-level) before you can specify any options or flags. (in fact command groups can't have any parameters at all)

From the documentation perspective, the way they work is that for every command group you've got a short doc and the list of subcommands with their one-sentence descriptions. The bulk of documentation (and all flag documentation) then lives in the leaves of this tree.

A few things I think are great about unrestricted command group nesting:

  • it makes it much easier to grow the cli without sacrificing discoverability by grouping related commands together and putting weird and experimental commands into weird corners of the command tree.
  • it makes it possible for libraries to expose whole command groups without having to know where in the application cli the group will be included
  • it makes it possible to merge multiple binaries into one without having to redesign the cli and, conversely, makes it easy to split the binary into multiple along the subcommand boundaries

@dbuenzli
Copy link
Owner

From the documentation perspective, the way they work is that for every command group you've got a short doc and the list of subcommands with their one-sentence descriptions. The bulk of documentation (and all flag documentation) then lives in the leaves of this tree.

Unless this has changed in Core.Command, documentation is a little a bit different in cmdliner as it comes under the form of manpages which can be enriched in the program itself. It's not a simple enumeration of one line option summaries. So there are quite a few questions about how you want to present these things in practice (cli synopses, default and overridable sectioning, where and in which sections the subcommands options are documented, etc.).

A few things I think are great about unrestricted command group nesting:
[...]

The properties you mention seems rather to be in favour of the person who is designing the command line than the person using it.

I don't think it's a good idea for end users to go beyond tool cmd sub which will certainly cater for 95% of the usage out there (object verb or verb object) and will allow to provide a good story at the documentation level that I'm not sure arbitrary nesting would. I know programmers are very fond of generalizing everything, but it's not always a good idea.

  • it makes it possible for libraries to expose whole command groups without having to know where in the application cli the group will be included

One has function abstraction for that.

  • it makes it possible to merge multiple binaries into one without having to redesign the cli and, conversely, makes it easy to split the binary into multiple along the subcommand boundaries

I prefer if the person doing this takes time to reflect on and redesign the cli to make it fit in the above structure so that I don't have, as an end-user, to suffer absurd nesting levels.

@ghost
Copy link

ghost commented May 23, 2019

Pretty much everything in computers is hierarchically organised as a tree: the file systems, the modules of a library, etc... A program with several commands is simply a bunch of different functionalities organised under a single toplevel program name, so it makes sense to allow arbitrary tree of commands.

Now, I agree that if you take any tree in any context you can find a way to refactor it to reduce the nesting, trim it and simplify it. However, having this extra power of being able to construct arbitrary trees allows you to explore the design space and converge towards better designs as your program evolves. Because at the end of the day, it's pretty rare that the right design just pops out. Instead you need to experiment to find out what works best.

@dbuenzli
Copy link
Owner

Pretty much everything in computers is hierarchically organised as a tree: the file systems, the modules of a library, etc...

Cmdliner is interested about giving good UIs for humans not about mirroring how things are organized for computers. I don't think I ever saw a program using more than the levels I mentioned and I don't think it would be a good thing ux wise.

Now I don't want to waste my time discussing this. As I said the first step is to come up with a good design at the API and documentation presentation level. I suspect it will be much easier if the person who does this first starts to think about a single additional level with documentation for the subcommands presented in the manpage of the command, basically mirroring what is done for the main manpage when multiple commands are used.

After that whether this can/should be generalized can be discussed later.

@ghost
Copy link

ghost commented May 23, 2019

Yh, I guess the only examples I have in mind are Jane Street tools that are not public.

Regarding the API, if we want to only support 2 levels I would suggest something like this:

type 'a leaf_command = 'a t * info

type 'a command =
  | Normal_command of 'a leaf_command
  | Command_with_subcommabds of {
    default : 'a leaf_command;
    sub_commands : 'a leaf_command list;
  }

val eval_choice_with_subcommands : ... ->
  default:'a leaf_command -> commands:'a command list -> 'a result

and the man page for a command with sub-command would include a COMMANDS section as for the main man page.

Although, personally I would change the type of the default field and label to just info. I.e. at any level, a command that accept sub-commands would only be allowed to print a man page and nothing else. I feel like it is a bit confusing otherwise, especially if the command accepts anonymous arguments (but maybe that's already forbidden by cmdliner?).

@dbuenzli
Copy link
Owner

I.e. at any level, a command that accept sub-commands would only be allowed to print a man page and nothing else. I feel like it is a bit confusing otherwise, especially if the command accepts anonymous arguments (but maybe that's already forbidden by cmdliner?).

An other way of saying this is that it it not allowed for commands to have a default subcommand. I would tend to be in favour of this as it reduces parsing ambiguities. However note that opam has quite a few commands where this is not the case (e.g. switch, repo), so I'm a bit undecided.

The current cmdliner when you have commands and a default command forces you to specify the command as the second argument. I always found that unpleasant because it doesn't respect the general model of cli parsing where you are supposed to be able to arbitrarily interleave option and position arguments.

Now I don't know/remember why I didn't do this: on tool OPTS... POS0 if POS0 is
a command use the command. If it's not use the default command. If you want to use the default command with a first positional argument that is a command use tool ... -- POS0. Should we maybe support that scheme for subcommands (and maybe commands as well) ?

and the man page for a command with sub-command would include a COMMANDS section as for the main man page

That feels ok. There's also the question of where to put the subcommand's argument descriptions by default (when docs is unspecified). Maybe create a SUBCMD ARGUMENTS section for them (for environment variables and exit statuses I have the feeling we can share them with the command, for my own programs I already tend to share them among all commands). May be worth checking out a few manpages to see if there's an established pattern.

Thinking about a potential generalization, I think maybe you'd have a manpage for each level except for the last-to-one which collapses the last level in its own.

@ghost
Copy link

ghost commented May 23, 2019

An other way of saying this is that it it not allowed for commands to have a default subcommand. I would tend to be in favour of this as it reduces parsing ambiguities. However note that opam has quite a few commands where this is not the case (e.g. switch, repo), so I'm a bit undecided.

Indeed. We are having a similar discussion for dune, and so far it seems that having shell completion would provide the same benefit without the troubles: ocaml/dune#2097

Now I don't know/remember why I didn't do this: on tool OPTS... POS0 if POS0 is
a command use the command. If it's not use the default command. If you want to use the default command with a first positional argument that is a command use tool ... -- POS0. Should we maybe support that scheme for subcommands (and maybe commands as well) ?

Isn't that ambiguous though? Since we don't know the grammar of the options until we know the command, it's difficult to say whether the first argument not starting with a dash is a command or the argument of a command line option.

Additionally, I feel like it plays badly with adding new commands. A command line of the form prog x ... that is a valid invocation of the default command could become invalid if x becomes a command in the future.

That feels ok. There's also the question of where to put the subcommand's argument descriptions by default (when docs is unspecified). Maybe create a SUBCMD ARGUMENTS section for them (for environment variables and exit statuses I have the feeling we can share them with the command, for my own programs I already tend to share them among all commands). May be worth checking out a few manpages to see if there's an established pattern.

opam switch puts them under just OPTIONS and the common options under COMMON OPTIONS. The options are shared across all sub-commands, however some arguments seems to be interpreted by only one sub-command. The COMMANDS section describes what sub-command interpret which arguments. I feel like sub-commands shouldn't accept and silently ignore arguments they don't interpret though.

I also checked git bisect. Some of the sub-commands have specific arguments but they are never listed in a section. Instead the DESCRIPTION section starts by listing the synopsis of the various sub-commands, mentioning their specific arguments and then describe the bisection process explaining the arguments in the middle of the description. That seems good but also require more development and maintenance effort. Additionally, the documentation of an argument that is specific to one sub-command would no longer be attached to the Term.t of the argument since it's documentation would be in the middle of the main description.

Thinking about a potential generalization, I think maybe you'd have a manpage for each level except for the last-to-one which collapses the last level in its own.

That seems good.

Here is a concrete proposal that I feel would produce good default man pages without too much trouble:

NAME
  prog-foo - ...

SYNOPSIS
  prog foo bar ...
  prog foo baz ...

DESCRIPTION
  General overview of "prog foo"

COMMANDS
  bar
    descrption of bar

    --bar-specific-option
        blah

  baz
    description of baz.

OPTIONS
  <options shared across all sub-commands>

COMMON OPTIONS
  <options shared across all commands and sub-commands>

i.e. the options specific to one sub-command are listed in the sub-command description, with one more level of indentation. If there are not too many, that should be ok.

@dbuenzli
Copy link
Owner

Isn't that ambiguous though? Since we don't know the grammar of the options until we know the command, it's difficult to say whether the first argument not starting with a dash is a command or the argument of a command line option.

Ah yes that was likely the reason :-)

Assuming subcommands do not have too many specific options but mainly specific positional arguments, I think we can try the manpage layout your propose.

@rgrinberg
Copy link

rgrinberg commented Nov 9, 2020

This feature is now seriously needed for dune, so I gave it a try here. It seems to work fine in the happy case, but I'd like the following behaviour for dune:

If one calls $ dune group --help or $ dune group, you will see the listing of all available subcommands under this group. Pointers on how to achieve that will be much appreciated.

@dbuenzli
Copy link
Owner

dbuenzli commented Nov 9, 2020

Pointers on how to achieve that will be much appreciated.

Not sure exactly what you are asking for but if you follow do_result you should eventually get to Cmdliner_docgen.pp_man which is where things should happen. I suspect you will have to enrich Cmdliner_info.eval to make it aware of the grouping structure.

@rgrinberg
Copy link

Thanks will take a look.

To clarify, I'd like $ dune <subcommand> --help to be equivalent to $ dune <subcommand>, and to show the appropriate manpage for <subcommand>. This manpage should include a listing of all available subcommands.

@dbuenzli
Copy link
Owner

dbuenzli commented Nov 9, 2020

That sounds reasonable, as discussed above allowing doing something else on dune <subcommand> like cmdliner does at the first level would be ambiguous (unless we force no options before exec cmd sub, but I'm often already annoyed about no options between exec cmd so I don't think it's a good idea).

Alternatively we could also error, just mentioning a sub among the group names must be provided.

@rgrinberg
Copy link

Okay, I've made some progress to the point where it should be good to experiment with in dune. There's still some work remaining on:

  • Integrating the prefix based autosuggest. I know how to do this one.

  • I don't know howchoice_names should be handled.

  • Similarly, the following constructor for 'a ret: Help of Manpage.format * string option isn't usable for grouped commands. Should we add a second constructor like Help_subcommand of Manpage.format * string list to make it possible to return help for any subcommand?

@snowleopard
Copy link

snowleopard commented Apr 9, 2021

While using the existing support for nested commands in Dune, I came across the need to deprecate subcommands.

Say, we have dune cmd foo and dune cmd bar and some day we deprecate bar. Of course, we can just delete bar from the cmd's group but that is not very user-friendly. Ideally, if a user types dune cmd bar, they would get a deprecation message, e.g. "dune cmd bar was removed in Dune 3.0, use dune baz instead". On the other hand, if they type dune cmd --help, it's best not to list bar there to avoid unnecessary noise.

Providing support for deprecated commands doesn't seem difficult, but it seems worth discussing it here, so that it interacts well with the whole nested commands feature.

EDIT: If there is a consensus that this is a good idea, I'd be happy to implement a prototype.

@dbuenzli
Copy link
Owner

dbuenzli commented Jan 18, 2022

So I eventually got to something. In a nutshell:

  1. Tree of commands. Each command defines its own cli.
  2. Root of the tree is the name of the tool itself, it's the command you give to the evaluation function.
  3. At any level, a grouping command can have its own cli or not. In the latter case not specifying a command results in a parse error (like a required positional argument).
  4. Each command has its own manpage.
  5. Manpage were improved so that the synopsis of subcommands is shown
    in the parent command.

The drawback of 3. is that no option is allowed before a subcommand (like the current cmdliner does).

This comes under the form of a new Cmdliner.Cmd module. All previous term evaluation functions (Term.{eval,eval_choices}) are deprecated. Backward compatibility should not be broken though.

There's more information in the release notes and the documentation online was updated with the new terminology along with the tutorial and examples.

I'm not ready to release yet and I want to test the API on a few projects of mine. If you have time to go through the changes or even test them do not hesitate to open issues about the new API.

Thanks all for the discussion and especially to @rgrinberg for implementing #123 which significantly informed the current result.

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

No branches or pull requests

9 participants