Commander.js: feature request: pre run and post run hooks

Created on 15 Feb 2020  路  11Comments  路  Source: tj/commander.js

How about option to add a preRun and a postRun function to each command to run before or after the main function?

enhancement

All 11 comments

This idea does come up occasionally. We need a good idea of what problem this is solving.

What problem would this help with? What is an example of what you would do in the preRun or postRun, and how would this be better than just calling it from the main function?

Related issues: #76 #229 #936 #996 #1158

Opening database connection, creating directories or log file, etc.
We can call it from the main function but i guess it is better if we let the main function be about the command it self and not some repetitive functions.

Here is an example of a commander with support of prerun and postrun hooks

https://github.com/spf13/cobra#prerun-and-postrun-hooks

I looked at Cobra recently, has a lot of features. Good example thanks.

I see for pre/post Cobra has not two but four extra functions, with two of them specific to the command, and two of them "persistent" (I would have said inherited) and passed on to subcommands.

My first impression is pre/post hooks are going to be somewhat complex to use and of limited use.

I'm happy to leave this issue open though to see whether it generates further interest or insights.

(I know people are interested in somewhere to put some sorts of common code, and a hook after parsing but before the command handling, and don't have a good feel for whether pre/postrun is the best pattern for these.)

One implementation complication with pre/post hooks would be if they should support async.

Hi,

We develop relatively extensive CLI (190 categories + 670 operations = 860 commands). Our CLI is contains multiple nested groups of commands. We are currently using structured-cli, which is abandoned and causes problems that require solutions in the engine. We tried to use commander.js. We quickly came across needs of plugins.

Our CLI is designed to manage cloud computing services. In practice, it is calling the platform API.

I developed my own mechanism around commander.js while trying to make a new CLI with it. So far I prepared plugins such as:

  • api - provides API client initialization, including ensuring that the user has previously executed login commands, and if not - rejects the command until logging in, also supports the injection of additional login-related ones,
  • formatOutput - adds parameters responsible for output formatting (similar to the "--output" and "--query" parameters available in CLI gcloud, aws and az), and also format the output accordingly,
  • noWait - modifies the client-API so that it does not wait for the operation to complete after it is accepted by the API,
  • validatorChoices - introduces parameter verifications in the range of acceptable values, e.g. acceptable output formats.
  • formatError - format errors consuming standard error API format

The developed plugin mechanism turns out to be quite flexible in complementing the restrictions of commander.js in parameter parsing. Our plugins implementation wrap action in wrapper and promisify everyting:

const createCommand = (options) => {
    options.handler = options.handler || (() => {
        throw new Error("Not implemented")
    })
    options.plugins = options.plugins || [];
    options.parameters = options.parameters || {};

    const cmd = new Command(options.name).storeOptionsAsProperties(false);
    if (options.description) {
        cmd.description(options.description);
    }

    for (const plugin of options.plugins.filter(x => x.beforeParameter)) {
        plugin.beforeParameter(options);
    };

    for (const plugin of options.plugins.filter(x => x.parameters)) {
        Object.assign(options.parameters, plugin.parameters)
    };

    for (const [name, config] of Object.entries(options.parameters)) {
        const flag = config.type == 'boolean' ? `--${name}` :`--${name} [${(config.placeholder || name).toUpperCase()}]`
        cmd._optionEx(
            { mandatory: config.required || false, },
            flag,
            config.description,
            config.validator,
            config.defaultValue
        )
    }

    const toolbox = {};

    const wrapper = () => {
        const ctx = { input: cmd.opts(), options, toolbox };
        let p = Promise.resolve(ctx);

        for (const plugin of options.plugins.filter(x => x.beforeHandler)) {
            p = p.then(() =>
                Promise.resolve(timingPromise(`${plugin.name}:beforeHandler`, plugin.beforeHandler(ctx)))
            );
        }

        p = p.then(() => timingPromise('handler', Promise.resolve(options.handler(ctx))))

        for (const plugin of options.plugins.filter(x => x.afterHandler)) {
            p = p.then(output => {
                return timingPromise(`${plugin.name}:afterHandler`, Promise.resolve(plugin.afterHandler(ctx, output)))
            });
        }

        for (const plugin of options.plugins.filter(x => x.error)) {
            p = p.catch(err =>
                timingPromise(`${plugin.name}:error`, Promise.resolve(plugin.error(ctx, err)))
            );
        }
        return p;
    };
    cmd.action(wrapper);

    return cmd;
}

From our point of view - promise in plugins is necessary to, e.g. download additional information from the API, e.g. regarding user login, but also in the case of verification of parameters provided by the user (currently we verify requests only after sending to the API).

As you can see, we will pack commander.js quite heavily to be able to use it for our needs. We currently have over 700 commands in CLI based on commander.js (most of them are generated on the basis of machine-readable documentation), but we're considering whether using commander.js is the way to go.

I hope these comments are helpful. I was considering using koa-compose for this, but I finally decided not to build our plugin interface around middleware.

Very interesting, thanks @ad-m

An additional note, the toolbox is a container that is forwarded and we anticipate that various plugins will add tools to it. Glugun have a lot of extensions like that: https://github.com/infinitered/gluegun/tree/master/src/core-extensions

@shadowspawn: The heart of the problem is that doing argument processing and execution as a single step has lost a phase distinction that matters for a whole bunch of CLIs. Offhand, I can't think of the last time I wrote a CLI application in UNIX that _didn't_ need to do some form of configuration or validation step based on argument input before running the core of the application. I'm actually very puzzled that essentially _every_ CLI handling package I have looked at seems to have omitted this phase distinction.

A common example is needing to process a config file whose filename can be overridden on the command line. You _can't_ process it until the command line is parsed (so you know the filename), but you _must_ process it before execution begins. Commander can handle this case by combining requiredOption() with a default, which I didn't see until I read the code. Nice job on that one, but that's a common enough case to be worth describing in the quick docs. It's not super obvious that one reason to use requiredOption() is to force the callback to happen when you have a default.

Another common example is anything that requires a sanity cross-check between multiple options. E.g. for mutual exclusion. The negatable boolean [no-]cheese/sauce example in your docs is a case where conflicting options should be diagnosed rather than accepted. That particular example can be managed adequately with callbacks, but that solution doesn't scale well. Consider what that callback would have to look like for a commander-based version of the UNIX _find_ or _cpio_ commands. The config file case can become an example of this if you want command line options to be able to override the defaults from the config file.

Early morning here after a short sleep, so that may not read as clearly as I think it does. Don't hesitate to reach out if clarification is helpful. I'd be glad to do the change and send you a pull request if you like.

Well darn. The requiredOption() callback _doesn't_ get called unless the option is actually passed on the command line. Bother.

And now that I look closer, i see why the phase distinction I was hoping for does not and cannot exist. I think I see where and how to emit an event. Pull request for you shortly.

Separating the argument processing and execution would be hard, but might offer a simpler pattern than callbacks and events and hooks for at least some cases. Conceptually:

const details = program.phase1_parse();
myPreRunProcessing(program, details);
program.phase2_run(stuff);

oclif has "hooks": https://oclif.io/docs/hooks#lifecycle-events (init, prerun, postrun, command_not_found)
yargs has "middleware:: http://yargs.js.org/docs/#api-reference-middlewarecallbacks-applybeforevalidation

Was this page helpful?
0 / 5 - 0 ratings

Related issues

san-templates picture san-templates  路  5Comments

hossamelmansy picture hossamelmansy  路  4Comments

RoXioTD picture RoXioTD  路  4Comments

oknoorap picture oknoorap  路  4Comments

shadowspawn picture shadowspawn  路  4Comments