Skip to content

Commit cd20847

Browse files
authored
feat: add default option parameter (#142)
1 parent 89e1b63 commit cd20847

File tree

6 files changed

+292
-2
lines changed

6 files changed

+292
-2
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ changes:
3030
times. If `true`, all values will be collected in an array. If
3131
`false`, values for the option are last-wins. **Default:** `false`.
3232
* `short` {string} A single character alias for the option.
33+
* `default` {string | boolean | string\[] | boolean\[]} The default option
34+
value when it is not set by args. It must be of the same type as the
35+
the `type` property. When `multiple` is `true`, it must be an array.
3336
* `strict` {boolean} Should an error be thrown when unknown arguments
3437
are encountered, or when arguments are passed that do not match the
3538
`type` configured in `options`.

examples/is-default-value.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict';
2+
3+
// This example shows how to understand if a default value is used or not.
4+
5+
// 1. const { parseArgs } = require('node:util'); // from node
6+
// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
7+
const { parseArgs } = require('..'); // in repo
8+
9+
const options = {
10+
file: { short: 'f', type: 'string', default: 'FOO' },
11+
};
12+
13+
const { values, tokens } = parseArgs({ options, tokens: true });
14+
15+
const isFileDefault = !tokens.some((token) => token.kind === 'option' &&
16+
token.name === 'file'
17+
);
18+
19+
console.log(values);
20+
console.log(`Is the file option [${values.file}] the default value? ${isFileDefault}`);
21+
22+
// Try the following:
23+
// node is-default-value.js
24+
// node is-default-value.js -f FILE
25+
// node is-default-value.js --file FILE

index.js

+55-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ const {
2020
const {
2121
validateArray,
2222
validateBoolean,
23+
validateBooleanArray,
2324
validateObject,
2425
validateString,
26+
validateStringArray,
2527
validateUnion,
2628
} = require('./internal/validators');
2729

@@ -38,6 +40,7 @@ const {
3840
isOptionLikeValue,
3941
isShortOptionAndValue,
4042
isShortOptionGroup,
43+
useDefaultValueOption,
4144
objectGetOwn,
4245
optionsGetOwn,
4346
} = require('./utils');
@@ -142,6 +145,24 @@ function storeOption(longOption, optionValue, options, values) {
142145
}
143146
}
144147

148+
/**
149+
* Store the default option value in `values`.
150+
*
151+
* @param {string} longOption - long option name e.g. 'foo'
152+
* @param {string
153+
* | boolean
154+
* | string[]
155+
* | boolean[]} optionValue - default value from option config
156+
* @param {object} values - option values returned in `values` by parseArgs
157+
*/
158+
function storeDefaultOption(longOption, optionValue, values) {
159+
if (longOption === '__proto__') {
160+
return; // No. Just no.
161+
}
162+
163+
values[longOption] = optionValue;
164+
}
165+
145166
/**
146167
* Process args and turn into identified tokens:
147168
* - option (along with value, if any)
@@ -265,6 +286,7 @@ function argsToTokens(args, options) {
265286

266287
ArrayPrototypePush(tokens, { kind: 'positional', index, value: arg });
267288
}
289+
268290
return tokens;
269291
}
270292

@@ -289,7 +311,8 @@ const parseArgs = (config = kEmptyObject) => {
289311
validateObject(optionConfig, `options.${longOption}`);
290312

291313
// type is required
292-
validateUnion(objectGetOwn(optionConfig, 'type'), `options.${longOption}.type`, ['string', 'boolean']);
314+
const optionType = objectGetOwn(optionConfig, 'type');
315+
validateUnion(optionType, `options.${longOption}.type`, ['string', 'boolean']);
293316

294317
if (ObjectHasOwn(optionConfig, 'short')) {
295318
const shortOption = optionConfig.short;
@@ -303,8 +326,24 @@ const parseArgs = (config = kEmptyObject) => {
303326
}
304327
}
305328

329+
const multipleOption = objectGetOwn(optionConfig, 'multiple');
306330
if (ObjectHasOwn(optionConfig, 'multiple')) {
307-
validateBoolean(optionConfig.multiple, `options.${longOption}.multiple`);
331+
validateBoolean(multipleOption, `options.${longOption}.multiple`);
332+
}
333+
334+
const defaultValue = objectGetOwn(optionConfig, 'default');
335+
if (defaultValue !== undefined) {
336+
let validator;
337+
switch (optionType) {
338+
case 'string':
339+
validator = multipleOption ? validateStringArray : validateString;
340+
break;
341+
342+
case 'boolean':
343+
validator = multipleOption ? validateBooleanArray : validateBoolean;
344+
break;
345+
}
346+
validator(defaultValue, `options.${longOption}.default`);
308347
}
309348
}
310349
);
@@ -335,6 +374,20 @@ const parseArgs = (config = kEmptyObject) => {
335374
}
336375
});
337376

377+
// Phase 3: fill in default values for missing args
378+
ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption,
379+
1: optionConfig }) => {
380+
const mustSetDefault = useDefaultValueOption(longOption,
381+
optionConfig,
382+
result.values);
383+
if (mustSetDefault) {
384+
storeDefaultOption(longOption,
385+
objectGetOwn(optionConfig, 'default'),
386+
result.values);
387+
}
388+
});
389+
390+
338391
return result;
339392
};
340393

internal/validators.js

+21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
'use strict';
22

3+
// This file is a proxy of the original file located at:
4+
// https://github.com/nodejs/node/blob/main/lib/internal/validators.js
5+
// Every addition or modification to this file must be evaluated
6+
// during the PR review.
7+
38
const {
49
ArrayIsArray,
510
ArrayPrototypeIncludes,
@@ -36,6 +41,20 @@ function validateArray(value, name) {
3641
}
3742
}
3843

44+
function validateStringArray(value, name) {
45+
validateArray(value, name);
46+
for (let i = 0; i < value.length; i++) {
47+
validateString(value[i], `${name}[${i}]`);
48+
}
49+
}
50+
51+
function validateBooleanArray(value, name) {
52+
validateArray(value, name);
53+
for (let i = 0; i < value.length; i++) {
54+
validateBoolean(value[i], `${name}[${i}]`);
55+
}
56+
}
57+
3958
/**
4059
* @param {unknown} value
4160
* @param {string} name
@@ -63,6 +82,8 @@ module.exports = {
6382
validateArray,
6483
validateObject,
6584
validateString,
85+
validateStringArray,
6686
validateUnion,
6787
validateBoolean,
88+
validateBooleanArray,
6889
};

test/default-values.js

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/* global assert */
2+
/* eslint max-len: 0 */
3+
'use strict';
4+
5+
const { test } = require('./utils');
6+
const { parseArgs } = require('../index.js');
7+
8+
test('default must be a boolean when option type is boolean', () => {
9+
const args = [];
10+
const options = { alpha: { type: 'boolean', default: 'not a boolean' } };
11+
assert.throws(() => {
12+
parseArgs({ args, options });
13+
}, /options\.alpha\.default must be Boolean/
14+
);
15+
});
16+
17+
test('default must accept undefined value', () => {
18+
const args = [];
19+
const options = { alpha: { type: 'boolean', default: undefined } };
20+
const result = parseArgs({ args, options });
21+
const expected = {
22+
values: {
23+
__proto__: null,
24+
},
25+
positionals: []
26+
};
27+
assert.deepStrictEqual(result, expected);
28+
});
29+
30+
test('default must be a boolean array when option type is boolean and multiple', () => {
31+
const args = [];
32+
const options = { alpha: { type: 'boolean', multiple: true, default: 'not an array' } };
33+
assert.throws(() => {
34+
parseArgs({ args, options });
35+
}, /options\.alpha\.default must be Array/
36+
);
37+
});
38+
39+
test('default must be a boolean array when option type is string and multiple is true', () => {
40+
const args = [];
41+
const options = { alpha: { type: 'boolean', multiple: true, default: [true, true, 42] } };
42+
assert.throws(() => {
43+
parseArgs({ args, options });
44+
}, /options\.alpha\.default\[2\] must be Boolean/
45+
);
46+
});
47+
48+
test('default must be a string when option type is string', () => {
49+
const args = [];
50+
const options = { alpha: { type: 'string', default: true } };
51+
assert.throws(() => {
52+
parseArgs({ args, options });
53+
}, /options\.alpha\.default must be String/
54+
);
55+
});
56+
57+
test('default must be an array when option type is string and multiple is true', () => {
58+
const args = [];
59+
const options = { alpha: { type: 'string', multiple: true, default: 'not an array' } };
60+
assert.throws(() => {
61+
parseArgs({ args, options });
62+
}, /options\.alpha\.default must be Array/
63+
);
64+
});
65+
66+
test('default must be a string array when option type is string and multiple is true', () => {
67+
const args = [];
68+
const options = { alpha: { type: 'string', multiple: true, default: ['str', 42] } };
69+
assert.throws(() => {
70+
parseArgs({ args, options });
71+
}, /options\.alpha\.default\[1\] must be String/
72+
);
73+
});
74+
75+
test('default accepted input when multiple is true', () => {
76+
const args = ['--inputStringArr', 'c', '--inputStringArr', 'd', '--inputBoolArr', '--inputBoolArr'];
77+
const options = {
78+
inputStringArr: { type: 'string', multiple: true, default: ['a', 'b'] },
79+
emptyStringArr: { type: 'string', multiple: true, default: [] },
80+
fullStringArr: { type: 'string', multiple: true, default: ['a', 'b'] },
81+
inputBoolArr: { type: 'boolean', multiple: true, default: [false, true, false] },
82+
emptyBoolArr: { type: 'boolean', multiple: true, default: [] },
83+
fullBoolArr: { type: 'boolean', multiple: true, default: [false, true, false] },
84+
};
85+
const expected = { values: { __proto__: null,
86+
inputStringArr: ['c', 'd'],
87+
inputBoolArr: [true, true],
88+
emptyStringArr: [],
89+
fullStringArr: ['a', 'b'],
90+
emptyBoolArr: [],
91+
fullBoolArr: [false, true, false] },
92+
positionals: [] };
93+
const result = parseArgs({ args, options });
94+
assert.deepStrictEqual(result, expected);
95+
});
96+
97+
test('when default is set, the option must be added as result', () => {
98+
const args = [];
99+
const options = {
100+
a: { type: 'string', default: 'HELLO' },
101+
b: { type: 'boolean', default: false },
102+
c: { type: 'boolean', default: true }
103+
};
104+
const expected = { values: { __proto__: null, a: 'HELLO', b: false, c: true }, positionals: [] };
105+
106+
const result = parseArgs({ args, options });
107+
assert.deepStrictEqual(result, expected);
108+
});
109+
110+
test('when default is set, the args value takes precedence', () => {
111+
const args = ['--a', 'WORLD', '--b', '-c'];
112+
const options = {
113+
a: { type: 'string', default: 'HELLO' },
114+
b: { type: 'boolean', default: false },
115+
c: { type: 'boolean', default: true }
116+
};
117+
const expected = { values: { __proto__: null, a: 'WORLD', b: true, c: true }, positionals: [] };
118+
119+
const result = parseArgs({ args, options });
120+
assert.deepStrictEqual(result, expected);
121+
});
122+
123+
test('tokens should not include the default options', () => {
124+
const args = [];
125+
const options = {
126+
a: { type: 'string', default: 'HELLO' },
127+
b: { type: 'boolean', default: false },
128+
c: { type: 'boolean', default: true }
129+
};
130+
131+
const expectedTokens = [];
132+
133+
const { tokens } = parseArgs({ args, options, tokens: true });
134+
assert.deepStrictEqual(tokens, expectedTokens);
135+
});
136+
137+
test('tokens:true should not include the default options after the args input', () => {
138+
const args = ['--z', 'zero', 'positional-item'];
139+
const options = {
140+
z: { type: 'string' },
141+
a: { type: 'string', default: 'HELLO' },
142+
b: { type: 'boolean', default: false },
143+
c: { type: 'boolean', default: true }
144+
};
145+
146+
const expectedTokens = [
147+
{ kind: 'option', name: 'z', rawName: '--z', index: 0, value: 'zero', inlineValue: false },
148+
{ kind: 'positional', index: 2, value: 'positional-item' },
149+
];
150+
151+
const { tokens } = parseArgs({ args, options, tokens: true, allowPositionals: true });
152+
assert.deepStrictEqual(tokens, expectedTokens);
153+
});
154+
155+
test('proto as default value must be ignored', () => {
156+
const args = [];
157+
const options = Object.create(null);
158+
159+
// eslint-disable-next-line no-proto
160+
options.__proto__ = { type: 'string', default: 'HELLO' };
161+
162+
const result = parseArgs({ args, options, allowPositionals: true });
163+
const expected = { values: { __proto__: null }, positionals: [] };
164+
assert.deepStrictEqual(result, expected);
165+
});
166+
167+
168+
test('multiple as false should expect a String', () => {
169+
const args = [];
170+
const options = { alpha: { type: 'string', multiple: false, default: ['array'] } };
171+
assert.throws(() => {
172+
parseArgs({ args, options });
173+
}, / must be String got array/);
174+
});

utils.js

+14
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,19 @@ function findLongOptionForShort(shortOption, options) {
170170
return longOptionEntry?.[0] ?? shortOption;
171171
}
172172

173+
/**
174+
* Check if the given option includes a default value
175+
* and that option has not been set by the input args.
176+
*
177+
* @param {string} longOption - long option name e.g. 'foo'
178+
* @param {object} optionConfig - the option configuration properties
179+
* @param {object} values - option values returned in `values` by parseArgs
180+
*/
181+
function useDefaultValueOption(longOption, optionConfig, values) {
182+
return objectGetOwn(optionConfig, 'default') !== undefined &&
183+
values[longOption] === undefined;
184+
}
185+
173186
module.exports = {
174187
findLongOptionForShort,
175188
isLoneLongOption,
@@ -179,6 +192,7 @@ module.exports = {
179192
isOptionLikeValue,
180193
isShortOptionAndValue,
181194
isShortOptionGroup,
195+
useDefaultValueOption,
182196
objectGetOwn,
183197
optionsGetOwn,
184198
};

0 commit comments

Comments
 (0)