diff --git a/docs/advanced.md b/docs/advanced.md
index 84d3e3e9b..da5c1d9ca 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -233,6 +233,8 @@ This example uses [jest](https://github.com/facebook/jest) as a test runner, but
.commandDir(directory, [opts])
------------------------------
+_Note: `commandDir()` does not work with ESM or Deno, see [hierarchy using index.mjs](/docs/advanced.md#esm-hierarchy) for an example of building a complex nested CLI using ESM._
+
Apply command modules from a directory relative to the module calling this method.
This allows you to organize multiple commands into their own modules under a
@@ -360,6 +362,38 @@ exports.handler = function (argv) {
}
```
+
+### Example command hierarchy using index.mjs
+
+To support creating a complex nested CLI when using ESM, the method
+`.command()` was extended to accept an array of command modules.
+Rather than using `.commandDir()`, create an `index.mjs` in each command
+directory with a list of the commands:
+
+cmds/index.mjs:
+
+```js
+import * as a from './init.mjs';
+import * as b from './remote.mjs';
+export const commands = [a, b];
+```
+
+This index will then be imported and registered with your CLI:
+
+cli.js:
+
+```js
+#!/usr/bin/env node
+
+import yargs from 'yargs';
+import { hideBin } from 'yargs/helpers';
+import { commands } from './cmds/index.mjs';
+
+yargs(hideBin(process.argv))
+ .command(commands)
+ .argv;
+```
+
## Building Configurable CLI Apps
diff --git a/lib/command.ts b/lib/command.ts
index 33da0b220..781812268 100644
--- a/lib/command.ts
+++ b/lib/command.ts
@@ -26,6 +26,7 @@ import {
import whichModule from './utils/which-module.js';
const DEFAULT_MARKER = /(^\*)|(^\$0)/;
+export type DefinitionOrCommandName = string | CommandHandlerDefinition;
// handles parsing positional arguments,
// and populating argv with said positional
@@ -43,9 +44,9 @@ export function command(
let defaultCommand: CommandHandler | undefined;
self.addHandler = function addHandler(
- cmd: string | string[] | CommandHandlerDefinition,
+ cmd: DefinitionOrCommandName | [DefinitionOrCommandName, ...string[]],
description?: string | false,
- builder?: CommandBuilder | CommandBuilderDefinition,
+ builder?: CommandBuilder,
handler?: CommandHandlerCallback,
commandMiddleware?: Middleware[],
deprecated?: boolean
@@ -54,9 +55,16 @@ export function command(
const middlewares = commandMiddlewareFactory(commandMiddleware);
handler = handler || (() => {});
+ // If an array is provided that is all CommandHandlerDefinitions, add
+ // each handler individually:
if (Array.isArray(cmd)) {
- aliases = cmd.slice(1);
- cmd = cmd[0];
+ if (isCommandAndAliases(cmd)) {
+ [cmd, ...aliases] = cmd;
+ } else {
+ for (const command of cmd) {
+ self.addHandler(command);
+ }
+ }
} else if (isCommandHandlerDefinition(cmd)) {
let command =
Array.isArray(cmd.command) || typeof cmd.command === 'string'
@@ -73,10 +81,8 @@ export function command(
cmd.deprecated
);
return;
- }
-
- // allow a module to be provided instead of separate builder and handler
- if (isCommandBuilderDefinition(builder)) {
+ } else if (isCommandBuilderDefinition(builder)) {
+ // Allow a module to be provided as builder, rather than function:
self.addHandler(
[cmd].concat(aliases),
description,
@@ -88,53 +94,57 @@ export function command(
return;
}
- // parse positionals out of cmd string
- const parsedCommand = parseCommand(cmd);
+ // The 'cmd' provided was a string, we apply the command DSL:
+ // https://github.com/yargs/yargs/blob/master/docs/advanced.md#advanced-topics
+ if (typeof cmd === 'string') {
+ // parse positionals out of cmd string
+ const parsedCommand = parseCommand(cmd);
+
+ // remove positional args from aliases only
+ aliases = aliases.map(alias => parseCommand(alias).cmd);
+
+ // check for default and filter out '*'
+ let isDefault = false;
+ const parsedAliases = [parsedCommand.cmd].concat(aliases).filter(c => {
+ if (DEFAULT_MARKER.test(c)) {
+ isDefault = true;
+ return false;
+ }
+ return true;
+ });
- // remove positional args from aliases only
- aliases = aliases.map(alias => parseCommand(alias).cmd);
+ // standardize on $0 for default command.
+ if (parsedAliases.length === 0 && isDefault) parsedAliases.push('$0');
- // check for default and filter out '*''
- let isDefault = false;
- const parsedAliases = [parsedCommand.cmd].concat(aliases).filter(c => {
- if (DEFAULT_MARKER.test(c)) {
- isDefault = true;
- return false;
+ // shift cmd and aliases after filtering out '*'
+ if (isDefault) {
+ parsedCommand.cmd = parsedAliases[0];
+ aliases = parsedAliases.slice(1);
+ cmd = cmd.replace(DEFAULT_MARKER, parsedCommand.cmd);
}
- return true;
- });
- // standardize on $0 for default command.
- if (parsedAliases.length === 0 && isDefault) parsedAliases.push('$0');
-
- // shift cmd and aliases after filtering out '*'
- if (isDefault) {
- parsedCommand.cmd = parsedAliases[0];
- aliases = parsedAliases.slice(1);
- cmd = cmd.replace(DEFAULT_MARKER, parsedCommand.cmd);
- }
+ // populate aliasMap
+ aliases.forEach(alias => {
+ aliasMap[alias] = parsedCommand.cmd;
+ });
- // populate aliasMap
- aliases.forEach(alias => {
- aliasMap[alias] = parsedCommand.cmd;
- });
+ if (description !== false) {
+ usage.command(cmd, description, isDefault, aliases, deprecated);
+ }
- if (description !== false) {
- usage.command(cmd, description, isDefault, aliases, deprecated);
+ handlers[parsedCommand.cmd] = {
+ original: cmd,
+ description,
+ handler,
+ builder: builder || {},
+ middlewares,
+ deprecated,
+ demanded: parsedCommand.demanded,
+ optional: parsedCommand.optional,
+ };
+
+ if (isDefault) defaultCommand = handlers[parsedCommand.cmd];
}
-
- handlers[parsedCommand.cmd] = {
- original: cmd,
- description,
- handler,
- builder: builder || {},
- middlewares,
- deprecated,
- demanded: parsedCommand.demanded,
- optional: parsedCommand.optional,
- };
-
- if (isDefault) defaultCommand = handlers[parsedCommand.cmd];
};
self.addDirectory = function addDirectory(
@@ -368,7 +378,7 @@ export function command(
const builder = defaultCommand.builder;
if (isCommandBuilderCallback(builder)) {
builder(yargs);
- } else {
+ } else if (!isCommandBuilderDefinition(builder)) {
Object.keys(builder).forEach(key => {
yargs.option(key, builder[key]);
});
@@ -555,7 +565,7 @@ export interface CommandInstance {
opts?: RequireDirectoryOptions
): void;
addHandler(
- cmd: string | string[] | CommandHandlerDefinition,
+ cmd: string | CommandHandlerDefinition | DefinitionOrCommandName[],
description?: CommandHandler['description'],
builder?: CommandBuilderDefinition | CommandBuilder,
handler?: CommandHandlerCallback,
@@ -592,12 +602,6 @@ export interface CommandHandlerDefinition
describe?: CommandHandler['description'];
}
-export function isCommandHandlerDefinition(
- cmd: string | string[] | CommandHandlerDefinition
-): cmd is CommandHandlerDefinition {
- return typeof cmd === 'object';
-}
-
export interface CommandBuilderDefinition {
builder?: CommandBuilder;
deprecated?: boolean;
@@ -639,6 +643,16 @@ interface CommandBuilderCallback {
(y: YargsInstance): YargsInstance | void;
}
+function isCommandAndAliases(
+ cmd: DefinitionOrCommandName[]
+): cmd is [CommandHandlerDefinition, ...string[]] {
+ if (cmd.every(c => typeof c === 'string')) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
export function isCommandBuilderCallback(
builder: CommandBuilder
): builder is CommandBuilderCallback {
@@ -651,6 +665,12 @@ function isCommandBuilderOptionDefinitions(
return typeof builder === 'object';
}
+export function isCommandHandlerDefinition(
+ cmd: DefinitionOrCommandName | [DefinitionOrCommandName, ...string[]]
+): cmd is CommandHandlerDefinition {
+ return typeof cmd === 'object' && !Array.isArray(cmd);
+}
+
interface Positionals extends Pick {
demand: Dictionary;
}
diff --git a/lib/yargs-factory.ts b/lib/yargs-factory.ts
index 76f7f4e38..5622dd628 100644
--- a/lib/yargs-factory.ts
+++ b/lib/yargs-factory.ts
@@ -5,14 +5,15 @@
// Works by accepting a shim which shims methods that contain platform
// specific logic.
import {
+ command as Command,
CommandInstance,
CommandHandler,
CommandBuilderDefinition,
CommandBuilder,
CommandHandlerCallback,
- FinishCommandHandler,
- command as Command,
CommandHandlerDefinition,
+ FinishCommandHandler,
+ DefinitionOrCommandName,
} from './command.js';
import type {
Dictionary,
@@ -655,7 +656,7 @@ function Yargs(
};
self.command = function (
- cmd: string | string[] | CommandHandlerDefinition,
+ cmd: string | CommandHandlerDefinition | DefinitionOrCommandName[],
description?: CommandHandler['description'],
builder?: CommandBuilderDefinition | CommandBuilder,
handler?: CommandHandlerCallback,
diff --git a/test/command.cjs b/test/command.cjs
index efae6b039..ea89f3455 100644
--- a/test/command.cjs
+++ b/test/command.cjs
@@ -1832,4 +1832,32 @@ describe('Command', () => {
const argv = yargs.command('$0 ', '', yargs => {}).parse('+5550100');
argv.phone.should.equal('+5550100');
});
+
+ it('allows an array of commands to be provided', () => {
+ const innerCommands = [
+ {
+ command: 'c ',
+ describe: 'add x to y',
+ builder: () => {},
+ handler: argv => {
+ argv.output.value = argv.x + argv.y;
+ },
+ },
+ ];
+ const cmds = [
+ {
+ command: 'a',
+ describe: 'numeric comamand',
+ builder: yargs => {
+ yargs.command(innerCommands);
+ },
+ handler: () => {},
+ },
+ ];
+ const context = {
+ output: {},
+ };
+ yargs().command(cmds).parse('a c 10 5', context);
+ context.output.value.should.equal(15);
+ });
});
diff --git a/test/deno/yargs.test.ts b/test/deno/yargs.test.ts
index 25797a07b..895b6f011 100644
--- a/test/deno/yargs.test.ts
+++ b/test/deno/yargs.test.ts
@@ -6,6 +6,7 @@ import {
} from 'https://deno.land/std/testing/asserts.ts'
import yargs from '../../deno.ts'
import { Arguments } from '../../deno-types.ts'
+import { commands } from '../esm/fixtures/commands/index.mjs'
Deno.test('demandCommand(1) throw error if no command provided', () => {
let err: Error|null = null
@@ -38,3 +39,11 @@ Deno.test('does not drop .0 if positional is configured as string', async () =>
}).parse() as Arguments
assertEquals(argv.str, '33.0')
})
+
+Deno.test('hierarchy of commands', async () => {
+ const context = {
+ output: {value: 0},
+ };
+ yargs().command(commands).parse('a c 10 5', context);
+ assertEquals(context.output.value, 15);
+})
diff --git a/test/esm/fixtures/commands/a.mjs b/test/esm/fixtures/commands/a.mjs
new file mode 100644
index 000000000..90fc41265
--- /dev/null
+++ b/test/esm/fixtures/commands/a.mjs
@@ -0,0 +1,8 @@
+import {commands} from './subcommands/index.mjs';
+
+export const command = 'a';
+export const describe = 'numeric commands';
+export const builder = yargs => {
+ yargs.command(commands);
+};
+export const handler = function (argv) {};
diff --git a/test/esm/fixtures/commands/b.mjs b/test/esm/fixtures/commands/b.mjs
new file mode 100644
index 000000000..c5fe2230d
--- /dev/null
+++ b/test/esm/fixtures/commands/b.mjs
@@ -0,0 +1,8 @@
+export const command = 'b ';
+export const describe = 'string commands';
+export const builder = yargs => {
+ yargs.string(['str1', 'str2']);
+};
+export const handler = function (argv) {
+ argv.output.text = `${argv.str1} ${argv.str2}`;
+};
diff --git a/test/esm/fixtures/commands/index.mjs b/test/esm/fixtures/commands/index.mjs
new file mode 100644
index 000000000..f5cdfcacb
--- /dev/null
+++ b/test/esm/fixtures/commands/index.mjs
@@ -0,0 +1,3 @@
+import * as a from './a.mjs';
+import * as b from './b.mjs';
+export const commands = [a, b];
diff --git a/test/esm/fixtures/commands/subcommands/c.mjs b/test/esm/fixtures/commands/subcommands/c.mjs
new file mode 100644
index 000000000..8085aeb95
--- /dev/null
+++ b/test/esm/fixtures/commands/subcommands/c.mjs
@@ -0,0 +1,6 @@
+export const command = 'c ';
+export const describe = 'add x to y';
+export const builder = yargs => {};
+export const handler = function (argv) {
+ argv.output.value = argv.x + argv.y;
+};
diff --git a/test/esm/fixtures/commands/subcommands/d.mjs b/test/esm/fixtures/commands/subcommands/d.mjs
new file mode 100644
index 000000000..62c937dfe
--- /dev/null
+++ b/test/esm/fixtures/commands/subcommands/d.mjs
@@ -0,0 +1,6 @@
+export const command = 'd ';
+export const describe = 'multiply x by y';
+export const builder = yargs => {};
+export const handler = function (argv) {
+ argv.output.value = argv.x * argv.y;
+};
diff --git a/test/esm/fixtures/commands/subcommands/index.mjs b/test/esm/fixtures/commands/subcommands/index.mjs
new file mode 100644
index 000000000..bf656652b
--- /dev/null
+++ b/test/esm/fixtures/commands/subcommands/index.mjs
@@ -0,0 +1,3 @@
+import * as c from './c.mjs';
+import * as d from './d.mjs';
+export const commands = [c, d];
diff --git a/test/esm/yargs-test.mjs b/test/esm/yargs-test.mjs
index 2d0331892..33baa9fd2 100644
--- a/test/esm/yargs-test.mjs
+++ b/test/esm/yargs-test.mjs
@@ -3,6 +3,9 @@
import * as assert from 'assert';
import yargs from '../../index.mjs';
+// Example of composing hierarchical commands when using ESM:
+import {commands} from './fixtures/commands/index.mjs';
+
describe('ESM', () => {
describe('parser', () => {
it('parses process.argv by default', () => {
@@ -35,4 +38,20 @@ describe('ESM', () => {
assert.strictEqual(argv.str, '33.0');
});
});
+ describe('hierarchy of commands', () => {
+ it('allows array of commands to be registered', () => {
+ const context = {
+ output: {},
+ };
+ yargs().command(commands).parse('b hello world!', context);
+ assert.strictEqual(context.output.text, 'hello world!');
+ });
+ it('allows array of subcommands to be registered', () => {
+ const context = {
+ output: {},
+ };
+ yargs().command(commands).parse('a d 10 5', context);
+ assert.strictEqual(context.output.value, 50);
+ });
+ });
});