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

Add subcommands support #211

Conversation

aaronccasanova
Copy link

@aaronccasanova aaronccasanova commented Jun 4, 2022

I've been experimenting with an alternative approach to PR# 205 for adding subcommands support. My favorite feature of meow is the objects as configuration convention for flags and I was interested in exploring how to carry over that pattern for subcommands. I'm pretty satisfied with the POC and would love some initial feedback before I continue iterating and moving this PR out of draft.

Key features:

  • Objects as configuration for subcommands
  • Out of the box support for nested subcommands
  • subcommands use the existing meow.options properties to keep the API minimal, extensible, and familiar
  • Using --help (with autoHelp: true) automatically shows the help text for the current subcommand
  • A new allowParentFlags option is introduced to restrict top level options.flags from being used with a given subcommand

Note: Since the subcommand.options recursively merge, authors have the ability to override meow.options uniquely for each subcommand

  • And finally, a simple return value (e.g. cli.command) containing the input args of the subcommand and a reference to the resolved subcommands options

Here is a subset of the proposed API! Though, I recommend pulling down the branch and testing out barista.js with the provided examples.

const cli = meow({
  description: "Your friendly neighborhood barista",
  help: `
    Usage:
      barista brew <beverage> <options>`,
  commands: {
    brew: {
      description: "A prompt to select what to brew",
      help: `
        Usage:
          barista brew <beverage> <options>`,
      flags: {
        temp: { type: "string", default: "hot" },
      },
      handler: () => console.log("What do you want to brew?"),
      commands: {
        coffee: {
          description: "Brew coffee",
          help: `
            Usage:
              barista brew coffee <options>`,
          handler: (cli) => console.log(`Here's your ${cli.flags.temp} coffee!`),
        },
        tea: {
          description: "Brew tea",
          help: `
            Usage:
              barista brew tea <options>`,
          handler: (cli) => console.log(`Here's your ${cli.flags.temp} tea!`),
        },
      },
    },
  },
});

cli.command.options.handler?.(cli)

image

@skoshx
Copy link

skoshx commented Jun 4, 2022

@aaronccasanova I actually prefer this implementation over mine, seems simpler. Just need Sindre's approval though I think they are quite busy with other projects :D

@corysimmons
Copy link

Great solution. Using this and it works perfect.

It does break redent (because it's looking at options.help instead of however deeply nested .help could be, so I'm just wrapping my nested helps in redent('my help text', 2) to fix it for now.

@sindresorhus
Copy link
Owner

Sorry for the late reply. I also think this is the way to go.

@skoshx skoshx mentioned this pull request Jul 24, 2022
@skoshx
Copy link

skoshx commented Jul 26, 2022

@aaronccasanova Do you want help with getting this draft merged? I'm willing to help to speed up the merging process :)

@aaronccasanova
Copy link
Author

Do you want help with getting this draft merged?

Hey @skoshx! Yes! My bandwidth is limited at the moment and I would greatly appreciate help getting this PR out of draft 🙌

Here are some outstanding todo items:

  • Write tests
  • Write documentation
  • Fix spacing issue (mentioned above)
  • Discuss and get sign-off on some of the POC APIs:
    • I tentatively included an allowParentFlags to restrict top level options.flags from being used with a given subcommand. Do we want this functionality? Should it be more flexible? Is it out of scope for v1 of subcommands?
    • You'll notice in my example I'm passing a handler callback to each subcommand options. While it doesn't factor into the implementation, I'm curious if there is a preference to encourage or discourage users adding arbitrary properties to the meow options?
    • Internally, subcommands are parsed and populate the cli.command object with the following shape:
      • Any thoughts or feedback about the return values and naming conventions?
// node barista.js brew tea
cli.command === {
  // Array of each subcommand passed to the CLI
  args: ['brew', 'tea'],
  options: {/* tea subcommand options */}
}

@aaronccasanova
Copy link
Author

Closing to avoid leaving stale PRs in this repository

@tommy-mitchell
Copy link
Contributor

@aaronccasanova I'd like to help get this merged. Some thoughts from trying to use this:

  • cli.input includes any parsed cli.command.args. I think cli.input should be amended to be all non-flag and non-command arguments
  • Is there any reason to access command.args? The parsed flags, help text, etc. are merged into cli anyway. cli.command could just be the user-inputted commands.
    • Related, should this be an array or just a normal string? I'm not sure what's easier for a user to compare against (e.g. cli.command === 'brew tea')
    • If no command is parsed, maybe cli.command should be omitted from the result?

You'll notice in my example I'm passing a handler callback to each subcommand options.

I don't think this kind of usage should be encouraged.

I tentatively included an allowParentFlags to restrict top level options.flags from being used with a given subcommand. Do we want this functionality?

I think this could be useful. Imagine if the top-level CLI defines a --verbose flag. Subcommands shouldn't have to redeclare that flag just to get access to it. There are some potential issues though:

  • If cli.help becomes the subcommand's help text, what happens to top-level options? Are users required to copy their usage information for a given flag to each subcommand?
  • Can a user restrict which top-level flags a subcommand can use? I'm not sure if there would even be a reason to, but it's worth asking.

I'd be happy to help, especially with types/documentation. I can also take a look at the redent issue.

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

Successfully merging this pull request may close these issues.

None yet

5 participants