diff --git a/lib/command.ts b/lib/command.ts index 81a7b50e5..8cf3c2db0 100644 --- a/lib/command.ts +++ b/lib/command.ts @@ -238,6 +238,10 @@ export function command( // up a yargs chain and possibly returns it. const builderOutput = builder(yargs.reset(parsed.aliases)); const innerYargs = isYargsInstance(builderOutput) ? builderOutput : yargs; + // A null command indicates we are running the default command, + // if this is the case, we should show the root usage instructions + // rather than the usage instructions for the nested default command: + if (!command) innerYargs.getUsageInstance().unfreeze(); if (shouldUpdateUsage(innerYargs)) { innerYargs .getUsageInstance() @@ -261,6 +265,10 @@ export function command( // as a short hand, an object can instead be provided, specifying // the options that a command takes. const innerYargs = yargs.reset(parsed.aliases); + // A null command indicates we are running the default command, + // if this is the case, we should show the root usage instructions + // rather than the usage instructions for the nested default command: + if (!command) innerYargs.getUsageInstance().unfreeze(); if (shouldUpdateUsage(innerYargs)) { innerYargs .getUsageInstance() diff --git a/lib/usage.ts b/lib/usage.ts index d436f209f..93f982fa0 100644 --- a/lib/usage.ts +++ b/lib/usage.ts @@ -231,7 +231,12 @@ export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) { // your application's commands, i.e., non-option // arguments populated in '_'. - if (commands.length) { + // + // If there's only a single command, and it's the default command + // (represented by commands[0][2]) don't show command stanza: + // + // TODO(@bcoe): why isnt commands[0][2] an object with a named property? + if (commands.length > 1 || (commands.length === 1 && !commands[0][2])) { ui.div(__('Commands:')); const context = yargs.getContext(); @@ -703,7 +708,10 @@ export function usage(yargs: YargsInstance, y18n: Y18N, shim: PlatformShim) { }; self.unfreeze = function unfreeze() { const frozen = frozens.pop(); - assertNotStrictEqual(frozen, undefined, shim); + // In the case of running a defaultCommand, we reset + // usage early to ensure we receive the top level instructions. + // unfreezing again should just be a noop: + if (!frozen) return; ({ failMessage, failureOutput, diff --git a/lib/yargs-factory.ts b/lib/yargs-factory.ts index 57d129505..f41c9c8b3 100644 --- a/lib/yargs-factory.ts +++ b/lib/yargs-factory.ts @@ -934,12 +934,11 @@ function Yargs( [args, shortCircuit, _parseFn], arguments.length ); - freeze(); + freeze(); // Push current state of parser onto stack. if (typeof args === 'undefined') { const argv = self._parseArgs(processArgs); const tmpParsed = self.parsed; - unfreeze(); - // TODO: remove this compatibility hack when we release yargs@15.x: + unfreeze(); // Pop the stack. self.parsed = tmpParsed; return argv; } @@ -967,7 +966,7 @@ function Yargs( const parsed = self._parseArgs(args, !!shortCircuit); completion!.setParsed(self.parsed as DetailedArguments); if (parseFn) parseFn(exitError, parsed, output); - unfreeze(); + unfreeze(); // Pop the stack. return parsed; }; @@ -1472,7 +1471,9 @@ function Yargs( }; Object.defineProperty(self, 'argv', { - get: () => self._parseArgs(processArgs), + get: () => { + return self.parse(); + }, enumerable: true, }); @@ -1483,7 +1484,7 @@ function Yargs( commandIndex = 0, helpOnly = false ) { - let skipValidation = !!calledFromCommand; + let skipValidation = !!calledFromCommand || helpOnly; args = args || processArgs; options.__ = y18n.__; @@ -1550,10 +1551,8 @@ function Yargs( const handlerKeys = command.getCommands(); const requestCompletions = completion!.completionKey in argv; - const skipRecommendation = argv[helpOpt!] || requestCompletions; - const skipDefaultCommand = - skipRecommendation && - (handlerKeys.length > 1 || handlerKeys[0] !== '$0'); + const skipRecommendation = + argv[helpOpt!] || requestCompletions || helpOnly; if (argv._.length) { if (handlerKeys.length) { @@ -1584,7 +1583,7 @@ function Yargs( } // run the default command, if defined - if (command.hasDefaultCommand() && !skipDefaultCommand) { + if (command.hasDefaultCommand() && !skipRecommendation) { const innerArgv = command.runCommand( null, self, @@ -1617,7 +1616,7 @@ function Yargs( self.showCompletionScript(); self.exit(0); } - } else if (command.hasDefaultCommand() && !skipDefaultCommand) { + } else if (command.hasDefaultCommand() && !skipRecommendation) { const innerArgv = command.runCommand(null, self, parsed, 0, helpOnly); return self._postProcess( innerArgv, diff --git a/test/usage.cjs b/test/usage.cjs index 355455bbb..f09b88c41 100644 --- a/test/usage.cjs +++ b/test/usage.cjs @@ -4261,4 +4261,181 @@ describe('usage tests', () => { ' --small Packet size', ]); }); + + // Refs: https://github.com/yargs/yargs/pull/1826 + describe('usage for default command', () => { + describe('default only', () => { + const expected = [ + 'usage', + '', + 'Default command description', + '', + 'Options:', + ' --help Show help [boolean]', + ' --version Show version number [boolean]', + ]; + + it('should contain the expected output for --help', () => { + const r = checkUsage(() => + yargs('--help') + .scriptName('usage') + .command('*', 'Default command description') + .parse() + ); + + r.logs[0].split('\n').should.deep.equal(expected); + }); + + it('should contain the expected output for showhelp', () => { + const r = checkUsage(() => { + const y = yargs() + .scriptName('usage') + .command('*', 'Default command description'); + y.showHelp('log'); + }); + r.logs[0].split('\n').should.deep.equal(expected); + }); + + it('should contain the expected output for getHelp', async () => { + const y = yargs() + .scriptName('usage') + .command('*', 'Default command description'); + const help = await y.getHelp(); + help.split('\n').should.deep.equal(expected); + }); + + it('should contain the expected output for getHelp when called from within handler', async () => { + let help = ''; + const y = yargs() + .scriptName('usage') + .command('*', 'Default command description', {}, async () => { + help = await y.getHelp(); + }); + await y.parse(); + help.split('\n').should.deep.equal(expected); + }); + + it('should contain the expected output for showHelp when called from within handler', () => { + const r = checkUsage(() => + yargs() + .scriptName('usage') + .command('*', 'Default command description', {}, () => + yargs.showHelp('log') + ) + .parse('') + ); + r.logs[0].split('\n').should.deep.equal(expected); + }); + + it('should contain the expected output for showHelp, when exception occurs', () => { + const r = checkUsage(() => + yargs() + .scriptName('usage') + .command('*', 'Default command description', {}, () => + yargs.showHelp('log') + ) + .check(() => { + return false; + }) + .parse('') + ); + r.errors[0].split('\n').should.deep.equal(expected); + }); + }); + + describe('multiple', () => { + const expected = [ + 'Hello, world!', + '', + 'Commands:', + ' usage Default command description [default]', + ' usage foo Foo command description', + '', + 'Options:', + ' --help Show help [boolean]', + ' --version Show version number [boolean]', + ]; + it('should contain the expected output for --help', () => { + const r = checkUsage(() => + yargs('--help') + .scriptName('usage') + .usage('Hello, world!') + .commands([ + {command: '*', desc: 'Default command description'}, + {command: 'foo', desc: 'Foo command description'}, + ]) + .parse() + ); + r.logs[0].split('\n').should.deep.equal(expected); + }); + + it('should contain the expected output for showHelp', () => { + const r = checkUsage(() => { + yargs() + .scriptName('usage') + .usage('Hello, world!') + .commands([ + {command: '*', desc: 'Default command description'}, + {command: 'foo', desc: 'Foo command description'}, + ]) + .parse(); + yargs.showHelp('log'); + }); + r.logs[0].split('\n').should.deep.equal(expected); + }); + + it('should contain the expected output for showHelp when called from within handler', () => { + const r = checkUsage( + () => + yargs() + .scriptName('usage') + .usage('Hello, world!') + .commands([ + { + command: '*', + desc: 'Default command description', + handler: _ => yargs.showHelp('log'), + }, + {command: 'foo', desc: 'Foo command description'}, + ]).argv + ); + r.logs[0].split('\n').should.deep.equal(expected); + }); + + it('should contain the expected output for getHelp', async () => { + const y = yargs() + .scriptName('usage') + .usage('Hello, world!') + .commands([ + { + command: '*', + desc: 'Default command description', + handler: () => {}, + }, + {command: 'foo', desc: 'Foo command description'}, + ]); + const help = await y.getHelp(); + help.split('\n').should.deep.equal(expected); + }); + + it('should contain the expected output for getHelp when called from within handler', async () => { + let help = ''; + const y = yargs() + .scriptName('usage') + .usage('Hello, world!') + .commands([ + { + command: '*', + desc: 'Default command description', + handler: async () => { + help = await y.getHelp(); + }, + }, + {command: 'foo', desc: 'Foo command description'}, + ]); + await y.argv; + help.split('\n').should.deep.equal(expected); + }); + }); + }); });