From a9765ed36f9db455a49a6bea77e27978c9b3bdba Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 29 Nov 2020 18:55:03 +1300 Subject: [PATCH 1/5] Add proof of concept of CommandStrict --- index.js | 19 ++++ typings/commander-tests.ts | 197 +++++++++++++++++++------------------ typings/index.d.ts | 49 +++++---- 3 files changed, 150 insertions(+), 115 deletions(-) diff --git a/index.js b/index.js index 668c0e775..a45863c44 100644 --- a/index.js +++ b/index.js @@ -2038,6 +2038,24 @@ Expecting one of '${allowedValues.join("', '")}'`); } }; +class CommandStrict extends Command { + /** + * Initialize a new `CommandStrict` object, which adopts new patterns and best practices + * more aggressively than Command. + * + * @param {string} [name] + */ + constructor(name) { + super(name); + this.storeOptionsAsProperties(false); + this.passCommandToAction(false); + } + + createCommand(name) { + return new CommandStrict(name); + }; +} + /** * Expose the root command. */ @@ -2050,6 +2068,7 @@ exports.program = exports; // More explicit access to global command. */ exports.Command = Command; +exports.CommandStrict = CommandStrict; exports.Option = Option; exports.CommanderError = CommanderError; exports.InvalidOptionArgumentError = InvalidOptionArgumentError; diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index fed35998c..f53a7ea00 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -1,4 +1,5 @@ import * as commander from './index'; +import { command } from './index'; // Test Commander usage with TypeScript. // This is NOT a usable program, just used to test for compile errors! @@ -9,17 +10,8 @@ import * as commander from './index'; // This conflicts with esline rule saying no space! /* eslint-disable @typescript-eslint/space-before-function-paren */ -// Defined stricter type, as the options as properties `[key: string]: any` -// makes the type checking very weak. -// https://github.com/Microsoft/TypeScript/issues/25987#issuecomment-441224690 -type KnownKeys = { - [K in keyof T]: string extends K ? never : number extends K ? never : K -} extends {[_ in keyof T]: infer U} ? ({} extends U ? never : U) : never; -type CommandWithoutOptionsAsProperties = Pick>; - -const program: CommandWithoutOptionsAsProperties = commander.program; -const programWithOptions = commander.program; -// program.silly; // <-- Error, hurrah! +const program: commander.CommandStrict = new commander.CommandStrict(); // Use CommandStrict for stricter type checking in this file. +const programWithOptions: commander.Command = commander.program; // Check for exported global Command objects const importedDefaultProgram: commander.Command = commander; @@ -28,47 +20,57 @@ const importedExplicitProgram: commander.Command = commander.program; // Check export classes exist const commandInstance1 = new commander.Command(); const commandInstance2 = new commander.Command('name'); +const commandStrictInstance1 = new commander.CommandStrict(); +const commandStrictInstance2 = new commander.CommandStrict('name'); const optionsInstance = new commander.Option('-f'); const errorInstance = new commander.CommanderError(1, 'code', 'message'); const invalidOptionErrorInstance = new commander.InvalidOptionArgumentError('message'); -// Command properties +// Command with options stored as properties +// console.log(program.someOption); // Property 'someOption' does not exist on type 'CommandStrict'. console.log(programWithOptions.someOption); // eslint-disable-next-line @typescript-eslint/dot-notation console.log(programWithOptions['someOption']); const theArgs = program.args; -const theCommands: commander.Command[] = program.commands; +const theCommands: commander.CommandInterface[] = program.commands; + +// CommandStrict is actually implemented by deriving from Command, so check assignment is allowed in TypeScript. +const assignmentCommand: commander.Command = new commander.CommandStrict(); // version -const versionThis1: commander.Command = program.version('1.2.3'); -const versionThis2: commander.Command = program.version('1.2.3', '-r,--revision'); -const versionThis3: commander.Command = program.version('1.2.3', '-r,--revision', 'show revision information'); +const versionThis1: commander.CommandStrict = program.version('1.2.3'); +const versionThis2: commander.CommandStrict = program.version('1.2.3', '-r,--revision'); +const versionThis3: commander.CommandStrict = program.version('1.2.3', '-r,--revision', 'show revision information'); // command (and CommandOptions) -const commandNew1: commander.Command = program.command('action'); -const commandNew2: commander.Command = program.command('action', { isDefault: true, hidden: true, noHelp: true }); -const commandThis1: commander.Command = program.command('exec', 'exec description'); -const commandThis2: commander.Command = program.command('exec', 'exec description', { isDefault: true, hidden: true, noHelp: true, executableFile: 'foo' }); +const commandNew1: commander.CommandStrict = program.command('action'); +const commandNew2: commander.CommandStrict = program.command('action', { isDefault: true, hidden: true, noHelp: true }); +const commandThis1: commander.CommandStrict = program.command('exec', 'exec description'); +const commandThis2: commander.CommandStrict = program.command('exec', 'exec description', { isDefault: true, hidden: true, noHelp: true, executableFile: 'foo' }); // addCommand -const addCommandThis: commander.Command = program.addCommand(new commander.Command('abc')); +const addCommandThis1: commander.CommandStrict = program.addCommand(new commander.ComCommandStrictmand('abc')); +const addCommandThis2: commander.Command = programWithOptions.addCommand(new commander.Command('abc')); +// and again with mixed classes +const addCommandThis3: commander.CommandStrict = program.addCommand(new commander.Command('abc')); +const addCommandThis4: commander.Command = programWithOptions.addCommand(new commander.CommandStrict('abc')); // arguments -const argumentsThis: commander.Command = program.arguments(' [env]'); +const argumentsThis: commander.CommandStrict = program.arguments(' [env]'); // addHelpCommand -const addHelpCommandThis1: commander.Command = program.addHelpCommand(); -const addHelpCommandThis3: commander.Command = program.addHelpCommand(false); -const addHelpCommandThis2: commander.Command = program.addHelpCommand(true); -const addHelpCommandThis4: commander.Command = program.addHelpCommand('compress '); -const addHelpCommandThis5: commander.Command = program.addHelpCommand('compress ', 'compress target file'); +const addHelpCommandThis1: commander.CommandStrict = program.addHelpCommand(); +const addHelpCommandThis3: commander.CommandStrict = program.addHelpCommand(false); +const addHelpCommandThis2: commander.CommandStrict = program.addHelpCommand(true); +const addHelpCommandThis4: commander.CommandStrict = program.addHelpCommand('compress '); +const addHelpCommandThis5: commander.CommandStrict = program.addHelpCommand('compress ', 'compress target file'); // exitOverride -const exitThis1: commander.Command = program.exitOverride(); -const exitThis2: commander.Command = program.exitOverride((err): never => { +const exitThis1: commander.CommandStrict = program.exitOverride(); +const exitThis2: commander.CommandStrict = program.exitOverride((err): never => { return process.exit(err.exitCode); }); -const exitThis3: commander.Command = program.exitOverride((err): void => { +const exitThis3: commander.CommandStrict = program.exitOverride((err): void => { if (err.code !== 'commander.executeSubCommandAsync') { throw err; } else { @@ -77,18 +79,18 @@ const exitThis3: commander.Command = program.exitOverride((err): void => { }); // action -const actionThis1: commander.Command = program.action(() => { +const actionThis1: commander.CommandStrict = program.action(() => { // do nothing. }); -const actionThis2: commander.Command = program.action(async() => { +const actionThis2: commander.CommandStrict = program.action(async() => { // do nothing. }); // option -const optionThis1: commander.Command = program.option('-a,--alpha'); -const optionThis2: commander.Command = program.option('-p, --peppers', 'Add peppers'); -const optionThis3: commander.Command = program.option('-s, --string [value]', 'default string', 'value'); -const optionThis4: commander.Command = program.option('-b, --boolean', 'default boolean', false); +const optionThis1: commander.CommandStrict = program.option('-a,--alpha'); +const optionThis2: commander.CommandStrict = program.option('-p, --peppers', 'Add peppers'); +const optionThis3: commander.CommandStrict = program.option('-s, --string [value]', 'default string', 'value'); +const optionThis4: commander.CommandStrict = program.option('-b, --boolean', 'default boolean', false); program.option('--drink ', 'float argument', parseFloat); -const optionThis6: commander.Command = program.option('-f, --float ', 'float argument', parseFloat, 3.2); -const optionThis7: commander.Command = program.option('-i, --integer ', 'integer argument', myParseInt); -const optionThis8: commander.Command = program.option('-i, --integer ', 'integer argument', myParseInt, 5); -const optionThis9: commander.Command = program.option('-v, --verbose', 'verbosity that can be increased', increaseVerbosity, 0); -const optionThis10: commander.Command = program.option('-c, --collect ', 'repeatable value', collect, []); -const optionThis11: commander.Command = program.option('-l, --list ', 'comma separated list', commaSeparatedList); +const optionThis5: commander.CommandStrict = program.option('-f, --float ', 'float argument', parseFloat); +const optionThis6: commander.CommandStrict = program.option('-f, --float ', 'float argument', parseFloat, 3.2); +const optionThis7: commander.CommandStrict = program.option('-i, --integer ', 'integer argument', myParseInt); +const optionThis8: commander.CommandStrict = program.option('-i, --integer ', 'integer argument', myParseInt, 5); +const optionThis9: commander.CommandStrict = program.option('-v, --verbose', 'verbosity that can be increased', increaseVerbosity, 0); +const optionThis10: commander.CommandStrict = program.option('-c, --collect ', 'repeatable value', collect, []); +const optionThis11: commander.CommandStrict = program.option('-l, --list ', 'comma separated list', commaSeparatedList); // requiredOption, same tests as option -const requiredOptionThis1: commander.Command = program.requiredOption('-a,--alpha'); -const requiredOptionThis2: commander.Command = program.requiredOption('-p, --peppers', 'Add peppers'); -const requiredOptionThis3: commander.Command = program.requiredOption('-s, --string [value]', 'default string', 'value'); -const requiredOptionThis4: commander.Command = program.requiredOption('-b, --boolean', 'default boolean', false); +const requiredOptionThis1: commander.CommandStrict = program.requiredOption('-a,--alpha'); +const requiredOptionThis2: commander.CommandStrict = program.requiredOption('-p, --peppers', 'Add peppers'); +const requiredOptionThis3: commander.CommandStrict = program.requiredOption('-s, --string [value]', 'default string', 'value'); +const requiredOptionThis4: commander.CommandStrict = program.requiredOption('-b, --boolean', 'default boolean', false); program.requiredOption('--drink ', 'float argument', parseFloat); -const requiredOptionThis6: commander.Command = program.requiredOption('-f, --float ', 'float argument', parseFloat, 3.2); -const requiredOptionThis7: commander.Command = program.requiredOption('-i, --integer ', 'integer argument', myParseInt); -const requiredOptionThis8: commander.Command = program.requiredOption('-i, --integer ', 'integer argument', myParseInt, 5); -const requiredOptionThis9: commander.Command = program.requiredOption('-v, --verbose', 'verbosity that can be increased', increaseVerbosity, 0); -const requiredOptionThis10: commander.Command = program.requiredOption('-c, --collect ', 'repeatable value', collect, []); -const requiredOptionThis11: commander.Command = program.requiredOption('-l, --list ', 'comma separated list', commaSeparatedList); +const requiredOptionThis5: commander.CommandStrict = program.requiredOption('-f, --float ', 'float argument', parseFloat); +const requiredOptionThis6: commander.CommandStrict = program.requiredOption('-f, --float ', 'float argument', parseFloat, 3.2); +const requiredOptionThis7: commander.CommandStrict = program.requiredOption('-i, --integer ', 'integer argument', myParseInt); +const requiredOptionThis8: commander.CommandStrict = program.requiredOption('-i, --integer ', 'integer argument', myParseInt, 5); +const requiredOptionThis9: commander.CommandStrict = program.requiredOption('-v, --verbose', 'verbosity that can be increased', increaseVerbosity, 0); +const requiredOptionThis10: commander.CommandStrict = program.requiredOption('-c, --collect ', 'repeatable value', collect, []); +const requiredOptionThis11: commander.CommandStrict = program.requiredOption('-l, --list ', 'comma separated list', commaSeparatedList); // createOption const createOption1: commander.Option = program.createOption('a, --alpha'); const createOption2: commander.Option = program.createOption('a, --alpha', 'description'); // addOption -const addOptionThis: commander.Command = program.addOption(new commander.Option('-s,--simple')); +const addOptionThis: commander.CommandStrict = program.addOption(new commander.Option('-s,--simple')); // storeOptionsAsProperties -const storeOptionsAsPropertiesThis1: commander.Command = program.storeOptionsAsProperties(); -const storeOptionsAsPropertiesThis2: commander.Command = program.storeOptionsAsProperties(false); +const storeOptionsAsPropertiesThis1: commander.CommandStrict = program.storeOptionsAsProperties(); +const storeOptionsAsPropertiesThis2: commander.CommandStrict = program.storeOptionsAsProperties(false); // passCommandToAction -const passCommandToActionThis1: commander.Command = program.passCommandToAction(); -const passCommandToActionThis2: commander.Command = program.passCommandToAction(false); +const passCommandToActionThis1: commander.CommandStrict = program.passCommandToAction(); +const passCommandToActionThis2: commander.CommandStrict = program.passCommandToAction(false); // combineFlagAndOptionalValue -const combineFlagAndOptionalValueThis1: commander.Command = program.combineFlagAndOptionalValue(); -const combineFlagAndOptionalValueThis2: commander.Command = program.combineFlagAndOptionalValue(false); +const combineFlagAndOptionalValueThis1: commander.CommandStrict = program.combineFlagAndOptionalValue(); +const combineFlagAndOptionalValueThis2: commander.CommandStrict = program.combineFlagAndOptionalValue(false); // allowUnknownOption -const allowUnknownOptionThis1: commander.Command = program.allowUnknownOption(); -const allowUnknownOptionThis2: commander.Command = program.allowUnknownOption(false); +const allowUnknownOptionThis1: commander.CommandStrict = program.allowUnknownOption(); +const allowUnknownOptionThis2: commander.CommandStrict = program.allowUnknownOption(false); // parse -const parseThis1: commander.Command = program.parse(); -const parseThis2: commander.Command = program.parse(process.argv); -const parseThis3: commander.Command = program.parse(['node', 'script.js'], { from: 'node' }); -const parseThis4: commander.Command = program.parse(['node', 'script.js'], { from: 'electron' }); -const parseThis5: commander.Command = program.parse(['--option'], { from: 'user' }); +const parseThis1: commander.CommandStrict = program.parse(); +const parseThis2: commander.CommandStrict = program.parse(process.argv); +const parseThis3: commander.CommandStrict = program.parse(['node', 'script.js'], { from: 'node' }); +const parseThis4: commander.CommandStrict = program.parse(['node', 'script.js'], { from: 'electron' }); +const parseThis5: commander.CommandStrict = program.parse(['--option'], { from: 'user' }); // parseAsync, same tests as parse -const parseAsyncThis1: Promise = program.parseAsync(); -const parseAsyncThis2: Promise = program.parseAsync(process.argv); -const parseAsyncThis3: Promise = program.parseAsync(['node', 'script.js'], { from: 'node' }); -const parseAsyncThis4: Promise = program.parseAsync(['node', 'script.js'], { from: 'electron' }); -const parseAsyncThis5: Promise = program.parseAsync(['--option'], { from: 'user' }); +const parseAsyncThis1: Promise = program.parseAsync(); +const parseAsyncThis2: Promise = program.parseAsync(process.argv); +const parseAsyncThis3: Promise = program.parseAsync(['node', 'script.js'], { from: 'node' }); +const parseAsyncThis4: Promise = program.parseAsync(['node', 'script.js'], { from: 'electron' }); +const parseAsyncThis5: Promise = program.parseAsync(['--option'], { from: 'user' }); // parseOptions (and ParseOptionsResult) const { operands, unknown } = program.parseOptions(['node', 'script.js', 'hello']); @@ -183,23 +185,23 @@ const optsVal1 = opts.foo; const opstVale2 = opts['bar']; // description -const descriptionThis: commander.Command = program.description('my description'); +const descriptionThis: commander.CommandStrict = program.description('my description'); const descriptionValue: string = program.description(); // alias -const aliasThis: commander.Command = program.alias('my alias'); +const aliasThis: commander.CommandStrict = program.alias('my alias'); const aliasValue: string = program.alias(); // aliases -const aliasesThis: commander.Command = program.aliases(['first-alias', 'second-alias']); +const aliasesThis: commander.CommandStrict = program.aliases(['first-alias', 'second-alias']); const aliasesValue: string[] = program.aliases(); // usage -const usageThis: commander.Command = program.usage('my usage'); +const usageThis: commander.CommandStrict = program.usage('my usage'); const usageValue: string = program.usage(); // name -const nameThis: commander.Command = program.name('my-name'); +const nameThis: commander.CommandStrict = program.name('my-name'); const nameValue: string = program.name(); // outputHelp @@ -208,25 +210,25 @@ program.outputHelp((str: string) => { return str; }); program.outputHelp({ error: true }); // help -program.help(); -program.help((str: string) => { return str; }); -program.help({ error: true }); +function showHelp1(): never { program.help(); }; +function showHelp2(): never { program.help((str: string) => { return str; }); }; +function showHelp3(): never { program.help({ error: true }); }; // helpInformation const helpInformnationValue: string = program.helpInformation(); const helpInformnationValue2: string = program.helpInformation({ error: true }); // helpOption -const helpOptionThis1: commander.Command = program.helpOption('-h,--help'); -const helpOptionThis2: commander.Command = program.helpOption('-h,--help', 'custom description'); -const helpOptionThis3: commander.Command = program.helpOption(undefined, 'custom description'); -const helpOptionThis4: commander.Command = program.helpOption(false); +const helpOptionThis1: commander.CommandStrict = program.helpOption('-h,--help'); +const helpOptionThis2: commander.CommandStrict = program.helpOption('-h,--help', 'custom description'); +const helpOptionThis3: commander.CommandStrict = program.helpOption(undefined, 'custom description'); +const helpOptionThis4: commander.CommandStrict = program.helpOption(false); // addHelpText -const addHelpTextThis1: commander.Command = program.addHelpText('after', 'text'); -const addHelpTextThis2: commander.Command = program.addHelpText('afterAll', 'text'); -const addHelpTextThis3: commander.Command = program.addHelpText('before', () => 'before'); -const addHelpTextThis4: commander.Command = program.addHelpText('beforeAll', (context: commander.AddHelpTextContext) => { +const addHelpTextThis1: commander.CommandStrict = program.addHelpText('after', 'text'); +const addHelpTextThis2: commander.CommandStrict = program.addHelpText('afterAll', 'text'); +const addHelpTextThis3: commander.CommandStrict = program.addHelpText('before', () => 'before'); +const addHelpTextThis4: commander.CommandStrict = program.addHelpText('beforeAll', (context: commander.AddHelpTextContext) => { if (context.error) { return; // Can return nothing to skip display } @@ -234,15 +236,16 @@ const addHelpTextThis4: commander.Command = program.addHelpText('beforeAll', (co }); // on -const onThis: commander.Command = program.on('command:foo', () => { +const onThis: commander.CommandStrict = program.on('command:foo', () => { // do nothing. }); // createCommand -const createInstance2: commander.Command = program.createCommand('name'); +const createInstanceWithOptions: commander.Command = programWithOptions.createCommand('name'); +const createInstance: commander.CommandStrict = program.createCommand('name'); -class MyCommand extends commander.Command { +class MyCommand extends commander.CommandStrict { createCommand(name?: string): MyCommand { return new MyCommand(name); } @@ -259,14 +262,14 @@ mySub.myFunction(); // configureHelp const createHelpInstance: commander.Help = program.createHelp(); -const configureHelpThis: commander.Command = program.configureHelp({ +const configureHelpThis: commander.CommandStrict = program.configureHelp({ sortSubcommands: true, // override property - visibleCommands: (cmd: commander.Command) => [] // override method + visibleCommands: (cmd: commander.CommandInterface) => [] // override method }); const helpConfiguration: commander.HelpConfiguration = program.configureHelp(); // configureOutput -const configureOutputThis: commander.Command = program.configureOutput({ }); +const configureOutputThis: commander.CommandStrict = program.configureOutput({ }); const configureOutputConfig: commander.OutputConfiguration = program.configureOutput(); program.configureOutput({ @@ -279,7 +282,7 @@ program.configureOutput({ // Help const helper = new commander.Help(); -const helperCommand = new commander.Command(); +const helperCommand = new commander.CommandStrict(); const helperOption = new commander.Option('-a, --all'); helper.helpWidth = 3; @@ -293,7 +296,7 @@ const subcommandDescriptionStr: string = helper.subcommandDescription(helperComm const optionTermStr: string = helper.optionTerm(helperOption); const optionDescriptionStr: string = helper.optionDescription(helperOption); -const visibleCommands: commander.Command[] = helper.visibleCommands(helperCommand); +const visibleCommands: commander.CommandInterface[] = helper.visibleCommands(helperCommand); const visibleOptions: commander.Option[] = helper.visibleOptions(helperCommand); const visibleArguments: Array<{ term: string; description: string}> = helper.visibleArguments(helperCommand); diff --git a/typings/index.d.ts b/typings/index.d.ts index 1baa20922..330a9ea6f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -86,34 +86,34 @@ declare namespace commander { sortOptions: boolean; /** Get the command term to show in the list of subcommands. */ - subcommandTerm(cmd: Command): string; + subcommandTerm(cmd: CommandInterface): string; /** Get the command description to show in the list of subcommands. */ - subcommandDescription(cmd: Command): string; + subcommandDescription(cmd: CommandInterface): string; /** Get the option term to show in the list of options. */ optionTerm(option: Option): string; /** Get the option description to show in the list of options. */ optionDescription(option: Option): string; /** Get the command usage to be displayed at the top of the built-in help. */ - commandUsage(cmd: Command): string; + commandUsage(cmd: CommandInterface): string; /** Get the description for the command. */ - commandDescription(cmd: Command): string; + commandDescription(cmd: CommandInterface): string; /** Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one. */ - visibleCommands(cmd: Command): Command[]; + visibleCommands(cmd: CommandInterface): CommandInterface[]; /** Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one. */ - visibleOptions(cmd: Command): Option[]; + visibleOptions(cmd: CommandInterface): Option[]; /** Get an array of the arguments which have descriptions. */ - visibleArguments(cmd: Command): Array<{ term: string; description: string}>; + visibleArguments(cmd: CommandInterface): Array<{ term: string; description: string}>; /** Get the longest command term length. */ - longestSubcommandTermLength(cmd: Command, helper: Help): number; + longestSubcommandTermLength(cmd: CommandInterface, helper: Help): number; /** Get the longest option term length. */ - longestOptionTermLength(cmd: Command, helper: Help): number; + longestOptionTermLength(cmd: CommandInterface, helper: Help): number; /** Get the longest argument term length. */ - longestArgumentTermLength(cmd: Command, helper: Help): number; + longestArgumentTermLength(cmd: CommandInterface, helper: Help): number; /** Calculate the pad width from the maximum term length. */ - padWidth(cmd: Command, helper: Help): number; + padWidth(cmd: CommandInterface, helper: Help): number; /** * Wrap the given string to width characters per line, with lines after the first indented. @@ -122,7 +122,7 @@ declare namespace commander { wrap(str: string, width: number, indent: number): string; /** Generate the built-in help text. */ - formatHelp(cmd: Command, helper: Help): string; + formatHelp(cmd: CommandInterface, helper: Help): string; } type HelpConstructor = new () => Help; type HelpConfiguration = Partial; @@ -135,7 +135,7 @@ declare namespace commander { } interface AddHelpTextContext { // passed to text function used with .addHelpText() error: boolean; - command: Command; + command: CommandInterface; } interface OutputConfiguration { writeOut?(str: string): void; @@ -148,12 +148,25 @@ declare namespace commander { type AddHelpTextPosition = 'beforeAll' | 'before' | 'after' | 'afterAll'; - interface Command { + // Command which may have options stored as properties + interface Command extends CommandInterface { [key: string]: any; // options as properties + createCommand(name?: string): Command; + } + type CommandConstructor = new (name?: string) => Command; + // Command without options stored as properties + // Extending from CommandInterface rather than Command to allow stronger typing. + interface CommandStrict extends CommandInterface { + createCommand(name?: string): CommandStrict; + } + type CommandStrictConstructor = new (name?: string) => CommandStrict; + + // Additional layer compared with JavaScript to allow stronger typing. + interface CommandInterface { args: string[]; - commands: Command[]; + commands: CommandInterface[]; /** * Set the program version to `str`. @@ -212,7 +225,7 @@ declare namespace commander { * See .command() for creating an attached subcommand, which uses this routine to * create the command. You can override createCommand to customise subcommands. */ - createCommand(name?: string): Command; + createCommand(name?: string): CommandInterface; /** * Add a prepared subcommand. @@ -221,7 +234,7 @@ declare namespace commander { * * @returns `this` command for chaining */ - addCommand(cmd: Command, opts?: CommandOptions): this; + addCommand(cmd: CommandInterface, opts?: CommandOptions): this; /** * Define argument syntax for command. @@ -559,7 +572,6 @@ declare namespace commander { */ on(event: string | symbol, listener: (...args: any[]) => void): this; } - type CommandConstructor = new (name?: string) => Command; interface CommandOptions { hidden?: boolean; @@ -579,6 +591,7 @@ declare namespace commander { interface CommanderStatic extends Command { program: Command; Command: CommandConstructor; + CommandStrict: CommandStrictConstructor; Option: OptionConstructor; CommanderError: CommanderErrorConstructor; InvalidOptionArgumentError: InvalidOptionArgumentErrorConstructor; From d38a2b4d9240fc8304a192bcbf2cdd8e5b261332 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 30 Nov 2020 19:05:50 +1300 Subject: [PATCH 2/5] Remove redundant import --- typings/commander-tests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index f53a7ea00..7671b3361 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -1,5 +1,4 @@ import * as commander from './index'; -import { command } from './index'; // Test Commander usage with TypeScript. // This is NOT a usable program, just used to test for compile errors! From 492a661a9fe49b5f712228d55d4d27df1584a3e9 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 2 Dec 2020 20:28:00 +1300 Subject: [PATCH 3/5] Add strict test --- tests/command.strict.test.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/command.strict.test.js diff --git a/tests/command.strict.test.js b/tests/command.strict.test.js new file mode 100644 index 000000000..4a84b5c1f --- /dev/null +++ b/tests/command.strict.test.js @@ -0,0 +1,26 @@ +const commander = require('../'); + +/* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "expectStrictSettings"] }] */ + +function expectStrictSettings(command) { + // Use internal knowledge of the expected settings + expect(command._storeOptionsAsProperties).toEqual(false); + expect(command._passCommandToAction).toEqual(false); +} + +test('when new CommandStrict then settings are strict', () => { + const program = new commander.CommandStrict(); + expectStrictSettings(program); +}); + +test('when new CommandStrict creates command directly then settings are strict', () => { + const program = new commander.CommandStrict(); + const subcommand = program.createCommand(); + expectStrictSettings(subcommand); +}); + +test('when new CommandStrict creates command indirectly then settings are strict', () => { + const program = new commander.CommandStrict(); + const subcommand = program.command('sub'); + expectStrictSettings(subcommand); +}); From 2952581f676d5af266689c7c0fd5433c137f6725 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 2 Dec 2020 21:12:21 +1300 Subject: [PATCH 4/5] Remove example which has different action code pattern --- examples/storeOptionsAsProperties-opts.js | 39 ----------------------- 1 file changed, 39 deletions(-) delete mode 100755 examples/storeOptionsAsProperties-opts.js diff --git a/examples/storeOptionsAsProperties-opts.js b/examples/storeOptionsAsProperties-opts.js deleted file mode 100755 index 56185f36e..000000000 --- a/examples/storeOptionsAsProperties-opts.js +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env node - -// To avoid possible name clashes, you can change the default behaviour -// of storing the options values as properties on the command object. -// You access the option values using the .opts() function. -// -// Example output: -// -// $ node storeOptionsAsProperties-opts.js show -// undefined -// undefined -// -// $ node storeOptionsAsProperties-opts.js --name foo show --action jump -// jump -// foo - -// const commander = require('commander'); // (normal include) -const commander = require('../'); // include commander in git clone of commander repo -const program = new commander.Command(); - -program - .storeOptionsAsProperties(false); // <--- change behaviour - -program - .name('my-program-name') - .option('-n,--name '); - -program - .command('show') - .option('-a,--action ') - .action((cmd) => { - const options = cmd.opts(); // <--- use opts to access option values - console.log(options.action); - }); - -program.parse(process.argv); - -const programOptions = program.opts(); // <--- use opts to access option values -console.log(programOptions.name); From b951364dfd633523d04f059060b869e194c7f9ff Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 2 Dec 2020 21:18:46 +1300 Subject: [PATCH 5/5] Add CommandStrict to README --- Readme.md | 32 +++++++++++++++++---- examples/storeOptionsAsProperties-action.js | 1 + 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Readme.md b/Readme.md index 6f5b15afc..207fefec0 100644 --- a/Readme.md +++ b/Readme.md @@ -35,7 +35,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [Custom event listeners](#custom-event-listeners) - [Bits and pieces](#bits-and-pieces) - [.parse() and .parseAsync()](#parse-and-parseasync) - - [Avoiding option name clashes](#avoiding-option-name-clashes) + - [Strict](#strict) - [TypeScript](#typescript) - [createCommand()](#createcommand) - [Import into ECMAScript Module](#import-into-ecmascript-module) @@ -73,6 +73,14 @@ const program = new Command(); program.version('0.0.1'); ``` +Or even better for new programs, create a CommandStrict object which favors safe configuration over backwards compatibility. + +```js +const { CommandStrict } = require('commander'); +const program = new CommandStrict(); +program.version('0.0.1'); +``` + ## Options Options are defined with the `.option()` method, also serving as documentation for the options. Each option can have a short flag (single character) and a long name, separated by a comma or space or vertical bar ('|'). @@ -688,26 +696,38 @@ program.parse(); // Implicit, and auto-detect electron program.parse(['-f', 'filename'], { from: 'user' }); ``` -### Avoiding option name clashes +### Strict -The original and default behaviour is that the option values are stored -as properties on the program, and the action handler is passed a +The default configuration of the Command class changes slowly to preserve backwards compatibility. +The CommandStrict class uses latest recommended settings and is suggested +for new programs. All of the examples in this README and the examples work with both `Commnand` and +`CommandStrict`. + +**Avoiding option name clashes** + +The original and default `Command` behaviour is that the option values are stored +as properties on the command, and the action handler is passed a command object with the options values stored as properties. This is very convenient to code, but the downside is possible clashes with -existing properties of Command. +existing properties of `Command`. -There are two new routines to change the behaviour, and the default behaviour may change in the future: +There are two new routines to change the behaviour: - `storeOptionsAsProperties`: whether to store option values as properties on command object, or store separately (specify false) and access using `.opts()` - `passCommandToAction`: whether to pass command to action handler, or just the options (specify false) +The suggested manual use is set them both the same. `Command` sets both to true, while `CommandStrict` sets both to false. + Example file: [storeOptionsAsProperties-action.js](./examples/storeOptionsAsProperties-action.js) ```js +const program = new Command(); + program .storeOptionsAsProperties(false) .passCommandToAction(false); +// Now same settings as CommandStrict. program .name('my-program-name') diff --git a/examples/storeOptionsAsProperties-action.js b/examples/storeOptionsAsProperties-action.js index 8e9670ae2..5267c0eed 100755 --- a/examples/storeOptionsAsProperties-action.js +++ b/examples/storeOptionsAsProperties-action.js @@ -23,6 +23,7 @@ const program = new commander.Command(); program .storeOptionsAsProperties(false) // <--- change behaviour .passCommandToAction(false); // <--- change behaviour +// Now same settings as CommandStrict. program .name('my-program-name')