Skip to content

Commit d097ee3

Browse files
committed
Add settings for strict parsing and error-handling
These are "opt-ins" to enable restrictive behaviour that getOpts doesn't enforce by default. These behaviours are common to many CLI programs: * noMixedOrder: Terminate option-parsing at the first non-option. * noUndefined: Throw an error for unrecognised option names. * terminator: String demarcating options list (usually double-dash) which isn't included with the rest of the ARGV array. These options are separate, but most developers will want to enable them altogether. A "shorthand" to enable all three might be added in future.
1 parent 3c0f3b1 commit d097ee3

5 files changed

Lines changed: 648 additions & 4 deletions

File tree

index.d.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Import the function like
2-
// import getOpts = require('get-options');
2+
// import getOpts = require("get-options");
33
// If you need the types you can import them like normal
4-
// import { Options } from 'get-options';
4+
// import { Options } from "get-options";
55

66
/**
77
* Extract command-line options from a list of strings.
@@ -20,7 +20,10 @@ declare namespace getOpts {
2020
noAliasPropagation?: boolean | "first-only";
2121
noCamelCase?: boolean;
2222
noBundling?: boolean;
23+
noMixedOrder?: boolean;
24+
noUndefined?: boolean;
2325
ignoreEquals?: boolean;
26+
terminator?: string | RegExp;
2427
duplicates?:
2528
| "use-first"
2629
| "use-last"

index.js

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,28 @@ function formatName(input, noCamelCase){
158158
}
159159

160160

161+
/**
162+
* Test a string against a list of patterns.
163+
*
164+
* @param {String} input
165+
* @param {String[]|RegExp[]} patterns
166+
* @return {Boolean}
167+
* @internal
168+
*/
169+
function match(input, patterns = []){
170+
if(!patterns || 0 === patterns.length)
171+
return false;
172+
173+
input = String(input);
174+
patterns = arrayify(patterns).filter(Boolean);
175+
for(const pattern of patterns)
176+
if((pattern === input && "string" === typeof pattern)
177+
|| (pattern instanceof RegExp) && pattern.test(input))
178+
return true;
179+
return false;
180+
}
181+
182+
161183
/**
162184
* Filter duplicate strings from an array.
163185
*
@@ -363,6 +385,9 @@ function getOpts(input, optdef = null, config = {}){
363385
noAliasPropagation,
364386
noCamelCase,
365387
noBundling,
388+
noMixedOrder,
389+
noUndefined,
390+
terminator,
366391
ignoreEquals,
367392
duplicates = "use-last",
368393
} = config;
@@ -645,14 +670,43 @@ function getOpts(input, optdef = null, config = {}){
645670
}
646671

647672
else{
673+
const isTerminator = match(arg, terminator);
674+
const keepRest = () => result.argv.push(...input.slice(i + 1));
675+
648676
// A previous option is still collecting arguments
649-
if(currentOption && currentOption.canCollect)
677+
if(currentOption && currentOption.canCollect && !isTerminator)
650678
currentOption.values.push(arg);
651679

652-
// Not associated with an option; push this value onto the argv array
680+
// Not associated with an option
653681
else{
654682
currentOption && wrapItUp();
683+
684+
// Terminate option parsing?
685+
if(isTerminator){
686+
keepRest();
687+
break;
688+
}
689+
690+
// Raise an exception if unrecognised switches are considered an error
691+
if(noUndefined && /^-./.test(arg)){
692+
let error = noUndefined;
693+
694+
// Prepare an error object to be thrown in the user's direction
695+
switch(typeof noUndefined){
696+
case "function": error = error(arg); break;
697+
case "boolean": error = `Unknown option: "%s"`; // Fall-through
698+
case "string": error = new TypeError(error.replace("%s", arg));
699+
}
700+
throw error;
701+
}
702+
655703
result.argv.push(arg);
704+
705+
// Finish processing if mixed-order is disabled
706+
if(noMixedOrder){
707+
keepRest();
708+
break;
709+
}
656710
}
657711
}
658712
}

test/order-mixing.js

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
"use strict";
2+
3+
const getOpts = require("../index.js");
4+
5+
6+
suite("Mixed-order options", () => {
7+
const optdef = {
8+
"-e, --eval": "<string>",
9+
"--log-file": "<path>",
10+
"--size": "[width] [height]",
11+
"--files": "[list...]",
12+
"--verbose": "",
13+
};
14+
15+
16+
test("Mixed: Basic", () => {
17+
assert.deepEqual(getOpts([
18+
"--log-file", "/var/log/stuff.txt",
19+
"generate",
20+
"all-files",
21+
"--verbose",
22+
], optdef), {
23+
argv: ["generate", "all-files"],
24+
options: {
25+
logFile: "/var/log/stuff.txt",
26+
verbose: true,
27+
},
28+
});
29+
30+
assert.deepEqual(getOpts([
31+
"generate",
32+
"all-files",
33+
"--log-file", "/var/log/stuff.txt",
34+
"--size", "100", "200",
35+
"--verbose",
36+
], optdef), {
37+
argv: ["generate", "all-files"],
38+
options: {
39+
logFile: "/var/log/stuff.txt",
40+
size: ["100", "200"],
41+
verbose: true,
42+
},
43+
});
44+
});
45+
46+
47+
test("Mixed: Variadic", () => {
48+
const args = [
49+
"foo",
50+
"bar",
51+
"--files", "1.gif", "2.gif", "3.gif",
52+
"--size", "600", "400",
53+
"baz",
54+
];
55+
const expected = {
56+
argv: ["foo", "bar", "baz"],
57+
options: {
58+
files: ["1.gif", "2.gif", "3.gif"],
59+
size: ["600", "400"],
60+
},
61+
};
62+
assert.deepEqual(getOpts(args, optdef), expected);
63+
args.unshift("--verbose");
64+
expected.options.verbose = true;
65+
assert.deepEqual(getOpts(args, optdef), expected);
66+
});
67+
68+
69+
test("Mixed: Undefined", () => {
70+
assert.deepEqual(getOpts([
71+
"--unknown",
72+
"--verbose",
73+
"foo",
74+
"--also-unknown",
75+
"bar",
76+
"--size", "3", "2",
77+
], optdef), {
78+
argv: ["--unknown", "foo", "--also-unknown", "bar"],
79+
options: {verbose: true, size: ["3", "2"]},
80+
});
81+
});
82+
83+
84+
test("Mixed: With terminator", () => {
85+
assert.deepEqual(getOpts([
86+
"foo",
87+
"--verbose",
88+
"bar",
89+
"--",
90+
"--size", "600", "400",
91+
], optdef, {terminator: "--"}), {
92+
argv: ["foo", "bar", "--size", "600", "400"],
93+
options: {verbose: true},
94+
});
95+
96+
assert.deepEqual(getOpts([
97+
"foo",
98+
"--",
99+
"--verbose",
100+
"bar",
101+
"--",
102+
"--size", "600", "400",
103+
], optdef, {terminator: "--"}), {
104+
argv: ["foo", "--verbose", "bar", "--", "--size", "600", "400"],
105+
options: {},
106+
});
107+
});
108+
109+
110+
test("Unmixed: Basic", () => {
111+
assert.deepEqual(getOpts(["--verbose", "foo"], optdef, {noMixedOrder: true}), {
112+
argv: ["foo"],
113+
options: {verbose: true},
114+
});
115+
116+
assert.deepEqual(getOpts(["foo", "--verbose"], optdef, {noMixedOrder: true}), {
117+
argv: ["foo", "--verbose"],
118+
options: {},
119+
});
120+
121+
const args = [
122+
"--verbose",
123+
"--size", "100", "200",
124+
"generate",
125+
"--log-file",
126+
"/var/log/stuff.txt",
127+
"all-files",
128+
];
129+
assert.deepEqual(getOpts(args, optdef, {noMixedOrder: true}), {
130+
argv: ["generate", "--log-file", "/var/log/stuff.txt", "all-files"],
131+
options: {
132+
size: ["100", "200"],
133+
verbose: true,
134+
},
135+
});
136+
args.unshift("foo");
137+
assert.deepEqual(getOpts(args, optdef, {noMixedOrder: true}), {argv: args, options: {}});
138+
139+
assert.deepEqual(getOpts([
140+
"foo",
141+
"--verbose",
142+
"--files",
143+
"1.gif",
144+
"2.gif",
145+
], optdef, {noMixedOrder: true}), {
146+
argv: ["foo", "--verbose", "--files", "1.gif", "2.gif"],
147+
options: {},
148+
});
149+
});
150+
151+
152+
test("Unmixed: Variadic", () => {
153+
assert.deepEqual(getOpts([
154+
"--files", "1.gif", "2.gif", "3.gif",
155+
"--verbose",
156+
"foo",
157+
"--size", "1", "2",
158+
], optdef, {noMixedOrder: true}), {
159+
argv: ["foo", "--size", "1", "2"],
160+
options: {
161+
verbose: true,
162+
files: ["1.gif", "2.gif", "3.gif"],
163+
},
164+
});
165+
166+
const args = [
167+
"bar",
168+
"--files", "1.gif", "2.gif", "3.gif",
169+
"--verbose",
170+
"foo",
171+
"--size", "1", "2",
172+
];
173+
assert.deepEqual(getOpts(args, optdef, {noMixedOrder: true}), {
174+
argv: args,
175+
options: {},
176+
});
177+
});
178+
179+
180+
test("Unmixed: Undefined", () => {
181+
assert.deepEqual(getOpts(["--verbose", "foo", "--unknown", "bar"], optdef, {noMixedOrder: true}), {
182+
argv: ["foo", "--unknown", "bar"],
183+
options: {verbose: true},
184+
});
185+
186+
assert.deepEqual(getOpts(["foo", "--verbose", "--unknown", "bar"], optdef, {noMixedOrder: true}), {
187+
argv: ["foo", "--verbose", "--unknown", "bar"],
188+
options: {},
189+
});
190+
191+
assert.deepEqual(getOpts(["foo", "--unknown", "--verbose", "bar"], optdef, {noMixedOrder: true}), {
192+
argv: ["foo", "--unknown", "--verbose", "bar"],
193+
options: {},
194+
});
195+
});
196+
197+
198+
test("Unmixed: With terminator", () => {
199+
assert.deepEqual(getOpts(["--verbose", "--", "--size", "1", "2"], optdef, {
200+
noMixedOrder: true,
201+
terminator: "--",
202+
}), {
203+
options: {verbose: true},
204+
argv: ["--size", "1", "2"],
205+
});
206+
207+
assert.deepEqual(getOpts(["--", "--verbose", "--", "--size", "1", "2"], optdef, {
208+
noMixedOrder: true,
209+
terminator: "--",
210+
}), {
211+
options: {},
212+
argv: ["--verbose", "--", "--size", "1", "2"],
213+
});
214+
215+
assert.deepEqual(getOpts(["--files", "1.gif", "--", "2.gif", "--verbose"], optdef, {
216+
noMixedOrder: true,
217+
terminator: "--",
218+
}), {
219+
options: {files: ["1.gif"]},
220+
argv: ["2.gif", "--verbose"],
221+
});
222+
});
223+
});

0 commit comments

Comments
 (0)