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

Feature/multiple usages #2147

Open
vassudanagunta opened this issue Feb 7, 2024 · 14 comments
Open

Feature/multiple usages #2147

vassudanagunta opened this issue Feb 7, 2024 · 14 comments

Comments

@vassudanagunta
Copy link

vassudanagunta commented Feb 7, 2024

Many commands have different usages wherein some options or positional args only make sense with certain commands or other options. While Commander supports expressing this using Option.conflicts, its expression to the user is only as an error message after-the-fact. While one can explain these in the help for each option, it leads to a lot of verbosity and redundancy. Showing multiple usages is usually the most succinct way to make this clear to the user, as shown in the examples below.

This request was made at least once before as #80.

examples from popular tools

node --help:

Usage: node [options] [ script.js ] [arguments]
       node inspect [options] [ script.js | host:port ] [arguments]

git help rebase:

git rebase [-i | --interactive] [<options>] [--exec <cmd>]
               [--onto <newbase> | --keep-base] [<upstream> [<branch>]]
git rebase [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase>]
               --root [<branch>]
git rebase (--continue | --skip | --abort | --quit | --edit-todo | --show-current-patch)

git help stash:

git stash list [<log-options>]
git stash show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>]
git stash drop [-q | --quiet] [<stash>]
git stash pop [--index] [-q | --quiet] [<stash>]
git stash apply [--index] [-q | --quiet] [<stash>]
git stash branch <branchname> [<stash>]
git stash [push [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet]
                    [-u | --include-untracked] [-a | --all] [(-m | --message) <message>]
                    [--pathspec-from-file=<file> [--pathspec-file-nul]]
                    [--] [<pathspec>...]]
git stash save [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet]
                    [-u | --include-untracked] [-a | --all] [<message>]
git stash clear
git stash create [<message>]
git stash store [(-m | --message) <message>] [-q | --quiet] <commit>

mocha --help

mocha inspect [spec..]  Run tests with Mocha                         [default]
mocha init <path>       create a client-side Mocha setup at <path>

man cp (*nix), for which the usage pattern depends on whether the last positional arg is a file or directory:

cp [-R [-H | -L | -P]] [-fi | -n] [-alpSsvXx] source_file target_file
cp [-R [-H | -L | -P]] [-fi | -n] [-alpSsvXx] source_file ... target_directory
cp [-f | -i | -n] [-alPpSsvx] source_file target_file
cp [-f | -i | -n] [-alPpSsvx] source_file ... target_directory

workarounds

The only workaround I know of is to place all usages in a single string, using a newline followed by 7 spaces between successive usages. This works as long as the first usage is always prefixed by Usage: , or otherwise starts in column 8.

@shadowspawn
Copy link
Collaborator

shadowspawn commented Feb 7, 2024

I appreciate the examples and finding previous issue, thanks.

Having more than one usage string or usage example in the synopsis section is a pattern that Commander does not directly support.

@shadowspawn
Copy link
Collaborator

The git stash example might be done in Commander as nested commands. Making it look similar:

import { Command } from 'commander';
const program = new Command();
program.configureHelp({ subcommandTerm: (cmd) => cmd.name() + ' ' + cmd.usage() });

const stashCommand = program.command('stash');
stashCommand.command('list').usage('[<log-options>]')
stashCommand.command('show').usage('[-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>]');
stashCommand.command('drop').usage('[-q | --quiet] [<stash>]');

program.parse();
% node git.mjs help stash
Usage: git stash [options] [command]

Options:
  -h, --help                                                                     display help for command

Commands:
  list [<log-options>]
  show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>]
  drop [-q | --quiet] [<stash>]
  help [command]                                                                 display help for command

@shadowspawn
Copy link
Collaborator

shadowspawn commented Feb 7, 2024

The only workaround is to include a newline in the usage string, but successive usages are not correctly indented.

This work-around seems to work reasonably, with manual indentation?

import { Command } from 'commander';
const program = new Command();

program.usage(`[options] [ script.js ] [arguments]
       node inspect [options] [ script.js | host:port ] [arguments]`);

program.parse();
% node node.mjs --help
Usage: node [options] [ script.js ] [arguments]
       node inspect [options] [ script.js | host:port ] [arguments]

Options:
  -h, --help  display help for command

@vassudanagunta
Copy link
Author

vassudanagunta commented Feb 7, 2024

This work-around seems to work reasonably, with manual indentation?

Hardcoding a 7 char indent is exactly the workaround I'm using. Sorry I didn't mention that. Smells a wee bit bad, but not a big deal ;)

In my particular use case, nested commands won't work, and in fact Option.conflicts isn't applicable either, as the command has different semantics (and options) depending on whether the first positional arg is a file or directory, similar to cp which I'll add as an example above.

@vassudanagunta

This comment was marked as off-topic.

@shadowspawn

This comment was marked as off-topic.

@shadowspawn
Copy link
Collaborator

Another possible approach is to include text in the long description, which is fairly freeform (and avoid the 7 char indent annoyance):

program.command('stash')
   .description(`Synopsis:
git stash list [<log-options>]
git stash show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>]
git stash drop [-q | --quiet] [<stash>]`)
   .summary('Stash the changes in a dirty working directory away');
% node description.mjs stash -h
Usage: description stash [options]

Synopsis:
git stash list [<log-options>]
git stash show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>]
git stash drop [-q | --quiet] [<stash>]

Options:
  -h, --help  display help for command

@shadowspawn
Copy link
Collaborator

This has not had any likes (yet) and neither did #80, but I am impressed by the examples here. Keeping this open for more consideration.

@vassudanagunta

This comment was marked as off-topic.

@vassudanagunta
Copy link
Author

vassudanagunta commented Mar 27, 2024

As I hinted before, this isn't a big deal. The workaround is fine.

That said, since this is JS, I think it would be easy enough to support usage accepting a string array and acting accordingly.
Alternatively, allow multiple calls to usage. Or both, as is the case with conflicts.

If you agree with one of the above, I could look into submitting a PR.

@vassudanagunta
Copy link
Author

vassudanagunta commented May 7, 2024

I thought about this a little trying to work around it for one of my CLIs. It's implicit for all the CLIs that have multiple usages that aren't keyed by subcommands like git stash that there is some way to distinguish which of the usages is in effect.

Looking at git rebase for example:

git rebase [-i | --interactive] [<options>] [--exec <cmd>]
               [--onto <newbase> | --keep-base] [<upstream> [<branch>]]
git rebase [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase>]
               --root [<branch>]
git rebase (--continue | --skip | --abort | --quit | --edit-todo | --show-current-patch)
  • --continue, --skip, etc are unique to usage 3.
  • -i narrows it to usage 1 or 2.
    • --keep-base narrows it to usage 1.
    • --root narrows it to usage 2.
    • two non-option arguments narrow it to usage 1.

So one way to implement this:

  1. Start with a list of all usages as candidates.
  2. For each parsed option narrow the candidate list.
  3. If an option isn't valid for any of the remaining candidates, see if it is valid for any of the eliminated usages.
    • If it is, then report it as a conflict
    • If not, report it as an unrecognized option.

In addition, it would be good to have a way to narrow the candidate list by the type of non-option argument. For example, cp as shown earlier has usages based on whether an argument is a file or directory. My personal use case has this as well. So perhaps a way to process non-option arguments (aka "remaining arguments"). For example:

program.
  .argument('<mode>', 'mode to use')
  .argument('<file>', 'string to split', validateCallback)

where the optional validateCallback returns true only if the argument matches whatever is expected of that argument, e.g. in this case that it exists in the file system and that it is a file and not a directory. For single usage cases, this adds a feature that currently does not exist: A way to validate non-option arguments (If you like this idea separate from supporting multiple usages, let me know and I can create a separate feature request). For multi-usage cases, it is also used to narrow the candidate list.

If you find this interesting, happy to iterate an API design with you if that helps.

@shadowspawn
Copy link
Collaborator

Note that custom processing of arguments is supported. See: https://github.com/tj/commander.js#custom-argument-processing

Like:

   .argument('<first>', 'integer argument', myParseInt)

@shadowspawn
Copy link
Collaborator

Validating the command-line arguments using the usage (strings or otherwise) is an interesting idea, but I think going to be too much work. Your examples and comments show the complicated ways utilities overload their usage, like cp file/directory argument type, or rebase with 7 modes based on the exclusive options.

Simply supporting calling usage multiple times should be technically easy, but the earlier issue got no upvotes in 7 years. Since there are easy work-arounds, I don't think it is worth adding without some upvotes or interest on this issue. (Which is why I haven't closed it yet, giving it a chance to gather some support.)

@shadowspawn
Copy link
Collaborator

I thought about this a little trying to work around it for one of my CLIs.

And to be clear, thanks for sharing. Both for explaining some of your own use-case, and the thoughts on possible approaches.

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

2 participants