-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
feat: add preSubcommand hook #1763
Conversation
I think this fits in with the idea of life cycle hooks. The action hooks get called with the command the hook is attached to, and the command with the action handler. I wonder if the preSubcommand hook should get passed the command the hook is attached to, and the subcommand which is about to be called. |
An example program for thinking about expected callbacks. This has deeply nested commands. Should the program callback get called just once before the direct subcommand, or multiple times from each level of subcommand? program
.name('program')
.hook('preSubCommand', (thisCommand, subcommand) => {
console.log(`pre ${thisCommand.name()}/${subcommand.name()}}`)
});
program
.command('first')
.command('second')
.command('third');
program.parse(['first', 'second', 'third'], { from: 'user' }); Current behaviour at time of writing: % node index.js
pre program/first}
pre program/second}
pre program/third} |
Yes, that doesn't look right, it should only run once normally, repeated operation will cause trouble and meaningless. |
Lines 1203 to 1205 in 3ae30a2
|
That test won't work. The operands includes command-arguments too. Like: |
I was wondering about only calling the hook for the immediate subcommands like: _chainOrCallSubcommandHook(promise, subCommand, event) {
let result = promise;
if (this._lifeCycleHooks[event] !== undefined) {
this._lifeCycleHooks[event].forEach((hook) => {
result = this._chainOrCall(result, () => {
return hook(this, subCommand);
});
});
}
return result;
}
For the call:
The output would be:
This feels quite natural for subcommand processing, but is quite different than the way the action hooks work! |
@shadowspawn I feel like I'm in the wrong process, what you said makes sense. |
I totally agree with #1763 (comment)
|
|
The hook event name should be |
These two previous issues ask about passing program options into executable subcommands: #563 #1137 This PR is not a direct solution, but the preSubcommand hooks does allow modifying the environment variables before calling subcommand. (I don't think the parsing is currently set up to allow directly modifying the remaining arguments to be parsed, which was also mentioned as a possible approach. Future middleware concept!) |
How about a table? The supported events are:
Raw markdown:
|
Yes, it looks much clearer |
Here is a big rework of the existing hook example file to include all the events, with just one subcommand, and one example call each. (Using port idea from your example.) #!/usr/bin/env node
// const { Command, Option } = require('commander'); // (normal include)
const { Command, Option } = require('../'); // include commander in git clone of commander repo
const program = new Command();
// This example shows using some hooks for life cycle events.
const timeLabel = 'command duration';
program
.option('--profile', 'show how long command takes')
.hook('preAction', (thisCommand) => {
if (thisCommand.opts().profile) {
console.time(timeLabel);
}
})
.hook('postAction', (thisCommand) => {
if (thisCommand.opts().profile) {
console.timeEnd(timeLabel);
}
});
program
.option('--trace', 'display trace statements for commands')
.hook('preAction', (thisCommand, actionCommand) => {
if (thisCommand.opts().trace) {
console.log('>>>>');
console.log(`About to call action handler for subcommand: ${actionCommand.name()}`);
console.log('arguments: %O', actionCommand.args);
console.log('options: %o', actionCommand.opts());
console.log('<<<<');
}
});
program
.option('--env <filename>', 'specify environment file')
.hook('preSubcommand', (thisCommand, subcommand) => {
if (thisCommand.opts().env) {
// One use case for this hook is modifying environment variables before
// parsing the subcommand, say by reading .env file.
console.log(`Reading ${thisCommand.opts().env}...`);
process.env.PORT = 80;
console.log(`About to call subcommand: ${subcommand.name()}`);
}
});
program.command('start')
.argument('[script]', 'script name', 'server.js')
.option('-d, --delay <seconds>', 'how long to delay before starting')
.addOption(new Option('-p, --port <number>', 'port number').default(8080).env('PORT'))
.action(async(script, options) => {
if (options.delay) {
await new Promise(resolve => setTimeout(resolve, parseInt(options.delay) * 1000));
}
console.log(`Starting ${script} on port ${options.port}`);
});
// Some of the hooks or actions are async, so call parseAsync rather than parse.
program.parseAsync().then(() => {});
// Try the following:
// node hook.js start
// node hook.js --trace start --port 9000 test.js
// node hook.js --profile start --delay 5
// node hook.js --env=production.env start |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a couple of minor changes to README references to example file to do.
LGTM
Thanks for your help 😃 @shadowspawn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awsome!
Released in Commander v9.4.0 Thank you for your contributions. |
Pull Request
Problem
Read
--env
options and process it before subcommand parsed.Closes #1762
Solution
Add
preSubcommand
hook to handle some operations that take effect for all subcommands.ChangeLog