Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 8c73279

Browse filesBrowse files
Eommrichardlau
authored andcommitted
util: add default value option to parsearg
Users can set a default value for every expected input argument PR-URL: #44631 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 749a4b3 commit 8c73279
Copy full SHA for 8c73279

File tree

Expand file treeCollapse file tree

5 files changed

+275
-2
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

5 files changed

+275
-2
lines changed
Open diff view settings
Collapse file

‎doc/api/util.md‎

Copy file name to clipboardExpand all lines: doc/api/util.md
+6Lines changed: 6 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,9 @@ equality.
10241024
<!-- YAML
10251025
added: v16.17.0
10261026
changes:
1027+
- version: REPLACEME
1028+
pr-url: https://github.com/nodejs/node/pull/44631
1029+
description: Add support for default values in input `config`.
10271030
- version: v16.17.0
10281031
pr-url: https://github.com/nodejs/node/pull/43459
10291032
description: add support for returning detailed parse information
@@ -1044,6 +1047,9 @@ changes:
10441047
times. If `true`, all values will be collected in an array. If
10451048
`false`, values for the option are last-wins. **Default:** `false`.
10461049
* `short` {string} A single character alias for the option.
1050+
* `default` {string | boolean | string\[] | boolean\[]} The default option
1051+
value when it is not set by args. It must be of the same type as the
1052+
the `type` property. When `multiple` is `true`, it must be an array.
10471053
* `strict` {boolean} Should an error be thrown when unknown arguments
10481054
are encountered, or when arguments are passed that do not match the
10491055
`type` configured in `options`.
Collapse file

‎lib/internal/util/parse_args/parse_args.js‎

Copy file name to clipboardExpand all lines: lib/internal/util/parse_args/parse_args.js
+54-2Lines changed: 54 additions & 2 deletions
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

@@ -34,6 +36,7 @@ const {
3436
isOptionLikeValue,
3537
isShortOptionAndValue,
3638
isShortOptionGroup,
39+
useDefaultValueOption,
3740
objectGetOwn,
3841
optionsGetOwn,
3942
} = require('internal/util/parse_args/utils');
@@ -143,6 +146,24 @@ function storeOption(longOption, optionValue, options, values) {
143146
}
144147
}
145148

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

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

295317
if (ObjectHasOwn(optionConfig, 'short')) {
296318
const shortOption = optionConfig.short;
@@ -304,8 +326,24 @@ const parseArgs = (config = kEmptyObject) => {
304326
}
305327
}
306328

329+
const multipleOption = objectGetOwn(optionConfig, 'multiple');
307330
if (ObjectHasOwn(optionConfig, 'multiple')) {
308-
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`);
309347
}
310348
}
311349
);
@@ -336,6 +374,20 @@ const parseArgs = (config = kEmptyObject) => {
336374
}
337375
});
338376

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+
339391
return result;
340392
};
341393

Collapse file

‎lib/internal/util/parse_args/utils.js‎

Copy file name to clipboardExpand all lines: lib/internal/util/parse_args/utils.js
+14Lines changed: 14 additions & 0 deletions
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
};
Collapse file

‎lib/internal/validators.js‎

Copy file name to clipboardExpand all lines: lib/internal/validators.js
+32Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,36 @@ const validateArray = hideStackFrames((value, name, minLength = 0) => {
269269
}
270270
});
271271

272+
/**
273+
* @callback validateStringArray
274+
* @param {*} value
275+
* @param {string} name
276+
* @returns {asserts value is string[]}
277+
*/
278+
279+
/** @type {validateStringArray} */
280+
function validateStringArray(value, name) {
281+
validateArray(value, name);
282+
for (let i = 0; i < value.length; i++) {
283+
validateString(value[i], `${name}[${i}]`);
284+
}
285+
}
286+
287+
/**
288+
* @callback validateBooleanArray
289+
* @param {*} value
290+
* @param {string} name
291+
* @returns {asserts value is boolean[]}
292+
*/
293+
294+
/** @type {validateBooleanArray} */
295+
function validateBooleanArray(value, name) {
296+
validateArray(value, name);
297+
for (let i = 0; i < value.length; i++) {
298+
validateBoolean(value[i], `${name}[${i}]`);
299+
}
300+
}
301+
272302
// eslint-disable-next-line jsdoc/require-returns-check
273303
/**
274304
* @param {*} signal
@@ -414,6 +444,8 @@ module.exports = {
414444
isUint32,
415445
parseFileMode,
416446
validateArray,
447+
validateStringArray,
448+
validateBooleanArray,
417449
validateBoolean,
418450
validateBuffer,
419451
validateCallback,
Collapse file

‎test/parallel/test-parse-args.mjs‎

Copy file name to clipboardExpand all lines: test/parallel/test-parse-args.mjs
+169Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,3 +823,172 @@ test('tokens: strict:false with -- --', () => {
823823
const { tokens } = parseArgs({ strict: false, args, tokens: true });
824824
assert.deepStrictEqual(tokens, expectedTokens);
825825
});
826+
827+
test('default must be a boolean when option type is boolean', () => {
828+
const args = [];
829+
const options = { alpha: { type: 'boolean', default: 'not a boolean' } };
830+
assert.throws(() => {
831+
parseArgs({ args, options });
832+
}, /"options\.alpha\.default" property must be of type boolean/
833+
);
834+
});
835+
836+
test('default must accept undefined value', () => {
837+
const args = [];
838+
const options = { alpha: { type: 'boolean', default: undefined } };
839+
const result = parseArgs({ args, options });
840+
const expected = {
841+
values: {
842+
__proto__: null,
843+
},
844+
positionals: []
845+
};
846+
assert.deepStrictEqual(result, expected);
847+
});
848+
849+
test('default must be a boolean array when option type is boolean and multiple', () => {
850+
const args = [];
851+
const options = { alpha: { type: 'boolean', multiple: true, default: 'not an array' } };
852+
assert.throws(() => {
853+
parseArgs({ args, options });
854+
}, /"options\.alpha\.default" property must be an instance of Array/
855+
);
856+
});
857+
858+
test('default must be a boolean array when option type is string and multiple is true', () => {
859+
const args = [];
860+
const options = { alpha: { type: 'boolean', multiple: true, default: [true, true, 42] } };
861+
assert.throws(() => {
862+
parseArgs({ args, options });
863+
}, /"options\.alpha\.default\[2\]" property must be of type boolean/
864+
);
865+
});
866+
867+
test('default must be a string when option type is string', () => {
868+
const args = [];
869+
const options = { alpha: { type: 'string', default: true } };
870+
assert.throws(() => {
871+
parseArgs({ args, options });
872+
}, /"options\.alpha\.default" property must be of type string/
873+
);
874+
});
875+
876+
test('default must be an array when option type is string and multiple is true', () => {
877+
const args = [];
878+
const options = { alpha: { type: 'string', multiple: true, default: 'not an array' } };
879+
assert.throws(() => {
880+
parseArgs({ args, options });
881+
}, /"options\.alpha\.default" property must be an instance of Array/
882+
);
883+
});
884+
885+
test('default must be a string array when option type is string and multiple is true', () => {
886+
const args = [];
887+
const options = { alpha: { type: 'string', multiple: true, default: ['str', 42] } };
888+
assert.throws(() => {
889+
parseArgs({ args, options });
890+
}, /"options\.alpha\.default\[1\]" property must be of type string/
891+
);
892+
});
893+
894+
test('default accepted input when multiple is true', () => {
895+
const args = ['--inputStringArr', 'c', '--inputStringArr', 'd', '--inputBoolArr', '--inputBoolArr'];
896+
const options = {
897+
inputStringArr: { type: 'string', multiple: true, default: ['a', 'b'] },
898+
emptyStringArr: { type: 'string', multiple: true, default: [] },
899+
fullStringArr: { type: 'string', multiple: true, default: ['a', 'b'] },
900+
inputBoolArr: { type: 'boolean', multiple: true, default: [false, true, false] },
901+
emptyBoolArr: { type: 'boolean', multiple: true, default: [] },
902+
fullBoolArr: { type: 'boolean', multiple: true, default: [false, true, false] },
903+
};
904+
const expected = { values: { __proto__: null,
905+
inputStringArr: ['c', 'd'],
906+
inputBoolArr: [true, true],
907+
emptyStringArr: [],
908+
fullStringArr: ['a', 'b'],
909+
emptyBoolArr: [],
910+
fullBoolArr: [false, true, false] },
911+
positionals: [] };
912+
const result = parseArgs({ args, options });
913+
assert.deepStrictEqual(result, expected);
914+
});
915+
916+
test('when default is set, the option must be added as result', () => {
917+
const args = [];
918+
const options = {
919+
a: { type: 'string', default: 'HELLO' },
920+
b: { type: 'boolean', default: false },
921+
c: { type: 'boolean', default: true }
922+
};
923+
const expected = { values: { __proto__: null, a: 'HELLO', b: false, c: true }, positionals: [] };
924+
925+
const result = parseArgs({ args, options });
926+
assert.deepStrictEqual(result, expected);
927+
});
928+
929+
test('when default is set, the args value takes precedence', () => {
930+
const args = ['--a', 'WORLD', '--b', '-c'];
931+
const options = {
932+
a: { type: 'string', default: 'HELLO' },
933+
b: { type: 'boolean', default: false },
934+
c: { type: 'boolean', default: true }
935+
};
936+
const expected = { values: { __proto__: null, a: 'WORLD', b: true, c: true }, positionals: [] };
937+
938+
const result = parseArgs({ args, options });
939+
assert.deepStrictEqual(result, expected);
940+
});
941+
942+
test('tokens should not include the default options', () => {
943+
const args = [];
944+
const options = {
945+
a: { type: 'string', default: 'HELLO' },
946+
b: { type: 'boolean', default: false },
947+
c: { type: 'boolean', default: true }
948+
};
949+
950+
const expectedTokens = [];
951+
952+
const { tokens } = parseArgs({ args, options, tokens: true });
953+
assert.deepStrictEqual(tokens, expectedTokens);
954+
});
955+
956+
test('tokens:true should not include the default options after the args input', () => {
957+
const args = ['--z', 'zero', 'positional-item'];
958+
const options = {
959+
z: { type: 'string' },
960+
a: { type: 'string', default: 'HELLO' },
961+
b: { type: 'boolean', default: false },
962+
c: { type: 'boolean', default: true }
963+
};
964+
965+
const expectedTokens = [
966+
{ kind: 'option', name: 'z', rawName: '--z', index: 0, value: 'zero', inlineValue: false },
967+
{ kind: 'positional', index: 2, value: 'positional-item' },
968+
];
969+
970+
const { tokens } = parseArgs({ args, options, tokens: true, allowPositionals: true });
971+
assert.deepStrictEqual(tokens, expectedTokens);
972+
});
973+
974+
test('proto as default value must be ignored', () => {
975+
const args = [];
976+
const options = Object.create(null);
977+
978+
// eslint-disable-next-line no-proto
979+
options.__proto__ = { type: 'string', default: 'HELLO' };
980+
981+
const result = parseArgs({ args, options, allowPositionals: true });
982+
const expected = { values: { __proto__: null }, positionals: [] };
983+
assert.deepStrictEqual(result, expected);
984+
});
985+
986+
987+
test('multiple as false should expect a String', () => {
988+
const args = [];
989+
const options = { alpha: { type: 'string', multiple: false, default: ['array'] } };
990+
assert.throws(() => {
991+
parseArgs({ args, options });
992+
}, /"options\.alpha\.default" property must be of type string/
993+
);
994+
});

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.