Skip to content

Commit 3643338

Browse files
authored
feat!: positionals now opt-in when strict:true (#116)
1 parent 9d539c3 commit 3643338

File tree

6 files changed

+88
-19
lines changed

6 files changed

+88
-19
lines changed

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ added: REPLACEME
3030
are encountered, or when arguments are passed that do not match the
3131
`type` configured in `options`.
3232
**Default:** `true`.
33+
* `allowPositionals`: {boolean} Whether this command accepts positional arguments.
34+
**Default:** `false` if `strict` is `true`, otherwise `true`.
3335

3436
* Returns: {Object} An {Object} representing the parsed command line
3537
arguments:
@@ -38,7 +40,7 @@ added: REPLACEME
3840
* `positionals` {string\[]}, containing positional arguments.
3941

4042
Provides a higher level API for command-line argument parsing than interacting
41-
with `process.argv` directly.
43+
with `process.argv` directly. Takes a specification for the expected arguments and returns a structured object with the parsed options and positionals.
4244

4345
```mjs
4446
import { parseArgs } from 'util';
@@ -133,7 +135,7 @@ This package was implemented using [tape](https://www.npmjs.com/package/tape) as
133135
## 💡 `process.mainArgs` Proposal
134136

135137
> Note: This can be moved forward independently of the `util.parseArgs()` proposal/work.
136-
138+
137139
### Implementation:
138140

139141
```javascript
@@ -153,6 +155,7 @@ process.mainArgs = process.argv.slice(process._exec ? 1 : 2)
153155
* `multiple` {boolean} (Optional) If true, when appearing one or more times in `args`, results are collected in an `Array`
154156
* `short` {string} (Optional) A single character alias for an option; When appearing one or more times in `args`; Respects the `multiple` configuration
155157
* `strict` {Boolean} (Optional) A `Boolean` for whether or not to throw an error when unknown options are encountered, `type:'string'` options are missing an options-argument, or `type:'boolean'` options are passed an options-argument; defaults to `true`
158+
* `allowPositionals` {Boolean} (Optional) Whether this command accepts positional arguments. Defaults `false` if `strict` is `true`, otherwise defaults to `true`.
156159
* Returns: {Object} An object having properties:
157160
* `values` {Object}, key:value for each option found. Value is a string for string options, or `true` for boolean options, or an array (of strings or booleans) for options configured as `multiple:true`.
158161
* `positionals` {string[]}, containing [Positionals][]
@@ -203,7 +206,7 @@ const options = {
203206
},
204207
};
205208
const args = ['-f', 'b'];
206-
const { values, positionals } = parseArgs({ args, options });
209+
const { values, positionals } = parseArgs({ args, options, allowPositionals: true });
207210
// values = { foo: true }
208211
// positionals = ['b']
209212
```
@@ -213,7 +216,7 @@ const { parseArgs } = require('@pkgjs/parseargs');
213216
// unconfigured
214217
const options = {};
215218
const args = ['-f', '--foo=a', '--bar', 'b'];
216-
const { values, positionals } = parseArgs({ strict: false, args, options });
219+
const { values, positionals } = parseArgs({ strict: false, args, options, allowPositionals: true });
217220
// values = { f: true, foo: 'a', bar: true }
218221
// positionals = ['b']
219222
```
@@ -273,7 +276,7 @@ const { values, positionals } = parseArgs({ strict: false, args, options });
273276
- no, `-bar` is a short option or options, with expansion logic that follows the
274277
[Utility Syntax Guidelines in POSIX.1-2017](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). `-bar` expands to `-b`, `-a`, `-r`.
275278
- Is `---foo` the same as `--foo`?
276-
- no
279+
- no
277280
- the first is a long option named `'-foo'`
278281
- the second is a long option named `'foo'`
279282
- Is `-` a positional? ie, `bash some-test.sh | tap -`

errors.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,26 @@ class ERR_PARSE_ARGS_INVALID_OPTION_VALUE extends Error {
2222
}
2323

2424
class ERR_PARSE_ARGS_UNKNOWN_OPTION extends Error {
25-
constructor(option) {
26-
super(`Unknown option '${option}'`);
25+
constructor(option, allowPositionals) {
26+
const suggestDashDash = allowPositionals ? `. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- ${JSON.stringify(option)}` : '';
27+
super(`Unknown option '${option}'${suggestDashDash}`);
2728
this.code = 'ERR_PARSE_ARGS_UNKNOWN_OPTION';
2829
}
2930
}
3031

32+
class ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL extends Error {
33+
constructor(positional) {
34+
super(`Unexpected argument '${positional}'. This command does not take positional arguments`);
35+
this.code = 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL';
36+
}
37+
}
38+
3139
module.exports = {
3240
codes: {
3341
ERR_INVALID_ARG_TYPE,
3442
ERR_INVALID_ARG_VALUE,
3543
ERR_PARSE_ARGS_INVALID_OPTION_VALUE,
3644
ERR_PARSE_ARGS_UNKNOWN_OPTION,
45+
ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL,
3746
}
3847
};

index.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const {
3939
ERR_INVALID_ARG_VALUE,
4040
ERR_PARSE_ARGS_INVALID_OPTION_VALUE,
4141
ERR_PARSE_ARGS_UNKNOWN_OPTION,
42+
ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL,
4243
},
4344
} = require('./errors');
4445

@@ -86,12 +87,12 @@ function getMainArgs() {
8687
* @param {boolean} strict - show errors, from parseArgs({ strict })
8788
*/
8889
function checkOptionUsage(longOption, optionValue, options,
89-
shortOrLong, strict) {
90+
shortOrLong, strict, allowPositionals) {
9091
// Strict and options are used from local context.
9192
if (!strict) return;
9293

9394
if (!ObjectHasOwn(options, longOption)) {
94-
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(shortOrLong);
95+
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(shortOrLong, allowPositionals);
9596
}
9697

9798
const short = optionsGetOwn(options, longOption, 'short');
@@ -141,11 +142,13 @@ function storeOption(longOption, optionValue, options, values) {
141142
const parseArgs = (config = { __proto__: null }) => {
142143
const args = objectGetOwn(config, 'args') ?? getMainArgs();
143144
const strict = objectGetOwn(config, 'strict') ?? true;
145+
const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict;
144146
const options = objectGetOwn(config, 'options') ?? { __proto__: null };
145147

146148
// Validate input configuration.
147149
validateArray(args, 'args');
148150
validateBoolean(strict, 'strict');
151+
validateBoolean(allowPositionals, 'allowPositionals');
149152
validateObject(options, 'options');
150153
ArrayPrototypeForEach(
151154
ObjectEntries(options),
@@ -186,6 +189,10 @@ const parseArgs = (config = { __proto__: null }) => {
186189
// Check if `arg` is an options terminator.
187190
// Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html
188191
if (arg === '--') {
192+
if (!allowPositionals && remainingArgs.length > 0) {
193+
throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(nextArg);
194+
}
195+
189196
// Everything after a bare '--' is considered a positional argument.
190197
result.positionals = ArrayPrototypeConcat(
191198
result.positionals,
@@ -204,7 +211,8 @@ const parseArgs = (config = { __proto__: null }) => {
204211
// e.g. '-f', 'bar'
205212
optionValue = ArrayPrototypeShift(remainingArgs);
206213
}
207-
checkOptionUsage(longOption, optionValue, options, arg, strict);
214+
checkOptionUsage(longOption, optionValue, options,
215+
arg, strict, allowPositionals);
208216
storeOption(longOption, optionValue, options, result.values);
209217
continue;
210218
}
@@ -235,7 +243,7 @@ const parseArgs = (config = { __proto__: null }) => {
235243
const shortOption = StringPrototypeCharAt(arg, 1);
236244
const longOption = findLongOptionForShort(shortOption, options);
237245
const optionValue = StringPrototypeSlice(arg, 2);
238-
checkOptionUsage(longOption, optionValue, options, `-${shortOption}`, strict);
246+
checkOptionUsage(longOption, optionValue, options, `-${shortOption}`, strict, allowPositionals);
239247
storeOption(longOption, optionValue, options, result.values);
240248
continue;
241249
}
@@ -249,7 +257,8 @@ const parseArgs = (config = { __proto__: null }) => {
249257
// e.g. '--foo', 'bar'
250258
optionValue = ArrayPrototypeShift(remainingArgs);
251259
}
252-
checkOptionUsage(longOption, optionValue, options, arg, strict);
260+
checkOptionUsage(longOption, optionValue, options,
261+
arg, strict, allowPositionals);
253262
storeOption(longOption, optionValue, options, result.values);
254263
continue;
255264
}
@@ -259,12 +268,16 @@ const parseArgs = (config = { __proto__: null }) => {
259268
const index = StringPrototypeIndexOf(arg, '=');
260269
const longOption = StringPrototypeSlice(arg, 2, index);
261270
const optionValue = StringPrototypeSlice(arg, index + 1);
262-
checkOptionUsage(longOption, optionValue, options, `--${longOption}`, strict);
271+
checkOptionUsage(longOption, optionValue, options, `--${longOption}`, strict, allowPositionals);
263272
storeOption(longOption, optionValue, options, result.values);
264273
continue;
265274
}
266275

267276
// Anything left is a positional
277+
if (!allowPositionals) {
278+
throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(arg);
279+
}
280+
268281
ArrayPrototypePush(result.positionals, arg);
269282
}
270283

test/dash.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ test("dash: when args include '-' used as positional then result has '-' in posi
1515
const args = ['-'];
1616
const expected = { values: { __proto__: null }, positionals: ['-'] };
1717

18-
const result = parseArgs({ args });
18+
const result = parseArgs({ allowPositionals: true, args });
1919

2020
t.deepEqual(result, expected);
2121
t.end();

test/index.js

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ test('handles short-option groups with "short" alias configured', () => {
9898
test('Everything after a bare `--` is considered a positional argument', () => {
9999
const args = ['--', 'barepositionals', 'mopositionals'];
100100
const expected = { values: { __proto__: null }, positionals: ['barepositionals', 'mopositionals'] };
101-
const result = parseArgs({ args });
101+
const result = parseArgs({ allowPositionals: true, args });
102102
assert.deepStrictEqual(result, expected, Error('testing bare positionals'));
103103
});
104104

@@ -127,7 +127,7 @@ test('args equals are passed `type: "string"`', () => {
127127
test('when args include single dash then result stores dash as positional', () => {
128128
const args = ['-'];
129129
const expected = { values: { __proto__: null }, positionals: ['-'] };
130-
const result = parseArgs({ args });
130+
const result = parseArgs({ allowPositionals: true, args });
131131
assert.deepStrictEqual(result, expected);
132132
});
133133

@@ -196,12 +196,12 @@ test('order of option and positional does not matter (per README)', () => {
196196
const options = { foo: { type: 'string' } };
197197
const expected = { values: { __proto__: null, foo: 'bar' }, positionals: ['baz'] };
198198
assert.deepStrictEqual(
199-
parseArgs({ args: args1, options }),
199+
parseArgs({ allowPositionals: true, args: args1, options }),
200200
expected,
201201
Error('option then positional')
202202
);
203203
assert.deepStrictEqual(
204-
parseArgs({ args: args2, options }),
204+
parseArgs({ allowPositionals: true, args: args2, options }),
205205
expected,
206206
Error('positional then option')
207207
);
@@ -288,6 +288,25 @@ test('excess leading dashes on options are retained', () => {
288288
assert.deepStrictEqual(result, expected, Error('excess option dashes are retained'));
289289
});
290290

291+
test('positional arguments are allowed by default in strict:false', () => {
292+
const args = ['foo'];
293+
const options = { };
294+
const expected = {
295+
values: { __proto__: null },
296+
positionals: ['foo']
297+
};
298+
const result = parseArgs({ strict: false, args, options });
299+
assert.deepStrictEqual(result, expected);
300+
});
301+
302+
test('positional arguments may be explicitly disallowed in strict:false', () => {
303+
const args = ['foo'];
304+
const options = { };
305+
assert.throws(() => { parseArgs({ strict: false, allowPositionals: false, args, options }); }, {
306+
code: 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL'
307+
});
308+
});
309+
291310
// Test bad inputs
292311

293312
test('invalid argument passed for options', () => {
@@ -355,6 +374,31 @@ test('unknown option with explicit value', () => {
355374
});
356375
});
357376

377+
test('unexpected positional', () => {
378+
const args = ['foo'];
379+
const options = { foo: { type: 'boolean' } };
380+
assert.throws(() => { parseArgs({ args, options }); }, {
381+
code: 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL'
382+
});
383+
});
384+
385+
test('unexpected positional after --', () => {
386+
const args = ['--', 'foo'];
387+
const options = { foo: { type: 'boolean' } };
388+
assert.throws(() => { parseArgs({ args, options }); }, {
389+
code: 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL'
390+
});
391+
});
392+
393+
test('-- by itself is not a positional', () => {
394+
const args = ['--foo', '--'];
395+
const options = { foo: { type: 'boolean' } };
396+
const result = parseArgs({ args, options });
397+
const expected = { values: { __proto__: null, foo: true },
398+
positionals: [] };
399+
assert.deepStrictEqual(result, expected);
400+
});
401+
358402
test('string option used as boolean', () => {
359403
const args = ['--foo'];
360404
const options = { foo: { type: 'string' } };

test/short-option-groups.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ test('when pass full-config group of booleans then parsed as booleans', (t) => {
2020
const options = { r: { type: 'boolean' }, f: { type: 'boolean' } };
2121
const expected = { values: { __proto__: null, r: true, f: true }, positionals: ['p'] };
2222

23-
const result = parseArgs({ args, options });
23+
const result = parseArgs({ allowPositionals: true, args, options });
2424

2525
t.deepEqual(result, expected);
2626
t.end();

0 commit comments

Comments
 (0)