GeezerVison is showing github.com — util: add default value option to parsearg · nodejs/node@db5aeed
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/util.md +9-1Lines changed: 9 additions & 1 deletion - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1025,7 +1025,12 @@ equality. | 1025 | 1025 | <!-- YAML | 1026 | 1026 | added: v18.3.0 | 1027 | 1027 | changes: | 1028 | | - - version: v18.7.0 | | 1028 | + - version: REPLACEME | | 1029 | + pr-url: https://github.com/nodejs/node/pull/44631 | | 1030 | + description: Add support for default values in input `config`. | | 1031 | + - version: | | 1032 | + - v18.7.0 | | 1033 | + - v16.17.0 | 1029 | 1034 | pr-url: https://github.com/nodejs/node/pull/43459 | 1030 | 1035 | description: add support for returning detailed parse information | 1031 | 1036 | using `tokens` in input `config` and returned properties. | @@ -1045,6 +1050,9 @@ changes: | 1045 | 1050 | times. If `true`, all values will be collected in an array. If | 1046 | 1051 | `false`, values for the option are last-wins. **Default:** `false`. | 1047 | 1052 | * `short` {string} A single character alias for the option. | | 1053 | + * `default` {string | boolean | string\[] | boolean\[]} The default option | | 1054 | + value when it is not set by args. It must be of the same type as the | | 1055 | + the `type` property. When `multiple` is `true`, it must be an array. | 1048 | 1056 | * `strict` {boolean} Should an error be thrown when unknown arguments | 1049 | 1057 | are encountered, or when arguments are passed that do not match the | 1050 | 1058 | `type` configured in `options`. | |
Collapse file 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 number | Diff line number | Diff line change |
|---|
@@ -20,8 +20,10 @@ const { | 20 | 20 | const { | 21 | 21 | validateArray, | 22 | 22 | validateBoolean, | | 23 | + validateBooleanArray, | 23 | 24 | validateObject, | 24 | 25 | validateString, | | 26 | + validateStringArray, | 25 | 27 | validateUnion, | 26 | 28 | } = require('internal/validators'); | 27 | 29 |
| @@ -34,6 +36,7 @@ const { | 34 | 36 | isOptionLikeValue, | 35 | 37 | isShortOptionAndValue, | 36 | 38 | isShortOptionGroup, | | 39 | + useDefaultValueOption, | 37 | 40 | objectGetOwn, | 38 | 41 | optionsGetOwn, | 39 | 42 | } = require('internal/util/parse_args/utils'); | @@ -143,6 +146,24 @@ function storeOption(longOption, optionValue, options, values) { | 143 | 146 | } | 144 | 147 | } | 145 | 148 |
| | 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 | + | 146 | 167 | /** | 147 | 168 | * Process args and turn into identified tokens: | 148 | 169 | * - option (along with value, if any) | @@ -290,7 +311,8 @@ const parseArgs = (config = kEmptyObject) => { | 290 | 311 | validateObject(optionConfig, `options.${longOption}`); | 291 | 312 |
| 292 | 313 | // 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']); | 294 | 316 |
| 295 | 317 | if (ObjectHasOwn(optionConfig, 'short')) { | 296 | 318 | const shortOption = optionConfig.short; | @@ -304,8 +326,24 @@ const parseArgs = (config = kEmptyObject) => { | 304 | 326 | } | 305 | 327 | } | 306 | 328 |
| | 329 | + const multipleOption = objectGetOwn(optionConfig, 'multiple'); | 307 | 330 | 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`); | 309 | 347 | } | 310 | 348 | } | 311 | 349 | ); | @@ -336,6 +374,20 @@ const parseArgs = (config = kEmptyObject) => { | 336 | 374 | } | 337 | 375 | }); | 338 | 376 |
| | 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 | + | 339 | 391 | return result; | 340 | 392 | }; | 341 | 393 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/util/parse_args/utils.js +14Lines changed: 14 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -170,6 +170,19 @@ function findLongOptionForShort(shortOption, options) { | 170 | 170 | return longOptionEntry?.[0] ?? shortOption; | 171 | 171 | } | 172 | 172 |
| | 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 | + | 173 | 186 | module.exports = { | 174 | 187 | findLongOptionForShort, | 175 | 188 | isLoneLongOption, | @@ -179,6 +192,7 @@ module.exports = { | 179 | 192 | isOptionLikeValue, | 180 | 193 | isShortOptionAndValue, | 181 | 194 | isShortOptionGroup, | | 195 | + useDefaultValueOption, | 182 | 196 | objectGetOwn, | 183 | 197 | optionsGetOwn, | 184 | 198 | }; |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/validators.js +32Lines changed: 32 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -276,6 +276,36 @@ const validateArray = hideStackFrames((value, name, minLength = 0) => { | 276 | 276 | } | 277 | 277 | }); | 278 | 278 |
| | 279 | +/** | | 280 | + * @callback validateStringArray | | 281 | + * @param {*} value | | 282 | + * @param {string} name | | 283 | + * @returns {asserts value is string[]} | | 284 | + */ | | 285 | + | | 286 | +/** @type {validateStringArray} */ | | 287 | +function validateStringArray(value, name) { | | 288 | + validateArray(value, name); | | 289 | + for (let i = 0; i < value.length; i++) { | | 290 | + validateString(value[i], `${name}[${i}]`); | | 291 | + } | | 292 | +} | | 293 | + | | 294 | +/** | | 295 | + * @callback validateBooleanArray | | 296 | + * @param {*} value | | 297 | + * @param {string} name | | 298 | + * @returns {asserts value is boolean[]} | | 299 | + */ | | 300 | + | | 301 | +/** @type {validateBooleanArray} */ | | 302 | +function validateBooleanArray(value, name) { | | 303 | + validateArray(value, name); | | 304 | + for (let i = 0; i < value.length; i++) { | | 305 | + validateBoolean(value[i], `${name}[${i}]`); | | 306 | + } | | 307 | +} | | 308 | + | 279 | 309 | // eslint-disable-next-line jsdoc/require-returns-check | 280 | 310 | /** | 281 | 311 | * @param {*} signal | @@ -471,6 +501,8 @@ module.exports = { | 471 | 501 | isUint32, | 472 | 502 | parseFileMode, | 473 | 503 | validateArray, | | 504 | + validateStringArray, | | 505 | + validateBooleanArray, | 474 | 506 | validateBoolean, | 475 | 507 | validateBuffer, | 476 | 508 | validateEncoding, | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-parse-args.mjs +169Lines changed: 169 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -823,3 +823,172 @@ test('tokens: strict:false with -- --', () => { | 823 | 823 | const { tokens } = parseArgs({ strict: false, args, tokens: true }); | 824 | 824 | assert.deepStrictEqual(tokens, expectedTokens); | 825 | 825 | }); | | 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 | +}); |
|