skip to Main Content

I’m trying to implement prompting users for missing required args/mandatory options.

I thought I could use a preSubcommand hook, change process.argv with prompts, and then allow commander to parse the updated argv.

My hook gets executed before any exit however, After the complete of my hook’s callback, I will get this error: error: required option '--foo' not specified.

It seems that parsing continues after the hook on the old process.argv.

For example my-cli subcommand --foo bar will work fine.
my-cli subcommand will prompt for foo and push it to process.argv inside the hook but then exit with the error above.

Any ideas? I tried to suppress this error with exitOverride and re-calling parse afterwards, but my callback is not being reached. Surrounding in try-catch doesn’t work either.

Here’s my code:

program
        .name('cli')
        .description('foo')
        .version(version, '-v, --version')
        .addCommand(sub)
        .hook('preSubcommand', async (program, subcommand) => {
            await promptForMissingArgs(program, subcommand);
            console.log('here');
            console.log(process.argv);
            program.parse(process.argv); // called again with correct process.argv but still exits
        })
        .exitOverride((exitCode) => {
            console.log(`Exiting with code ${exitCode.exitCode}`); // not reached
            //  if (exitCode.exitCode !== 0) {
            //     program.parse(process.argv);
            //  }
        });
    // parse command line arguments and execute command
    try {
        program.parse(process.argv);
    } catch (error) {
        console.log('here'); // doesn't reach here
        console.error(error);
    }

From the docs I see this: if the first arg is a subcommand call preSubcommand hooks pass remaining arguments to subcommand, and process same way
https://github.com/tj/commander.js/blob/HEAD/docs/parsing-and-hooks.md

Is it because my hook is async?

2

Answers


  1. Chosen as BEST ANSWER

    Instead of trying to change process.argv, I was able to set options during preSubcommand hook. I was not able to find a way to change argument values so I settled with options

    import { Command } from 'commander';
    
    const program = new Command();
    const subcommand= new Command();
    
    (async () => {
    subcommand
      .option('-p, --port <number>')
      .action(async (options) => {
        console.log(options);
      });
    
    program.addCommand(subcommand)
      .hook('preSubcommand', async (thisCommand, sub) => {
        await promptForMissingArgs(thisCommand, sub);
      })
    
    await program.parseAsync();
    })();
    

    With promptForMissingArgs implementation something like this:

    async function promptForMissingArgs(command, subcommand) {
      const answers = inquirer.prompt(getMissingFromArgv(subcommand.registeredArguments ));
      Object.keys(answers).forEach(ans => subcommand.setOptionValue(ans, answers[ans]));
    }
    
    function getMissingFromArgv(registeredArgs ) {
      // compare process.argv against registeredArgs to see what is missing
      // return list of prompts for inquirer
    }
    

  2. Commander does not support mutating the arguments to be parsed in the middle of parsing. Also, there isn’t an easy place to inject last-minute processing before the check for mandatory options.

    What you can easily do is prompt for missing options without marking them as mandatory as such. A simple place to prompt is at the start of the action handler, but a hook may allow code reuse or separate your logic better. Here is an example with a hook in the spirit of your code.

    import { Command } from 'commander';
    
    const program = new Command();
    
    program
      .option('-p, --port <number>')
      .hook('preAction', async (thisCommand) => {
        const options = thisCommand.opts();
        // Simulate prompting for and setting missing options.
        if (!options.port) {
          console.log('You forgot to specify a port number.');
          thisCommand.setOptionValue('port', '3000');
        }
      })
      .action(async (options) => {
        console.log(options);
      });
    
    program.parseAsync();
    
    % node index.mjs -p 80
    { port: '80' }
    % node index.mjs      
    You forgot to specify a port number.
    

    (Disclaimer: I am a maintainer of Commander.)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search