Skip to content

Commit 882067b

Browse files
author
Benjamin E. Coe
authored
feat(parser): support short-option groups (#59)
1 parent 5cdf9e8 commit 882067b

4 files changed

Lines changed: 63 additions & 18 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,9 @@ const { flags, values, positionals } = parseArgs(argv, options);
194194
- If `--` signals the end, is `--` included as a positional? is `program -- foo` the same as `program foo`? Are both `{positionals:['foo']}`, or is the first one `{positionals:['--', 'foo']}`?
195195
- Does the API specify whether a `--` was present/relevant?
196196
- no
197-
- Is `-foo` the same as `--foo`?
198-
- no, `-foo` is a short option or options (WIP: https://github.com/pkgjs/parseargs/issues/2)
197+
- Is `-bar` the same as `--bar`?
198+
- no, `-bar` is a short option or options, with expansion logic that follows the
199+
[Utility Syntax Guidelines in POSIX.1-2017](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). `-bar` expands to `-b`, `-a`, `-r`.
199200
- Is `---foo` the same as `--foo`?
200201
- no
201202
- the first flag would be parsed as `'-foo'`

errors.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,8 @@ class ERR_INVALID_ARG_TYPE extends TypeError {
77
}
88
}
99

10-
class ERR_NOT_IMPLEMENTED extends Error {
11-
constructor(feature) {
12-
super(`${feature} not implemented`);
13-
this.code = 'ERR_NOT_IMPLEMENTED';
14-
}
15-
}
16-
1710
module.exports = {
1811
codes: {
1912
ERR_INVALID_ARG_TYPE,
20-
ERR_NOT_IMPLEMENTED
2113
}
2214
};

index.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const {
44
ArrayPrototypeConcat,
55
ArrayPrototypeIncludes,
66
ArrayPrototypeSlice,
7+
ArrayPrototypeSplice,
78
ArrayPrototypePush,
89
ObjectHasOwn,
910
StringPrototypeCharAt,
@@ -13,12 +14,6 @@ const {
1314
StringPrototypeStartsWith,
1415
} = require('./primordials');
1516

16-
const {
17-
codes: {
18-
ERR_NOT_IMPLEMENTED
19-
}
20-
} = require('./errors');
21-
2217
const {
2318
validateArray,
2419
validateObject
@@ -119,9 +114,14 @@ const parseArgs = (
119114
);
120115
return result;
121116
} else if (StringPrototypeCharAt(arg, 1) !== '-') {
122-
// Look for shortcodes: -fXzy
117+
// Look for shortcodes: -fXzy and expand them to -f -X -z -y:
123118
if (arg.length > 2) {
124-
throw new ERR_NOT_IMPLEMENTED('short option groups');
119+
for (let i = 2; i < arg.length; i++) {
120+
const short = StringPrototypeCharAt(arg, i);
121+
// Add 'i' to 'pos' such that short options are parsed in order
122+
// of definition:
123+
ArrayPrototypeSplice(argv, pos + (i - 1), 0, `-${short}`);
124+
}
125125
}
126126

127127
arg = StringPrototypeCharAt(arg, 1); // short

test/index.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,58 @@ test('when short option withValue used without value then stored as flag', funct
7070
t.end();
7171
});
7272

73+
test('short option group behaves like multiple short options', function(t) {
74+
const passedArgs = ['-rf'];
75+
const passedOptions = { };
76+
const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: [] };
77+
const args = parseArgs(passedArgs, passedOptions);
78+
79+
t.deepEqual(args, expected);
80+
81+
t.end();
82+
});
83+
84+
test('short option group does not consume subsequent positional', function(t) {
85+
const passedArgs = ['-rf', 'foo'];
86+
const passedOptions = { };
87+
const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['foo'] };
88+
const args = parseArgs(passedArgs, passedOptions);
89+
t.deepEqual(args, expected);
90+
91+
t.end();
92+
});
93+
94+
// See: Guideline 5 https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html
95+
test('if terminal of short-option group configured withValue, subsequent positional is stored', function(t) {
96+
const passedArgs = ['-rvf', 'foo'];
97+
const passedOptions = { withValue: ['f'] };
98+
const expected = { flags: { r: true, f: true, v: true }, values: { r: undefined, v: undefined, f: 'foo' }, positionals: [] };
99+
const args = parseArgs(passedArgs, passedOptions);
100+
t.deepEqual(args, expected);
101+
102+
t.end();
103+
});
104+
105+
test('handles short-option groups in conjunction with long-options', function(t) {
106+
const passedArgs = ['-rf', '--foo', 'foo'];
107+
const passedOptions = { withValue: ['foo'] };
108+
const expected = { flags: { r: true, f: true, foo: true }, values: { r: undefined, f: undefined, foo: 'foo' }, positionals: [] };
109+
const args = parseArgs(passedArgs, passedOptions);
110+
t.deepEqual(args, expected);
111+
112+
t.end();
113+
});
114+
115+
test('handles short-option groups with "short" alias configured', function(t) {
116+
const passedArgs = ['-rf'];
117+
const passedOptions = { short: { r: 'remove' } };
118+
const expected = { flags: { remove: true, f: true }, values: { remove: undefined, f: undefined }, positionals: [] };
119+
const args = parseArgs(passedArgs, passedOptions);
120+
t.deepEqual(args, expected);
121+
122+
t.end();
123+
});
124+
73125
test('Everything after a bare `--` is considered a positional argument', function(t) {
74126
const passedArgs = ['--', 'barepositionals', 'mopositionals'];
75127
const expected = { flags: {}, values: {}, positionals: ['barepositionals', 'mopositionals'] };

0 commit comments

Comments
 (0)