Skip to content

Commit 6b1f757

Browse files
committed
Improve subcommand usage, options inheritance and completions command management
- show app name in subcommand usage output - stop subcommands from inheriting `description` and `defaultCommand` from parent - prevent `gen-completions` command from being injected into subcommands or itself
1 parent f2e06e8 commit 6b1f757

5 files changed

Lines changed: 104 additions & 27 deletions

File tree

deno.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
22
"name": "@sigma/parse",
3-
"version": "0.52.0",
3+
"version": "0.53.0",
44
"exports": "./mod.ts",
55
"license": "MIT",
66
"tasks": {
77
"test": "deno doc --lint mod.ts && deno test --doc --parallel && bun test --preload ./bun-preload.ts"
88
},
99
"imports": {
10-
"@std/assert": "jsr:@std/assert@^1.0.18"
10+
"@std/assert": "jsr:@std/assert@^1.0.18",
11+
"@std/fmt": "jsr:@std/fmt@^1.0.9"
1112
},
1213
"nodeModulesDir": "auto",
1314
"exclude": ["./coverage"]

deno.lock

Lines changed: 8 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/help.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,21 @@ function printUsageSection(
8080
const fullCommandPath = commandPath || commandName;
8181
const hasSubCommands = subCommands && subCommands.size > 0;
8282

83+
const commandPrefix = appName
84+
? `${colors.cyan(appName)} ${colors.brightCyan(fullCommandPath)}`
85+
: colors.brightCyan(fullCommandPath);
86+
8387
if (hasSubCommands) {
8488
console.log(
85-
` ${colors.cyan(baseCommand)} ${colors.brightCyan(fullCommandPath)}${
86-
colors.green(usageArgs)
87-
} ${colors.yellow("<command>")} ${colors.dim("[options]")}`,
89+
` ${commandPrefix}${colors.green(usageArgs)} ${
90+
colors.yellow("<command>")
91+
} ${colors.dim("[options]")}`,
8892
);
8993
} else {
9094
console.log(
91-
` ${colors.cyan(baseCommand)} ${colors.brightCyan(fullCommandPath)}${
92-
colors.green(usageArgs)
93-
} ${colors.dim("[options]")}`,
95+
` ${commandPrefix}${colors.green(usageArgs)} ${
96+
colors.dim("[options]")
97+
}`,
9498
);
9599
}
96100
} else if (subCommands && subCommands.size > 0) {

src/index.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,10 @@ function parseInstanceBased(
153153
);
154154

155155
// 2. Inject gen-completions command if at root
156-
if (!commandPath && !subCommands.has("gen-completions")) {
156+
if (
157+
!commandPath && !subCommands.has("gen-completions") &&
158+
instance.constructor.name !== "GenCompletionsCommand"
159+
) {
157160
subCommands.set("gen-completions", {
158161
name: "gen-completions",
159162
commandClass: GenCompletionsCommand,
@@ -376,7 +379,8 @@ function executeSubCommand(
376379

377380
if (
378381
"parse" in commandConstructor &&
379-
typeof commandConstructor.parse === "function"
382+
typeof commandConstructor.parse === "function" &&
383+
commandConstructor.parse !== Args.parse
380384
) {
381385
return commandConstructor.parse(args);
382386
}
@@ -387,20 +391,14 @@ function executeSubCommand(
387391
})[Symbol.metadata];
388392
const subOptions = subMetadata?.__cliOptions as ParseOptions | undefined;
389393

390-
const mergedOptions = subOptions
391-
? { ...parentOptions, ...subOptions }
392-
: (parentOptions
393-
? { ...parentOptions, defaultCommand: undefined }
394-
: undefined);
395-
396-
if (mergedOptions) {
397-
if (sub.description) {
398-
mergedOptions.description = sub.description;
399-
} else if (!subOptions?.description) {
400-
// If neither @subCommand nor @cli provided a description, don't inherit parent's
401-
mergedOptions.description = undefined;
402-
}
403-
}
394+
const mergedOptions: ParseOptions = {
395+
...parentOptions,
396+
...subOptions,
397+
// Subcommands should not inherit defaultCommand from parent
398+
defaultCommand: subOptions?.defaultCommand,
399+
// Subcommands should not inherit description from parent
400+
description: sub.description || subOptions?.description,
401+
};
404402

405403
const parsedValues = parseInstanceBased(
406404
subInstance,

tests/subcommand_more.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {
2+
Args,
3+
cli,
4+
command,
5+
isParseError,
6+
opt,
7+
subCommand,
8+
} from "@sigma/parse";
9+
import { assertNotMatch, assertStringIncludes } from "@std/assert";
10+
import { stripAnsiCode } from "@std/fmt/colors";
11+
12+
@command()
13+
class ListCommand extends Args {
14+
@opt({
15+
short: "r",
16+
description: "Specify an installation root of scripts.",
17+
type: "string",
18+
})
19+
root?: string;
20+
}
21+
22+
@cli({
23+
name: "nub",
24+
description: "A command-line tool.",
25+
color: true,
26+
exitOnHelp: false,
27+
})
28+
class Nub extends Args {
29+
@subCommand(ListCommand, { description: "List installed scripts." })
30+
list?: ListCommand;
31+
}
32+
33+
function getHelpOutput(args: string[]): string {
34+
try {
35+
Nub.parse(args);
36+
return "";
37+
} catch (e) {
38+
if (isParseError(e)) {
39+
return e.message;
40+
}
41+
throw e;
42+
}
43+
}
44+
45+
Deno.test("subcommand help shows colors", () => {
46+
const helpOutput = getHelpOutput(["list", "--help"]);
47+
assertStringIncludes(helpOutput, "\x1b[");
48+
});
49+
50+
Deno.test("subcommand help shows correct description", () => {
51+
const helpOutput = getHelpOutput(["list", "--help"]);
52+
assertStringIncludes(helpOutput, "List installed scripts.");
53+
});
54+
55+
Deno.test("subcommand help does not show gen-completions", () => {
56+
const helpOutput = getHelpOutput(["list", "--help"]);
57+
assertNotMatch(helpOutput, /gen-completions/);
58+
});
59+
60+
Deno.test("root help shows gen-completions", () => {
61+
const helpOutput = getHelpOutput(["--help"]);
62+
assertStringIncludes(helpOutput, "gen-completions");
63+
});
64+
65+
Deno.test("subcommand usage shows app name and subcommand name", () => {
66+
const helpOutput = getHelpOutput(["list", "--help"]);
67+
const plainOutput = stripAnsiCode(helpOutput);
68+
assertStringIncludes(plainOutput, "Usage:\n nub list [options]");
69+
});

0 commit comments

Comments
 (0)