Skip to content

Commit

Permalink
feat: Add strict mode to parser (#74)
Browse files Browse the repository at this point in the history
This PR introduces a strict mode parser config that is enabled by default.

Errors on:
- Unknown option encountered
- Option of type:'string' used like a boolean option e.g. lone --string
- Option of type:'boolean' used like a string option e.g. --boolean=foo
  • Loading branch information
aaronccasanova committed Apr 13, 2022
1 parent 8232d36 commit 8267d02
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 48 deletions.
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ process.mainArgs = process.argv.slice(process._exec ? 1 : 2)
* `type` {'string'|'boolean'} (Required) Type of known option
* `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
* `strict` {Boolean} (Optional) A `Boolean` for whether or not to throw an error when unknown options are encountered, `type:'string'` options are missing an options-argument, or `type:'boolean'` options are passed an options-argument; defaults to `true`
* Returns: {Object} An object having properties:
* `values` {Object}, key:value for each option found. Value is a string for string options, or `true` for boolean options, or an array (of strings or booleans) for options configured as `multiple:true`.
* `positionals` {string[]}, containing [Positionals][]
Expand All @@ -98,59 +98,59 @@ const { parseArgs } = require('@pkgjs/parseargs');
```
```js
// unconfigured
const { parseArgs } = require('@pkgjs/parseargs');
const args = ['-f', '--foo=a', '--bar', 'b'];
const options = {};
const { values, positionals } = parseArgs({ args, options });
// values = { f: true, foo: 'a', bar: true }
// positionals = ['b']
```
```js
const { parseArgs } = require('@pkgjs/parseargs');
// type:string
const args = ['-f', '--foo=a', '--bar', 'b'];
// specify the options that may be used
const options = {
bar: {
type: 'string',
},
foo: { type: 'string'},
bar: { type: 'boolean' },
};
const args = ['--foo=a', '--bar'];
const { values, positionals } = parseArgs({ args, options });
// values = { f: true, foo: 'a', bar: 'b' }
// values = { foo: 'a', bar: true }
// positionals = []
```
```js
const { parseArgs } = require('@pkgjs/parseargs');
// type:string & multiple
const args = ['-f', '--foo=a', '--foo', 'b'];
const options = {
foo: {
type: 'string',
multiple: true,
},
};
const args = ['--foo=a', '--foo', 'b'];
const { values, positionals } = parseArgs({ args, options });
// values = { f: true, foo: [ 'a', 'b' ] }
// values = { foo: [ 'a', 'b' ] }
// positionals = []
```
```js
const { parseArgs } = require('@pkgjs/parseargs');
// shorts
const args = ['-f', 'b'];
const options = {
foo: {
short: 'f',
type: 'boolean'
},
};
const args = ['-f', 'b'];
const { values, positionals } = parseArgs({ args, options });
// values = { foo: true }
// positionals = ['b']
```
```js
const { parseArgs } = require('@pkgjs/parseargs');
// unconfigured
const options = {};
const args = ['-f', '--foo=a', '--bar', 'b'];
const { values, positionals } = parseArgs({ strict: false, args, options });
// values = { f: true, foo: 'a', bar: true }
// positionals = ['b']
```
### F.A.Qs
- Is `cmd --foo=bar baz` the same as `cmd baz --foo=bar`?
Expand Down
18 changes: 17 additions & 1 deletion errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,25 @@ class ERR_INVALID_ARG_VALUE extends TypeError {
}
}

class ERR_PARSE_ARGS_INVALID_OPTION_VALUE extends Error {
constructor(message) {
super(message);
this.code = 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE';
}
}

class ERR_PARSE_ARGS_UNKNOWN_OPTION extends Error {
constructor(option) {
super(`Unknown option '${option}'`);
this.code = 'ERR_PARSE_ARGS_UNKNOWN_OPTION';
}
}

module.exports = {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE
ERR_INVALID_ARG_VALUE,
ERR_PARSE_ARGS_INVALID_OPTION_VALUE,
ERR_PARSE_ARGS_UNKNOWN_OPTION,
}
};
59 changes: 51 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const {
const {
codes: {
ERR_INVALID_ARG_VALUE,
ERR_PARSE_ARGS_INVALID_OPTION_VALUE,
ERR_PARSE_ARGS_UNKNOWN_OPTION,
},
} = require('./errors');

Expand Down Expand Up @@ -73,16 +75,41 @@ function getMainArgs() {
}

const protoKey = '__proto__';
function storeOptionValue(options, longOption, value, result) {
const optionConfig = options[longOption] || {};

function storeOption({
strict,
options,
result,
longOption,
shortOption,
optionValue,
}) {
const hasOptionConfig = ObjectHasOwn(options, longOption);
const optionConfig = hasOptionConfig ? options[longOption] : {};

if (strict) {
if (!hasOptionConfig) {
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(shortOption == null ? `--${longOption}` : `-${shortOption}`);
}

const shortOptionErr = ObjectHasOwn(optionConfig, 'short') ? `-${optionConfig.short}, ` : '';

if (options[longOption].type === 'string' && optionValue == null) {
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortOptionErr}--${longOption} <value>' argument missing`);
}

if (options[longOption].type === 'boolean' && optionValue != null) {
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortOptionErr}--${longOption}' does not take an argument`);
}
}

if (longOption === protoKey) {
return;
}

// Values
const usedAsFlag = value === undefined;
const newValue = usedAsFlag ? true : value;
const usedAsFlag = optionValue === undefined;
const newValue = usedAsFlag ? true : optionValue;
if (optionConfig.multiple) {
// Always store value in array, including for flags.
// result.values[longOption] starts out not present,
Expand All @@ -99,9 +126,11 @@ function storeOptionValue(options, longOption, value, result) {

const parseArgs = ({
args = getMainArgs(),
strict = true,
options = {}
} = {}) => {
validateArray(args, 'args');
validateBoolean(strict, 'strict');
validateObject(options, 'options');
ArrayPrototypeForEach(
ObjectEntries(options),
Expand Down Expand Up @@ -158,7 +187,14 @@ const parseArgs = ({
// e.g. '-f', 'bar'
optionValue = ArrayPrototypeShift(remainingArgs);
}
storeOptionValue(options, longOption, optionValue, result);
storeOption({
strict,
options,
result,
longOption,
shortOption,
optionValue,
});
continue;
}

Expand Down Expand Up @@ -188,7 +224,14 @@ const parseArgs = ({
const shortOption = StringPrototypeCharAt(arg, 1);
const longOption = findLongOptionForShort(shortOption, options);
const optionValue = StringPrototypeSlice(arg, 2);
storeOptionValue(options, longOption, optionValue, result);
storeOption({
strict,
options,
result,
longOption,
shortOption,
optionValue,
});
continue;
}

Expand All @@ -200,7 +243,7 @@ const parseArgs = ({
// e.g. '--foo', 'bar'
optionValue = ArrayPrototypeShift(remainingArgs);
}
storeOptionValue(options, longOption, optionValue, result);
storeOption({ strict, options, result, longOption, optionValue });
continue;
}

Expand All @@ -209,7 +252,7 @@ const parseArgs = ({
const index = StringPrototypeIndexOf(arg, '=');
const longOption = StringPrototypeSlice(arg, 2, index);
const optionValue = StringPrototypeSlice(arg, index + 1);
storeOptionValue(options, longOption, optionValue, result);
storeOption({ strict, options, result, longOption, optionValue });
continue;
}

Expand Down
Loading

0 comments on commit 8267d02

Please sign in to comment.