-
-
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
Add support for "local" and global help:header, help:body and help:footer #1267
Conversation
The local listeners could be added manually everywhere with customisation via I don't think using a true global is the way to go for holding the listeners. The problem is if different packages in a program use Commander internally, they interact via any globals. Commander did start out as global only and have this problem, but we are now encouraging the use of A possible approach would be differently name events which are emitted by walking the command tree at runtime, following the |
(And thanks for the contribution!) |
Hi, I would like this for my own, yes. Overriding every single help to add a simple header gets old fast. And yes, I saw your "help issues compilation" and thought the With that said, I think your comments on the "local" listeners wasn't clear on your comment, do you think it can work the way I implemented? I can always remove the global listeners if the local ones are "go". 😄 |
Yes in general, that was what I had in mind, but l have a lot of suggestions on the details. You don't need to act on them at this stage unless you wish, consider them discussion points. Giving me an opportunity to develop the skeleton idea you started from. |
| 'help:header' | ||
| 'help:body' | ||
| 'help:footer' | ||
| (string & {}) |
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.
This does indeed seem to work in Visual Studio Code. Do you have any documentation or a source for what (string & {})
is doing to the type? An intersection of a string and an object with no properties?
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.
I went to hunt for some sort of explanation on this but couldn't find any. So I'll try to explain what I believe happens.
An empty interface {}
is almost like an "any", except it doesn't allow the undefined | null
, we can see it if you parse the code below on an editor:
interface Empty {}
const n:Empty = 0
const s:Empty = 'a'
const a:Empty = []
const o:Empty = {}
const t:Empty = true
const f:Empty = false
const sy:Empty = Symbol()
const u:Empty = undefined //error
const nu:Empty = null //error
So the typescript intersection (string & {})
would mean it has to be a string but can't be null or undefined.
When you declare const foo: 'a' | 'b' | string = ''
typescript seems to coerce all the values into the string
type. But when using (string & {})
, it doesn't, because it is not expecting a "true" string.
The two drawbacks I can find or thing of are:
In some typeguard functions like the one below, it will throw an error, though it is easily bypass-able.
function foo(arg: 'a' | 'b' | (string & {})): 'a' | undefined {
if (arg === 'a') {
return arg; // Type '(string & {}) | "a"' is not assignable to type '"a"'
//return arg as 'a'; // this works
}
return
}
The other drawback might be that this could be patched in the future, since I couldn't find any documentation or explanation for it. Though I suspect if patched, it would behave just like a string
type instead. But who knows...
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.
An alternative to this would be to overload the "on" function with
on(event: 'help:header' | 'help:body' | 'help:footer' | symbol, listener: (...args: any[]) => void): this;
on(event: string, listener: (...args: any[]) => void): this;
.on('help:header', () => { | ||
console.log('Custom Header\n'); | ||
}) | ||
.on('help:body', (instance) => { | ||
console.log('------------'); | ||
console.log(instance.helpInformation().replace(/\n$/, '\n------------\n')); | ||
}) | ||
.on('help:footer', () => { | ||
console.log(`Commander - ${new Date().getFullYear()}`); | ||
}) |
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.
The replace and \n
are a bit confusing here. I eventually recalled that helpInformation()
has a trailing return for legacy reasons because it is used with process.stdout.write
. I suggest a simpler more symmetrical pattern like:
.on('help:header', () => {
console.log('Custom Header\n');
})
.on('help:body', (instance) => {
console.log('------------');
process.stdout.write(instance.helpInformation()); // has trailing eol
console.log('------------');
})
.on('help:footer', () => {
console.log(`\nCommander - ${new Date().getFullYear()}`);
})
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.
yeah, what you suggested was close to my first approach I believe, but I wanted the output to be neat. But indeed it is much better to read.
Event parameter. I had been thinking we would pass the |
this._helpHeader = 'help:header'; | ||
this._helpBody = 'help:body'; | ||
this._helpFooter = 'help:footer'; |
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.
These can be used as literals rather than having a property for them. You were probably copying the pattern for _helpLongFlag
but it is a property because it can be changed by the user.
This comment has been minimized.
This comment has been minimized.
globalEventEmitter.emit(this._helpBody, this); | ||
this.emit(this._helpBody, this); |
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.
I think only one of the body events should get emitted, the local one in preference to the global one.
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.
Yeah I think it might be better too, I was in doubt of what to do there, thanks :)
const globalBodyListeners = globalEventEmitter.listeners(this._helpBody); | ||
const bodyListeners = this.listeners(this._helpBody); |
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.
I have been moving to a different pattern for testing listeners, since we don't call the listener directly we do not need the list. Just check the count. An example of pattern, not a direct code replacement:
if (this.listenerCount('help:body')) {
this.emit('help:body', this);
} else {
process.stdout.write(cbOutput);
}
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.
How is this.litenerCount
supposed to know how many listeners there are? Or is this just an indirect call to this.listeners('whatever').length
?
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.
listenerCount
is an EventEmitter method: https://nodejs.org/api/events.html#events_emitter_listenercount_eventname
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.
Oops. Yeah is missed that. 😅
@@ -1530,12 +1534,23 @@ class Command extends EventEmitter { | |||
return passthru; | |||
}; | |||
} | |||
globalEventEmitter.emit(this._helpHeader, this); | |||
this.emit(this._helpHeader, this); | |||
const cbOutput = cb(this.helpInformation()); |
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.
In theory we should try and include or make available cb in the processing of the listener too, but that callback is a pattern I would like to deprecate. Instead, the processing of the callback can shift into the non-override body block since no need to call if not being displayed.
That is probably enough comments from me for today! |
Prompted by this PR, I had a rethink of the event names to include the extra level, as I don't think the page layout terms work as well for that (i.e. header, body, footer). This is my favourite so far: 1. |
I decided to pass "this" to the listener so you would be able to check all the commands/parsed arguments or anything really if you wanted to customize the help while using an arrow function. In theory the "local" listeners already pass "this" to |
I think it is better than header/footer, but I prefer the 1. |
I've been looking more through the code to see how to improve this PR. And I had not notice that we always have access to So the idea would be to search for every - command('foo')
- command('bar').addListener('globalHelp')
- command('bas')
- command('xoo')
- command('yoo') foo # not
foo bar # trigger
foo bar bas # trigger
foo xoo # not
foo xoo yoo # not What are the advantages of this vs always triggering the global listener only the "root" command? |
All of the globalPreHelp would be called. Only the closest globalHelp would be called. Yes, the listener is only "global" from that point in the tree, and not truly global. I am not entirely happy with it myself! I think the most command usage will be adding it to the root command, but I would prefer more accurate wording which is still easy to understand. The qualifying word could go on either the "global" flavour or the "local" flavour. Alternative suggestions or examples welcome!
This does allow a custom behaviour for a subcommand and all its subcommands (say to add a description of shared options) |
I spent some time wondering about other words and looking for other uses. I came across "group" to describe a command and its subcommands. So could look like:
|
I'll mention the other thing I want to consider at the same time as part of adding new events. When the help is being displayed due to a usage error, it should be displayed on stderr rather than stdout. This has only come up once I am aware of in #997, so not much demand. To make it easy for implementers I am thinking probably want easy access to both a Since most people will only want one or the other, I was thinking they could go in a "context" object. I have looked for the stdout/stderr problem being solved elsewhere and not found examples. I might be missing a simpler solution! |
Trying out the context pattern idea, parameter could look like:
And be used like:
|
Why would someone use |
|
Ok if I work on this too @JCQuintas , or would you prefer I leave it to you for now? |
Sure, go ahead. :) |
Experimenting on branch: https://github.com/shadowspawn/commander.js/tree/feature/on-help |
I'll close this in favour of #1296, with especially the global listener idea here inspiring the current form of that PR. I am intrigued by the TypeScript trick to get Intellisense suggesting event strings in Visual Studio Code, but without some backing references it feels a bit too hacky. Thank you for your contributions. |
Pull Request
Problem
This adds the possibility to customize the header, body and footer of the help command. In theory this will also allow for the "suppression" of the help command if you supply an empty function to the
"help:body"
listener.It solves part of the problems listed in #1225
Solution
There are two solutions on this PR:
Local or Scoped Help Listeners
For "local" or "scoped" help, we now have three listenable events. Which are called in the following order:
help:header
help:body
help:footer
When you add a listener to
help:body
, the default help text won't be written to thestdout
.All emitters send their scoped
this
to the listeners as a parameter.Global Help Listeners
The global listeners behave similarly to local listeners and use the same keys, but affect all the help calls. Root and children commands. Local commands will still be called and the order is:
help:header
help:header
help:body
help:body
help:footer
help:footer
Typings, tests and documentation
ChangeLog