Skip to content

Commit a454158

Browse files
committed
add nameless options + more tests [skip publish]
1 parent 0ae9a73 commit a454158

6 files changed

Lines changed: 186 additions & 68 deletions

File tree

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ Yes, there are a lot of arg parsers. But hear us out.
1313
- Options with flexible parsing
1414
- Required options
1515
- Options with multiple values
16-
- Nameless options (TBD)
16+
- Nameless options
1717
- Automatically generated help text:
1818
- Customizable colors
1919
- Customizable header and footer text
2020
- Customizable usage examples
2121
- Automatic text alignment
2222
- Add run examples for your args (TBD)
23-
- Shows default value and type next to description (customizable) (TBD)
23+
- Shows default value and type next to description
2424
- TypeScript-first package: You will always have strong types
2525

2626
## Usage
@@ -109,6 +109,10 @@ $ ./mybin --my-string "Some string"
109109
Commands are activated when their keyword is included in the args. The first command that matches
110110
will be executed, skipping the rest. Options will still be parsed.
111111

112+
Any arguments that are not taken by options or commands, are automatically passed to
113+
`options.extra`, which you can access when running a command or when using the return value from
114+
`parseArgs()`.
115+
112116
#### Options
113117

114118
| Name | Type | Required | Example | Description |
@@ -149,6 +153,10 @@ $ ./mybin my-command --my-string "Some string"
149153
Options are variables you can accept via CLI and parse to use in your commands, e.g. `--my-bool`,
150154
`--my-string string`, `--my-number 1`
151155

156+
Any arguments that are not taken by options or commands, are automatically passed to
157+
`options.extra`, which you can access when running a command or when using the return value from
158+
`parseArgs()`.
159+
152160
#### Options
153161

154162
| Name | Type | Required | Default | Example | Description |

src/index.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ export class Massarg<Options extends OptionsBase = OptionsBase> {
1414
private _commands: CommandDef<Options>[] = []
1515
private _runCommand?: CommandDef<Options>
1616
private _maxNameLen = 0
17-
/** These are the parsed options passed via args. They will only be available after using `parse()` or `printHelp()`. */
18-
public data: Options = { help: false } as Options
17+
/**
18+
* These are the parsed options passed via args. They will only be available after using `parse()` or `printHelp()`,
19+
* or when retured by `parseArgs()`. */
20+
public data: Options = { help: false, extras: [] as string[] } as Options
1921

2022
private _help: Required<HelpDef> = {
2123
binName: undefined as any,
@@ -201,6 +203,10 @@ export class Massarg<Options extends OptionsBase = OptionsBase> {
201203
}
202204
this._runCommand = command
203205
}
206+
207+
if (!option && !command) {
208+
this.data.extras.push(arg)
209+
}
204210
}
205211
return this.data
206212
}
@@ -229,16 +235,16 @@ export class Massarg<Options extends OptionsBase = OptionsBase> {
229235
} else if (this._main) {
230236
this._ensureRequired()
231237
this._main(this.data)
238+
} else {
239+
this._ensureRequired()
232240
}
233241
} catch (e) {
234242
if (e.cmdName && e.fieldName) {
235243
console.log()
236244
console.error("Error")
237245
console.error(chalk.red`${e.message}`)
238-
process.exit(1)
239-
} else {
240-
throw e
241246
}
247+
throw e
242248
}
243249
return
244250
}

src/options.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import chalk from "chalk"
44
export type OptionsBase = {
55
/** When `true`, will output help text and exit the process without error */
66
help: boolean
7+
8+
/** This contains arguments that were not taken by any command or option. */
9+
extras: string[]
710
}
811

912
export type MainDef<Options> = (options: Options) => void

tests/extras.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import massarg from "../src"
2+
3+
describe("Extras", () => {
4+
test("should take extra values unparsed", () => {
5+
const options = massarg()
6+
.option({
7+
name: "number",
8+
parse: (v) => parseInt(v),
9+
})
10+
.parseArgs(["--number", "10", "app name"])
11+
expect(options).toHaveProperty("number", 10)
12+
expect(options).toHaveProperty("extras", ["app name"])
13+
})
14+
})

tests/help.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ describe("Print Help", () => {
2323
"\n"
2424
)
2525
})
26+
27+
test("should not throw when passing args", () => {
28+
expect(() =>
29+
massarg()
30+
.help({ binName: "test", useColors: false })
31+
.option({
32+
name: "number",
33+
description: "Number value",
34+
parse: (v) => parseInt(v),
35+
})
36+
.getHelpString(["--help"])
37+
.join("\n")
38+
).not.toThrow()
39+
})
40+
2641
test("should print help correctly with only global options", () => {
2742
const helpStr = massarg()
2843
.help({ binName: "test", useColors: false })

tests/options.test.ts

Lines changed: 133 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,78 +2,150 @@ import massarg from "../src"
22
import { OptionDef } from "../src/options"
33

44
describe("Options", () => {
5-
test("should parse properly", () => {
6-
const options = massarg()
7-
.option({
8-
name: "number",
9-
parse: (v) => parseInt(v),
10-
})
11-
.parseArgs(["--number", "10"])
12-
expect(options).toHaveProperty("number", 10)
13-
})
5+
describe("basics", () => {
6+
test("should parse properly", () => {
7+
const options = massarg()
8+
.option({
9+
name: "number",
10+
parse: (v) => parseInt(v),
11+
})
12+
.parseArgs(["--number", "10"])
13+
expect(options).toHaveProperty("number", 10)
14+
})
1415

15-
test("should read from alias", () => {
16-
const options = massarg()
17-
.option({
18-
name: "number",
19-
aliases: ["n"],
20-
parse: (v) => parseInt(v),
21-
})
22-
.parseArgs(["-n", "10"])
23-
expect(options).toHaveProperty("number", 10)
24-
expect(options).toHaveProperty("n", 10)
16+
test("should read from alias", () => {
17+
const options = massarg()
18+
.option({
19+
name: "number",
20+
aliases: ["n"],
21+
parse: (v) => parseInt(v),
22+
})
23+
.parseArgs(["-n", "10"])
24+
expect(options).toHaveProperty("number", 10)
25+
expect(options).toHaveProperty("n", 10)
26+
})
2527
})
2628

27-
test("should camelCase names", () => {
28-
const options = massarg()
29-
.option({
30-
name: "is-number",
31-
aliases: ["n"],
32-
parse: (v) => parseInt(v),
33-
})
34-
.parseArgs(["--is-number", "10"])
29+
describe("parsing", () => {
30+
test("should camelCase names", () => {
31+
const options = massarg()
32+
.option({
33+
name: "is-number",
34+
aliases: ["n"],
35+
parse: (v) => parseInt(v),
36+
})
37+
.parseArgs(["--is-number", "10"])
3538

36-
expect(options).toHaveProperty("isNumber", 10)
37-
expect(options).toHaveProperty("is-number", 10)
38-
expect(options).toHaveProperty("n", 10)
39-
})
39+
expect(options).toHaveProperty("isNumber", 10)
40+
expect(options).toHaveProperty("is-number", 10)
41+
expect(options).toHaveProperty("n", 10)
42+
})
4043

41-
test("should parse bool in correct forms", () => {
42-
const opts = {
43-
name: "bool",
44-
boolean: true,
45-
} as OptionDef<any, any>
46-
const noArg = massarg().option(opts).parseArgs(["--bool"])
47-
expect(noArg).toHaveProperty("bool", true)
44+
describe("bool", () => {
45+
test("should parse bool in correct forms", () => {
46+
const opts = {
47+
name: "bool",
48+
boolean: true,
49+
} as OptionDef<any, any>
4850

49-
const truthyNumArg = massarg().option(opts).parseArgs(["--bool", "1"])
50-
expect(truthyNumArg).toHaveProperty("bool", true)
51+
const noArg = massarg().option(opts).parseArgs(["--bool"])
52+
expect(noArg).toHaveProperty("bool", true)
5153

52-
const falsyNumArg = massarg().option(opts).parseArgs(["--bool", "0"])
53-
expect(falsyNumArg).toHaveProperty("bool", false)
54-
})
54+
const truthyNumArg = massarg().option(opts).parseArgs(["--bool", "1"])
55+
expect(truthyNumArg).toHaveProperty("bool", true)
5556

56-
test("should expect value when not bool", () => {
57-
const opts = {
58-
name: "number",
59-
parse: (v) => parseInt(v),
60-
} as OptionDef<any, any>
61-
expect(() => massarg().option(opts).parseArgs(["--number"])).toThrow("Missing value for: number")
62-
})
57+
const falsyNumArg = massarg().option(opts).parseArgs(["--bool", "0"])
58+
expect(falsyNumArg).toHaveProperty("bool", false)
59+
})
60+
})
6361

64-
test("should parse array in correct forms", () => {
65-
const opts = {
66-
name: "array",
67-
array: true,
68-
} as OptionDef<any, any>
62+
describe("array", () => {
63+
test("should parse array in correct forms", () => {
64+
const opts = {
65+
name: "array",
66+
array: true,
67+
} as OptionDef<any, any>
6968

70-
const arr0el = massarg().option(opts).parseArgs([])
71-
expect(arr0el).toHaveProperty("array", [])
69+
const arr0el = massarg().option(opts).parseArgs([])
70+
expect(arr0el).toHaveProperty("array", [])
7271

73-
const arr1el = massarg().option(opts).parseArgs(["--array", "something"])
74-
expect(arr1el).toHaveProperty("array", ["something"])
72+
const arr1el = massarg().option(opts).parseArgs(["--array", "something"])
73+
expect(arr1el).toHaveProperty("array", ["something"])
7574

76-
const arr2el = massarg().option(opts).parseArgs(["--array", "something", "--array", "another"])
77-
expect(arr2el).toHaveProperty("array", ["something", "another"])
75+
const arr2el = massarg().option(opts).parseArgs(["--array", "something", "--array", "another"])
76+
expect(arr2el).toHaveProperty("array", ["something", "another"])
77+
})
78+
})
79+
80+
test("should expect value when not bool", () => {
81+
expect(() =>
82+
massarg()
83+
.option({
84+
name: "number",
85+
parse: (v) => parseInt(v),
86+
})
87+
.parseArgs(["--number"])
88+
).toThrow("Missing value for: number")
89+
})
90+
91+
describe("required", () => {
92+
test("should throw on missing required value", () => {
93+
const mockConsoleError = jest.spyOn(console, "error").mockImplementation(() => void 0)
94+
const mockConsoleLog = jest.spyOn(console, "log").mockImplementation(() => void 0)
95+
expect(() =>
96+
massarg()
97+
.option({
98+
name: "number",
99+
parse: (v) => parseInt(v),
100+
required: true,
101+
})
102+
.parse(["--not-number", "abcdefg"])
103+
).toThrow("number is required, but was not defined")
104+
mockConsoleError.mockRestore()
105+
mockConsoleLog.mockRestore()
106+
})
107+
108+
test("should not throw on existing required value for command", () => {
109+
const mockConsoleError = jest.spyOn(console, "error").mockImplementation(() => void 0)
110+
const mockConsoleLog = jest.spyOn(console, "log").mockImplementation(() => void 0)
111+
expect(() =>
112+
massarg()
113+
.option({
114+
name: "number",
115+
parse: (v) => parseInt(v),
116+
commands: "cmd",
117+
required: true,
118+
})
119+
.command({
120+
name: "cmd",
121+
run: () => void 0,
122+
})
123+
.parse(["cmd", "--number", "10"])
124+
).not.toThrow("number is required, but was not defined")
125+
mockConsoleError.mockRestore()
126+
mockConsoleLog.mockRestore()
127+
})
128+
129+
test("should throw on missing required value for command", () => {
130+
const mockConsoleError = jest.spyOn(console, "error").mockImplementation(() => void 0)
131+
const mockConsoleLog = jest.spyOn(console, "log").mockImplementation(() => void 0)
132+
expect(() =>
133+
massarg()
134+
.option({
135+
name: "number",
136+
parse: (v) => parseInt(v),
137+
commands: "cmd",
138+
required: true,
139+
})
140+
.command({
141+
name: "cmd",
142+
run: () => void 0,
143+
})
144+
.parse(["cmd"])
145+
).toThrow("number is required, but was not defined")
146+
mockConsoleError.mockRestore()
147+
mockConsoleLog.mockRestore()
148+
})
149+
})
78150
})
79151
})

0 commit comments

Comments
 (0)