Skip to content

Commit f0ee853

Browse files
committed
feat: built-in help command + flag
1 parent 4051864 commit f0ee853

3 files changed

Lines changed: 36 additions & 9 deletions

File tree

src/command.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { z } from "zod"
2-
import { isZodError, ValidationError } from "./error"
2+
import { isZodError, ParseError, ValidationError } from "./error"
33
import { HelpGenerator } from "./help"
4-
import MassargOption, { MassargFlag, OptionConfig, TypedOptionConfig } from "./option"
4+
import MassargOption, {
5+
MassargFlag,
6+
OptionConfig,
7+
TypedOptionConfig,
8+
MassargHelpFlag,
9+
} from "./option"
510
import { setOrPush } from "./utils"
611

712
export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
@@ -13,6 +18,9 @@ export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
1318
.function()
1419
.args(args, z.any())
1520
.returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType<Runner<z.infer<RunArgs>>>,
21+
bindHelpCommand: z.boolean().optional(),
22+
bindHelpOption: z.boolean().optional(),
23+
// argsHint: z.string().optional(),
1624
})
1725

1826
export type CommandConfig<T = unknown> = z.infer<ReturnType<typeof CommandConfig<z.ZodType<T>>>>
@@ -39,6 +47,12 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
3947
this.description = options.description
4048
this.aliases = options.aliases ?? []
4149
this._run = options.run
50+
if (options.bindHelpCommand) {
51+
this.command(new MassargHelpCommand())
52+
}
53+
if (options.bindHelpOption) {
54+
this.option(new MassargHelpFlag())
55+
}
4256
}
4357

4458
command<A extends ArgsObject = Args>(config: CommandConfig<A>): MassargCommand<Args>
@@ -122,7 +136,12 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
122136
if (command) {
123137
return command.parse(_argv, this.args, parent ?? this)
124138
}
125-
// TODO pass all un-handled args to an "args" option
139+
const defaultOption = this.options.find((o) => o.isDefault)
140+
if (defaultOption) {
141+
console.log("Parsing default option")
142+
_argv = this.parseOption(`--${defaultOption.name}`, [arg, ..._argv])
143+
continue
144+
}
126145
}
127146
if (this._run) {
128147
this._run({ ...args, ...this.args } as Args, parent ?? this)
@@ -138,6 +157,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
138157
message: "Unknown option",
139158
})
140159
}
160+
console.log("parseOption", [arg, ...argv])
141161
const res = option._parseDetails([arg, ...argv])
142162
this.args[res.key as keyof Args] = setOrPush<Args[keyof Args]>(
143163
res.value,
@@ -189,22 +209,24 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
189209
}
190210

191211
export class MassargHelpCommand<T extends ArgsObject = ArgsObject> extends MassargCommand<T> {
192-
constructor(config: Partial<Omit<CommandConfig<T>, "run">>) {
212+
constructor(config: Partial<Omit<CommandConfig<T>, "run">> = {}) {
193213
super({
194214
name: "help",
195215
aliases: ["h"],
196-
description: "Print help",
216+
description: "Print help for this command, or a subcommand if specified",
217+
// argsHint: "[command]",
197218
run: (args, parent) => {
198219
if (args.command) {
199220
const command = parent.commands.find((c) => c.name === args.command)
200221
if (command) {
201222
command.printHelp()
202223
return
203224
} else {
204-
throw new ValidationError({
225+
throw new ParseError({
205226
path: ["command"],
206227
code: "unknown_command",
207228
message: "Unknown command",
229+
received: args.command,
208230
})
209231
}
210232
}

src/example.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ const removeCmd = new MassargCommand<{ component: string }>({
6262
const args = massarg<A>({
6363
name: "my-cli",
6464
description: "This is an example CLI",
65+
bindHelpOption: true,
66+
bindHelpCommand: true,
6567
})
6668
.main((opts, parser) => {
6769
console.log("Main command - printing all opts")

src/option.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export default class MassargOption<T = unknown> {
4949
aliases: string[]
5050
parse: (value: string) => T
5151
isArray: boolean
52+
isDefault: boolean
5253

5354
constructor(options: OptionConfig<T>) {
5455
OptionConfig(z.any()).parse(options)
@@ -58,6 +59,7 @@ export default class MassargOption<T = unknown> {
5859
this.aliases = options.aliases
5960
this.parse = options.parse ?? ((x) => x as unknown as T)
6061
this.isArray = options.array ?? false
62+
this.isDefault = options.isDefault ?? false
6163
}
6264

6365
static fromTypedConfig<T = unknown>(config: TypedOptionConfig<T>): MassargOption<T> {
@@ -70,10 +72,8 @@ export default class MassargOption<T = unknown> {
7072

7173
_parseDetails(argv: string[]): ArgvValue<T> {
7274
// TODO: support --option=value
73-
argv.shift()
7475
let input = ""
7576
try {
76-
input = argv.shift()!
7777
if (!this._match(argv[0])) {
7878
throw new ParseError({
7979
path: [this.name],
@@ -82,6 +82,8 @@ export default class MassargOption<T = unknown> {
8282
received: JSON.stringify(argv[0]),
8383
})
8484
}
85+
argv.shift()
86+
input = argv.shift()!
8587
const value = this.parse(input)
8688
return { key: this.name, value, argv }
8789
} catch (e) {
@@ -103,6 +105,7 @@ export default class MassargOption<T = unknown> {
103105
}
104106

105107
_match(arg: string): boolean {
108+
if (!arg) return false
106109
// full prefix
107110
if (arg.startsWith(OPT_FULL_PREFIX)) {
108111
// negate full prefix
@@ -225,7 +228,7 @@ export class MassargFlag extends MassargOption<boolean> {
225228
}
226229

227230
export class MassargHelpFlag extends MassargFlag {
228-
constructor(config: Partial<Omit<OptionConfig<boolean>, "parse">>) {
231+
constructor(config: Partial<Omit<OptionConfig<boolean>, "parse">> = {}) {
229232
super({
230233
name: "help",
231234
description: "Show this help message",

0 commit comments

Comments
 (0)