@pkgjs/parseargs
Advanced tools
Comparing version 0.3.0 to 0.4.0
# Changelog | ||
## [0.4.0](https://github.com/pkgjs/parseargs/compare/v0.3.0...v0.4.0) (2022-03-12) | ||
### ⚠ BREAKING CHANGES | ||
* parsing, revisit short option groups, add support for combined short and value (#75) | ||
* restructure configuration to take options bag (#63) | ||
### Code Refactoring | ||
* parsing, revisit short option groups, add support for combined short and value ([#75](https://github.com/pkgjs/parseargs/issues/75)) ([a92600f](https://github.com/pkgjs/parseargs/commit/a92600fa6c214508ab1e016fa55879a314f541af)) | ||
* restructure configuration to take options bag ([#63](https://github.com/pkgjs/parseargs/issues/63)) ([b412095](https://github.com/pkgjs/parseargs/commit/b4120957d90e809ee8b607b06e747d3e6a6b213e)) | ||
## [0.3.0](https://github.com/pkgjs/parseargs/compare/v0.2.0...v0.3.0) (2022-02-06) | ||
@@ -4,0 +17,0 @@ |
213
index.js
@@ -5,7 +5,8 @@ 'use strict'; | ||
ArrayPrototypeConcat, | ||
ArrayPrototypeIncludes, | ||
ArrayPrototypeForEach, | ||
ArrayPrototypeShift, | ||
ArrayPrototypeSlice, | ||
ArrayPrototypeSplice, | ||
ArrayPrototypePush, | ||
ObjectHasOwn, | ||
ObjectEntries, | ||
StringPrototypeCharAt, | ||
@@ -15,3 +16,2 @@ StringPrototypeIncludes, | ||
StringPrototypeSlice, | ||
StringPrototypeStartsWith, | ||
} = require('./primordials'); | ||
@@ -21,5 +21,18 @@ | ||
validateArray, | ||
validateObject | ||
validateObject, | ||
validateString, | ||
validateUnion, | ||
validateBoolean, | ||
} = require('./validators'); | ||
const { | ||
findLongOptionForShort, | ||
isLoneLongOption, | ||
isLoneShortOption, | ||
isLongOptionAndValue, | ||
isOptionValue, | ||
isShortOptionAndValue, | ||
isShortOptionGroup | ||
} = require('./utils'); | ||
function getMainArgs() { | ||
@@ -59,13 +72,12 @@ // This function is a placeholder for proposed process.mainArgs. | ||
function storeOptionValue(parseOptions, option, value, result) { | ||
const multiple = parseOptions.multiples && | ||
ArrayPrototypeIncludes(parseOptions.multiples, option); | ||
function storeOptionValue(options, longOption, value, result) { | ||
const optionConfig = options[longOption] || {}; | ||
// Flags | ||
result.flags[option] = true; | ||
result.flags[longOption] = true; | ||
// Values | ||
if (multiple) { | ||
if (optionConfig.multiple) { | ||
// Always store value in array, including for flags. | ||
// result.values[option] starts out not present, | ||
// result.values[longOption] starts out not present, | ||
// first value is added as new array [newValue], | ||
@@ -75,22 +87,39 @@ // subsequent values are pushed to existing array. | ||
const newValue = usedAsFlag ? true : value; | ||
if (result.values[option] !== undefined) | ||
ArrayPrototypePush(result.values[option], newValue); | ||
if (result.values[longOption] !== undefined) | ||
ArrayPrototypePush(result.values[longOption], newValue); | ||
else | ||
result.values[option] = [newValue]; | ||
result.values[longOption] = [newValue]; | ||
} else { | ||
result.values[option] = value; | ||
result.values[longOption] = value; | ||
} | ||
} | ||
const parseArgs = ( | ||
argv = getMainArgs(), | ||
const parseArgs = ({ | ||
args = getMainArgs(), | ||
options = {} | ||
) => { | ||
validateArray(argv, 'argv'); | ||
} = {}) => { | ||
validateArray(args, 'args'); | ||
validateObject(options, 'options'); | ||
for (const key of ['withValue', 'multiples']) { | ||
if (ObjectHasOwn(options, key)) { | ||
validateArray(options[key], `options.${key}`); | ||
ArrayPrototypeForEach( | ||
ObjectEntries(options), | ||
([longOption, optionConfig]) => { | ||
validateObject(optionConfig, `options.${longOption}`); | ||
if (ObjectHasOwn(optionConfig, 'type')) { | ||
validateUnion(optionConfig.type, `options.${longOption}.type`, ['string', 'boolean']); | ||
} | ||
if (ObjectHasOwn(optionConfig, 'short')) { | ||
const shortOption = optionConfig.short; | ||
validateString(shortOption, `options.${longOption}.short`); | ||
if (shortOption.length !== 1) { | ||
throw new Error(`options.${longOption}.short must be a single character, got '${shortOption}'`); | ||
} | ||
} | ||
if (ObjectHasOwn(optionConfig, 'multiple')) { | ||
validateBoolean(optionConfig.multiple, `options.${longOption}.multiple`); | ||
} | ||
} | ||
} | ||
); | ||
@@ -103,77 +132,85 @@ const result = { | ||
let pos = 0; | ||
while (pos < argv.length) { | ||
let arg = argv[pos]; | ||
let remainingArgs = ArrayPrototypeSlice(args); | ||
while (remainingArgs.length > 0) { | ||
const arg = ArrayPrototypeShift(remainingArgs); | ||
const nextArg = remainingArgs[0]; | ||
if (StringPrototypeStartsWith(arg, '-')) { | ||
if (arg === '-') { | ||
// '-' commonly used to represent stdin/stdout, treat as positional | ||
result.positionals = ArrayPrototypeConcat(result.positionals, '-'); | ||
++pos; | ||
continue; | ||
} else if (arg === '--') { | ||
// Everything after a bare '--' is considered a positional argument | ||
// and is returned verbatim | ||
result.positionals = ArrayPrototypeConcat( | ||
result.positionals, | ||
ArrayPrototypeSlice(argv, ++pos) | ||
); | ||
return result; | ||
} else if (StringPrototypeCharAt(arg, 1) !== '-') { | ||
// Look for shortcodes: -fXzy and expand them to -f -X -z -y: | ||
if (arg.length > 2) { | ||
for (let i = 2; i < arg.length; i++) { | ||
const short = StringPrototypeCharAt(arg, i); | ||
// Add 'i' to 'pos' such that short options are parsed in order | ||
// of definition: | ||
ArrayPrototypeSplice(argv, pos + (i - 1), 0, `-${short}`); | ||
} | ||
} | ||
// Check if `arg` is an options terminator. | ||
// Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html | ||
if (arg === '--') { | ||
// Everything after a bare '--' is considered a positional argument. | ||
result.positionals = ArrayPrototypeConcat( | ||
result.positionals, | ||
remainingArgs | ||
); | ||
break; // Finished processing args, leave while loop. | ||
} | ||
arg = StringPrototypeCharAt(arg, 1); // short | ||
if (options.short && options.short[arg]) | ||
arg = options.short[arg]; // now long! | ||
// ToDo: later code tests for `=` in arg and wrong for shorts | ||
} else { | ||
arg = StringPrototypeSlice(arg, 2); // remove leading -- | ||
if (isLoneShortOption(arg)) { | ||
// e.g. '-f' | ||
const shortOption = StringPrototypeCharAt(arg, 1); | ||
const longOption = findLongOptionForShort(shortOption, options); | ||
let optionValue; | ||
if (options[longOption]?.type === 'string' && isOptionValue(nextArg)) { | ||
// e.g. '-f', 'bar' | ||
optionValue = ArrayPrototypeShift(remainingArgs); | ||
} | ||
storeOptionValue(options, longOption, optionValue, result); | ||
continue; | ||
} | ||
if (StringPrototypeIncludes(arg, '=')) { | ||
// Store option=value same way independent of `withValue` as: | ||
// - looks like a value, store as a value | ||
// - match the intention of the user | ||
// - preserve information for author to process further | ||
const index = StringPrototypeIndexOf(arg, '='); | ||
storeOptionValue( | ||
options, | ||
StringPrototypeSlice(arg, 0, index), | ||
StringPrototypeSlice(arg, index + 1), | ||
result); | ||
} else if (pos + 1 < argv.length && | ||
!StringPrototypeStartsWith(argv[pos + 1], '-') | ||
) { | ||
// withValue option should also support setting values when '= | ||
// isn't used ie. both --foo=b and --foo b should work | ||
if (isShortOptionGroup(arg, options)) { | ||
// Expand -fXzy to -f -X -z -y | ||
const expanded = []; | ||
for (let index = 1; index < arg.length; index++) { | ||
const shortOption = StringPrototypeCharAt(arg, index); | ||
const longOption = findLongOptionForShort(shortOption, options); | ||
if (options[longOption]?.type !== 'string' || | ||
index === arg.length - 1) { | ||
// Boolean option, or last short in group. Well formed. | ||
ArrayPrototypePush(expanded, `-${shortOption}`); | ||
} else { | ||
// String option in middle. Yuck. | ||
// ToDo: if strict then throw | ||
// Expand -abfFILE to -a -b -fFILE | ||
ArrayPrototypePush(expanded, `-${StringPrototypeSlice(arg, index)}`); | ||
break; // finished short group | ||
} | ||
} | ||
remainingArgs = ArrayPrototypeConcat(expanded, remainingArgs); | ||
continue; | ||
} | ||
// If withValue option is specified, take next position arguement as | ||
// value and then increment pos so that we don't re-evaluate that | ||
// arg, else set value as undefined ie. --foo b --bar c, after setting | ||
// b as the value for foo, evaluate --bar next and skip 'b' | ||
const val = options.withValue && | ||
ArrayPrototypeIncludes(options.withValue, arg) ? argv[++pos] : | ||
undefined; | ||
storeOptionValue(options, arg, val, result); | ||
} else { | ||
// Cases when an arg is specified without a value, example | ||
// '--foo --bar' <- 'foo' and 'bar' flags should be set to true and | ||
// shave value as undefined | ||
storeOptionValue(options, arg, undefined, result); | ||
if (isShortOptionAndValue(arg, options)) { | ||
// e.g. -fFILE | ||
const shortOption = StringPrototypeCharAt(arg, 1); | ||
const longOption = findLongOptionForShort(shortOption, options); | ||
const optionValue = StringPrototypeSlice(arg, 2); | ||
storeOptionValue(options, longOption, optionValue, result); | ||
continue; | ||
} | ||
if (isLoneLongOption(arg)) { | ||
// e.g. '--foo' | ||
const longOption = StringPrototypeSlice(arg, 2); | ||
let optionValue; | ||
if (options[longOption]?.type === 'string' && isOptionValue(nextArg)) { | ||
// e.g. '--foo', 'bar' | ||
optionValue = ArrayPrototypeShift(remainingArgs); | ||
} | ||
storeOptionValue(options, longOption, optionValue, result); | ||
continue; | ||
} | ||
} else { | ||
// Arguements without a dash prefix are considered "positional" | ||
ArrayPrototypePush(result.positionals, arg); | ||
if (isLongOptionAndValue(arg)) { | ||
// e.g. --foo=bar | ||
const index = StringPrototypeIndexOf(arg, '='); | ||
const longOption = StringPrototypeSlice(arg, 2, index); | ||
const optionValue = StringPrototypeSlice(arg, index + 1); | ||
storeOptionValue(options, longOption, optionValue, result); | ||
continue; | ||
} | ||
pos++; | ||
// Anything left is a positional | ||
ArrayPrototypePush(result.positionals, arg); | ||
} | ||
@@ -180,0 +217,0 @@ |
{ | ||
"name": "@pkgjs/parseargs", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"description": "Polyfill of future proposal for `util.parseArgs()`", | ||
"main": "index.js", | ||
"exports": { | ||
".": "./index.js", | ||
"./package.json": "./package.json" | ||
}, | ||
"scripts": { | ||
"coverage": "c8 --check-coverage node test/index.js", | ||
"test": "c8 node test/index.js", | ||
"coverage": "c8 --check-coverage tape 'test/*.js'", | ||
"test": "c8 tape 'test/*.js'", | ||
"posttest": "eslint .", | ||
@@ -10,0 +14,0 @@ "fix": "npm run posttest -- --fix" |
@@ -12,5 +12,8 @@ <!-- omit in toc --> | ||
### Scope | ||
This package was implemented using [tape](https://www.npmjs.com/package/tape) as its test harness. | ||
It is already possible to build great arg parsing modules on top of what Node.js provides; the prickly API is abstracted away by these modules. Thus, process.parseArgs() is not necessarily intended for library authors; it is intended for developers of simple CLI tools, ad-hoc scripts, deployed Node.js applications, and learning materials. | ||
It is exceedingly difficult to provide an API which would both be friendly to these Node.js users while being extensible enough for libraries to build upon. We chose to prioritize these use cases because these are currently not well-served by Node.js' API. | ||
### Links & Resources | ||
@@ -29,3 +32,3 @@ | ||
- [Implementation:](#implementation) | ||
- [💡 `util.parseArgs(argv)` Proposal](#-utilparseargsargv-proposal) | ||
- [💡 `util.parseArgs([config])` Proposal](#-utilparseargsconfig-proposal) | ||
- [📃 Examples](#-examples) | ||
@@ -60,2 +63,4 @@ - [F.A.Qs](#faqs) | ||
This package was implemented using [tape](https://www.npmjs.com/package/tape) as its test harness. | ||
---- | ||
@@ -75,15 +80,12 @@ | ||
## 💡 `util.parseArgs([argv][, options])` Proposal | ||
## 💡 `util.parseArgs([config])` Proposal | ||
* `argv` {string[]} (Optional) Array of argument strings; defaults | ||
to [`process.mainArgs`](process_argv) | ||
* `options` {Object} (Optional) The `options` parameter is an | ||
* `config` {Object} (Optional) The `config` parameter is an | ||
object supporting the following properties: | ||
* `withValue` {string[]} (Optional) An `Array` of argument | ||
strings which expect a value to be defined in `argv` (see [Options][] | ||
for details) | ||
* `multiples` {string[]} (Optional) An `Array` of argument | ||
strings which, when appearing multiple times in `argv`, will be concatenated | ||
into an `Array` | ||
* `short` {Object} (Optional) An `Object` of key, value pairs of strings which map a "short" alias to an argument; When appearing multiples times in `argv`; Respects `withValue` & `multiples` | ||
* `args` {string[]} (Optional) Array of argument strings; defaults | ||
to [`process.mainArgs`](process_argv) | ||
* `options` {Object} (Optional) An object describing the known options to look for in `args`; `options` keys are the long names of the known options, and the values are objects with the following properties: | ||
* `type` {'string'|'boolean'} (Optional) Type of known option; defaults to `'boolean'`; | ||
* `multiple` {boolean} (Optional) If true, when appearing one or more times in `args`, results are collected in an `Array` | ||
* `short` {string} (Optional) A single character alias for an option; When appearing one or more times in `args`; Respects the `multiple` configuration | ||
* `strict` {Boolean} (Optional) A `Boolean` on wheather or not to throw an error when unknown args are encountered | ||
@@ -106,5 +108,5 @@ * Returns: {Object} An object having properties: | ||
const { parseArgs } = require('@pkgjs/parseargs'); | ||
const argv = ['-f', '--foo=a', '--bar', 'b']; | ||
const args = ['-f', '--foo=a', '--bar', 'b']; | ||
const options = {}; | ||
const { flags, values, positionals } = parseArgs(argv, options); | ||
const { flags, values, positionals } = parseArgs({ args, options }); | ||
// flags = { f: true, bar: true } | ||
@@ -118,7 +120,9 @@ // values = { foo: 'a' } | ||
// withValue | ||
const argv = ['-f', '--foo=a', '--bar', 'b']; | ||
const args = ['-f', '--foo=a', '--bar', 'b']; | ||
const options = { | ||
withValue: ['bar'] | ||
foo: { | ||
type: 'string', | ||
}, | ||
}; | ||
const { flags, values, positionals } = parseArgs(argv, options); | ||
const { flags, values, positionals } = parseArgs({ args, options }); | ||
// flags = { f: true } | ||
@@ -131,9 +135,11 @@ // values = { foo: 'a', bar: 'b' } | ||
const { parseArgs } = require('@pkgjs/parseargs'); | ||
// withValue & multiples | ||
const argv = ['-f', '--foo=a', '--foo', 'b']; | ||
// withValue & multiple | ||
const args = ['-f', '--foo=a', '--foo', 'b']; | ||
const options = { | ||
withValue: ['foo'], | ||
multiples: ['foo'] | ||
foo: { | ||
type: 'string', | ||
multiple: true, | ||
}, | ||
}; | ||
const { flags, values, positionals } = parseArgs(argv, options); | ||
const { flags, values, positionals } = parseArgs({ args, options }); | ||
// flags = { f: true } | ||
@@ -147,7 +153,9 @@ // values = { foo: ['a', 'b'] } | ||
// shorts | ||
const argv = ['-f', 'b']; | ||
const args = ['-f', 'b']; | ||
const options = { | ||
short: { f: 'foo' } | ||
foo: { | ||
short: 'f', | ||
}, | ||
}; | ||
const { flags, values, positionals } = parseArgs(argv, options); | ||
const { flags, values, positionals } = parseArgs({ args, options }); | ||
// flags = { foo: true } | ||
@@ -161,3 +169,3 @@ // values = {} | ||
- Is `cmd --foo=bar baz` the same as `cmd baz --foo=bar`? | ||
- Yes, if `withValue: ['foo']`, otherwise no | ||
- yes | ||
- Does the parser execute a function? | ||
@@ -164,0 +172,0 @@ - no |
@@ -5,2 +5,4 @@ 'use strict'; | ||
ArrayIsArray, | ||
ArrayPrototypeIncludes, | ||
ArrayPrototypeJoin, | ||
} = require('./primordials'); | ||
@@ -14,2 +16,20 @@ | ||
function validateString(value, name) { | ||
if (typeof value !== 'string') { | ||
throw new ERR_INVALID_ARG_TYPE(name, 'String', value); | ||
} | ||
} | ||
function validateUnion(value, name, union) { | ||
if (!ArrayPrototypeIncludes(union, value)) { | ||
throw new ERR_INVALID_ARG_TYPE(name, `('${ArrayPrototypeJoin(union, '|')}')`, value); | ||
} | ||
} | ||
function validateBoolean(value, name) { | ||
if (typeof value !== 'boolean') { | ||
throw new ERR_INVALID_ARG_TYPE(name, 'Boolean', value); | ||
} | ||
} | ||
function validateArray(value, name) { | ||
@@ -47,2 +67,5 @@ if (!ArrayIsArray(value)) { | ||
validateObject, | ||
validateString, | ||
validateUnion, | ||
validateBoolean, | ||
}; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
50009
10
800
217