Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add parsed meta-data to returned properties #129

Merged
merged 69 commits into from
Jul 20, 2022
Merged
Changes from 15 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
52c3a2e
Proof of concept
shadowspawn May 14, 2022
5ec71b1
Return originalArgs and indices
shadowspawn May 14, 2022
f711187
Change short in AST to boolean
shadowspawn May 24, 2022
cce90bb
Store inlineValue rather than valueIndex
shadowspawn May 24, 2022
e4bc5fe
Rename symbol property in AST to kind
shadowspawn May 24, 2022
87624a1
Rename returned ast property to parseElements
shadowspawn May 24, 2022
2925639
Refactor to use parseElements to test for errors and store values
shadowspawn May 29, 2022
412287e
Merge remote-tracking branch 'upstream/main' into feature/ast
shadowspawn May 29, 2022
1897dac
Build positionals from parseElements
shadowspawn May 29, 2022
45d12e7
Replace .push with primordial
shadowspawn May 29, 2022
1d19fb2
forEach replaced with primordial
shadowspawn May 29, 2022
9d1267d
Swap isShort for optionUsed
shadowspawn May 30, 2022
c9c4d4c
Rework checkOptionLikeValue
shadowspawn May 30, 2022
ed4168d
Consistent property order and renames
shadowspawn May 30, 2022
16a9f51
Comment out new returned property until naming and tests ready
shadowspawn May 30, 2022
915e6c3
Refactor tokenize into own function
shadowspawn May 31, 2022
641197e
First test pass, get tests working by ignoring tokens in results
shadowspawn May 31, 2022
18470bd
Less magical check now checking kind
shadowspawn May 31, 2022
38d1d6c
Add some token tests
shadowspawn May 31, 2022
cfbd436
Fix index after space-separated option value
shadowspawn May 31, 2022
397dcf1
Make tokens opt-in
shadowspawn Jun 1, 2022
938bdb0
Simplify more tests with opt-in details
shadowspawn Jun 1, 2022
c5da345
Rename token.optionName to name, and restore longOption/shortOption …
shadowspawn Jun 1, 2022
4e1ec2d
Add example
shadowspawn Jun 1, 2022
bd3f574
Add side-note
shadowspawn Jun 2, 2022
b703ee8
Add example of #52, limit long syntax
shadowspawn Jun 2, 2022
82339f9
Add ordered example
shadowspawn Jun 2, 2022
fab1dc4
Add example for no repeated options
shadowspawn Jun 2, 2022
99165c9
Comment wording
shadowspawn Jun 2, 2022
9bd039d
Switch from details to tokens for configuration
shadowspawn Jun 3, 2022
6a3f637
Simplify example by removing "library" support
shadowspawn Jun 3, 2022
4cfbc29
Remove comments on how well-advised the examples goals are
shadowspawn Jun 3, 2022
9a9a740
Switch from optionUser to rawName
shadowspawn Jun 3, 2022
7cd685c
Merge remote-tracking branch 'upstream/main' into feature/ast
shadowspawn Jun 3, 2022
6f93632
Use modern syntax
shadowspawn Jun 4, 2022
642d8c0
Validate new input property
shadowspawn Jun 4, 2022
e70609a
Move strict check outside check routines
shadowspawn Jun 4, 2022
fdaa553
Tidy object setup
shadowspawn Jun 4, 2022
c293307
Simplify test
shadowspawn Jun 4, 2022
041459d
Indentation and match filename
shadowspawn Jun 4, 2022
7aafaf6
Rework with strict:true
shadowspawn Jun 4, 2022
1b6e585
Longer but simpler
shadowspawn Jun 4, 2022
20eacb0
Add textual description, and some fixes
shadowspawn Jun 4, 2022
81239d6
Add example output for tokens
shadowspawn Jun 4, 2022
84dde8e
Add example used in new documentation
shadowspawn Jun 4, 2022
40a6201
Refactor documentation
shadowspawn Jun 4, 2022
07fbb91
Update .editorconfig from Node.js
shadowspawn Jun 4, 2022
35c16f1
rework token property descriptions
shadowspawn Jun 4, 2022
038480a
Minor refactor of remaining arg processing
shadowspawn Jun 7, 2022
d4f96cc
Replace switch with if/else
shadowspawn Jun 14, 2022
122727e
Remove side-affect, per feedback
shadowspawn Jun 15, 2022
5b3518d
Add tricky case of short option group to token expansion
shadowspawn Jun 15, 2022
c8c2f84
Rework description of token.index after token example.
shadowspawn Jun 15, 2022
2b119b0
Be less clever with script naming in example
shadowspawn Jun 15, 2022
1b3c076
Remove superfluous colons in docs
shadowspawn Jun 17, 2022
e9e30a7
Upstream lint
shadowspawn Jun 17, 2022
46ab295
Merge branch 'main' into feature/ast
shadowspawn Jun 23, 2022
01baee5
Merge branch 'main' into feature/ast
shadowspawn Jun 24, 2022
46903af
Improve description
shadowspawn Jul 4, 2022
9b74c05
Merge branch 'feature/ast' of github.com:shadowspawn/parseargs into f…
shadowspawn Jul 4, 2022
89e4532
Use negate as example for tokens. Rework description a little.
shadowspawn Jul 4, 2022
3914949
Lint and feedback
shadowspawn Jul 4, 2022
d8126d1
Expand small token tests and remove uber tests
shadowspawn Jul 5, 2022
87bab7b
Use kEmptyObject per upstream suggestion. Move node lookalikes to int…
shadowspawn Jul 5, 2022
02ea585
Reworks tokens documentation for config
shadowspawn Jul 5, 2022
56fe4ca
Add version changes to YAML.
shadowspawn Jul 5, 2022
722f05e
Update README with upstream changes
shadowspawn Jul 5, 2022
ae9eca4
Update negate example calls to match documentation
shadowspawn Jul 16, 2022
f9dcc4f
Remove extra examples
shadowspawn Jul 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
187 changes: 105 additions & 82 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const {
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeShift,
ArrayPrototypeSlice,
ArrayPrototypeUnshiftApply,
Expand All @@ -13,6 +12,7 @@ const {
StringPrototypeCharAt,
StringPrototypeIndexOf,
StringPrototypeSlice,
StringPrototypeStartsWith,
} = require('./primordials');

const {
Expand Down Expand Up @@ -64,19 +64,18 @@ function getMainArgs() {
/**
* In strict mode, throw for possible usage errors like --foo --bar
*
* @param {string} longOption - long option name e.g. 'foo'
* @param {string|undefined} optionValue - value from user args
* @param {string} shortOrLong - option used, with dashes e.g. `-l` or `--long`
* @param {boolean} strict - show errors, from parseArgs({ strict })
* @param {object} config - from config passed to parseArgs
* @param {object} element- array item from parseElements returned by parseArgs
*/
function checkOptionLikeValue(longOption, optionValue, shortOrLong, strict) {
if (strict && isOptionLikeValue(optionValue)) {
function checkOptionLikeValue(config, element) {
if (config.strict && (element.kind === 'option') &&
(element.inlineValue === false) && isOptionLikeValue(element.value)) {
// Only show short example if user used short option.
const example = (shortOrLong.length === 2) ?
`'--${longOption}=-XYZ' or '${shortOrLong}-XYZ'` :
`'--${longOption}=-XYZ'`;
const errorMessage = `Option '${shortOrLong}' argument is ambiguous.
Did you forget to specify the option argument for '${shortOrLong}'?
const example = StringPrototypeStartsWith(element.optionUsed, '--') ?
`'${element.optionUsed}=-XYZ'` :
`'--${element.optionName}=-XYZ' or '${element.optionUsed}-XYZ'`;
const errorMessage = `Option '${element.optionUsed}' argument is ambiguous.
Did you forget to specify the option argument for '${element.optionUsed}'?
To specify an option argument starting with a dash use ${example}.`;
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(errorMessage);
}
Expand All @@ -85,63 +84,59 @@ To specify an option argument starting with a dash use ${example}.`;
/**
* In strict mode, throw for usage errors.
*
* @param {string} longOption - long option name e.g. 'foo'
* @param {string|undefined} optionValue - value from user args
* @param {object} options - option configs, from parseArgs({ options })
* @param {string} shortOrLong - option used, with dashes e.g. `-l` or `--long`
* @param {boolean} strict - show errors, from parseArgs({ strict })
* @param {boolean} allowPositionals - from parseArgs({ allowPositionals })
* @param {object} config - from config passed to parseArgs
* @param {object} element- array item from parseElements returned by parseArgs
*/
function checkOptionUsage(longOption, optionValue, options,
shortOrLong, strict, allowPositionals) {
// Strict and options are used from local context.
if (!strict) return;
function checkOptionUsage(config, element) {
if (!config.strict) return;

if (!ObjectHasOwn(options, longOption)) {
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(shortOrLong, allowPositionals);
if (!ObjectHasOwn(config.options, element.optionName)) {
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
element.optionUsed, config.allowPositionals);
}

const short = optionsGetOwn(options, longOption, 'short');
const shortAndLong = short ? `-${short}, --${longOption}` : `--${longOption}`;
const type = optionsGetOwn(options, longOption, 'type');
if (type === 'string' && typeof optionValue !== 'string') {
const short = optionsGetOwn(config.options, element.optionName, 'short');
const shortAndLong = `${short ? `-${short}, ` : ''}--${element.optionName}`;
const type = optionsGetOwn(config.options, element.optionName, 'type');
if (type === 'string' && typeof element.value !== 'string') {
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} <value>' argument missing`);
}
// (Idiomatic test for undefined||null, expecting undefined.)
if (type === 'boolean' && optionValue != null) {
if (type === 'boolean' && element.value != null) {
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong}' does not take an argument`);
}
}


/**
* Store the option value in `values`.
*
* @param {string} longOption - long option name e.g. 'foo'
* @param {string} optionName - long option name e.g. 'foo'
* @param {string|undefined} optionValue - value from user args
* @param {object} options - option configs, from parseArgs({ options })
* @param {object} values - option values returned in `values` by parseArgs
*/
function storeOption(longOption, optionValue, options, values) {
if (longOption === '__proto__') {
function storeOption(optionName, optionValue, options, values) {
if (optionName === '__proto__') {
return; // No. Just no.
}

// We store based on the option value rather than option type,
// preserving the users intent for author to deal with.
const newValue = optionValue ?? true;
if (optionsGetOwn(options, longOption, 'multiple')) {
if (optionsGetOwn(options, optionName, 'multiple')) {
// Always store value in array, including for boolean.
// values[longOption] starts out not present,
// values[optionName] starts out not present,
// first value is added as new array [newValue],
// subsequent values are pushed to existing array.
// (note: values has null prototype, so simpler usage)
if (values[longOption]) {
ArrayPrototypePush(values[longOption], newValue);
if (values[optionName]) {
ArrayPrototypePush(values[optionName], newValue);
} else {
values[longOption] = [newValue];
values[optionName] = [newValue];
}
} else {
values[longOption] = newValue;
values[optionName] = newValue;
}
}

Expand All @@ -150,6 +145,7 @@ const parseArgs = (config = { __proto__: null }) => {
const strict = objectGetOwn(config, 'strict') ?? true;
const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict;
const options = objectGetOwn(config, 'options') ?? { __proto__: null };
const parseConfig = { args, strict, options, allowPositionals };

// Validate input configuration.
validateArray(args, 'args');
Expand Down Expand Up @@ -182,45 +178,47 @@ const parseArgs = (config = { __proto__: null }) => {
}
);

const result = {
values: { __proto__: null },
positionals: []
};
const elements = [];
let index = -1;
let groupCount = 0;

const remainingArgs = ArrayPrototypeSlice(args);
while (remainingArgs.length > 0) {
const arg = ArrayPrototypeShift(remainingArgs);
const nextArg = remainingArgs[0];
if (groupCount > 0)
groupCount--;
else
index++;

// Check if `arg` is an options terminator.
// Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html
if (arg === '--') {
if (!allowPositionals && remainingArgs.length > 0) {
throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(nextArg);
}

// Everything after a bare '--' is considered a positional argument.
ArrayPrototypePushApply(
result.positionals,
remainingArgs
);
ArrayPrototypePush(elements, { kind: 'option-terminator', index });
ArrayPrototypeForEach(remainingArgs, (arg) =>
ArrayPrototypePush(
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
elements,
{ kind: 'positional', index: ++index, value: arg }));
break; // Finished processing args, leave while loop.
}

if (isLoneShortOption(arg)) {
// e.g. '-f'
const shortOption = StringPrototypeCharAt(arg, 1);
const longOption = findLongOptionForShort(shortOption, options);
let optionValue;
if (optionsGetOwn(options, longOption, 'type') === 'string' &&
const optionName = findLongOptionForShort(shortOption, options);
let value;
let inlineValue;
if (optionsGetOwn(options, optionName, 'type') === 'string' &&
isOptionValue(nextArg)) {
// e.g. '-f', 'bar'
optionValue = ArrayPrototypeShift(remainingArgs);
checkOptionLikeValue(longOption, optionValue, arg, strict);
value = ArrayPrototypeShift(remainingArgs);
inlineValue = false;
}
checkOptionUsage(longOption, optionValue, options,
arg, strict, allowPositionals);
storeOption(longOption, optionValue, options, result.values);
ArrayPrototypePush(
elements,
{ kind: 'option', optionName, optionUsed: arg,
index, value, inlineValue });
continue;
}

Expand All @@ -229,8 +227,8 @@ const parseArgs = (config = { __proto__: null }) => {
const expanded = [];
for (let index = 1; index < arg.length; index++) {
const shortOption = StringPrototypeCharAt(arg, index);
const longOption = findLongOptionForShort(shortOption, options);
if (optionsGetOwn(options, longOption, 'type') !== 'string' ||
const optionName = findLongOptionForShort(shortOption, options);
if (optionsGetOwn(options, optionName, 'type') !== 'string' ||
index === arg.length - 1) {
// Boolean option, or last short in group. Well formed.
ArrayPrototypePush(expanded, `-${shortOption}`);
Expand All @@ -242,53 +240,78 @@ const parseArgs = (config = { __proto__: null }) => {
}
}
ArrayPrototypeUnshiftApply(remainingArgs, expanded);
groupCount = expanded.length;
continue;
}

if (isShortOptionAndValue(arg, options)) {
// e.g. -fFILE
const shortOption = StringPrototypeCharAt(arg, 1);
const longOption = findLongOptionForShort(shortOption, options);
const optionValue = StringPrototypeSlice(arg, 2);
checkOptionUsage(longOption, optionValue, options, `-${shortOption}`, strict, allowPositionals);
storeOption(longOption, optionValue, options, result.values);
const optionName = findLongOptionForShort(shortOption, options);
const value = StringPrototypeSlice(arg, 2);
ArrayPrototypePush(
elements,
{ kind: 'option', optionName, optionUsed: `-${shortOption}`,
index, value, inlineValue: true });
continue;
}

if (isLoneLongOption(arg)) {
// e.g. '--foo'
const longOption = StringPrototypeSlice(arg, 2);
let optionValue;
if (optionsGetOwn(options, longOption, 'type') === 'string' &&
const optionName = StringPrototypeSlice(arg, 2);
let value;
let inlineValue;
if (optionsGetOwn(options, optionName, 'type') === 'string' &&
isOptionValue(nextArg)) {
// e.g. '--foo', 'bar'
optionValue = ArrayPrototypeShift(remainingArgs);
checkOptionLikeValue(longOption, optionValue, arg, strict);
value = ArrayPrototypeShift(remainingArgs);
inlineValue = false;
}
checkOptionUsage(longOption, optionValue, options,
arg, strict, allowPositionals);
storeOption(longOption, optionValue, options, result.values);
ArrayPrototypePush(
elements,
{ kind: 'option', optionName, optionUsed: arg,
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
index, value, inlineValue });
continue;
}

if (isLongOptionAndValue(arg)) {
// e.g. --foo=bar
const index = StringPrototypeIndexOf(arg, '=');
const longOption = StringPrototypeSlice(arg, 2, index);
const optionValue = StringPrototypeSlice(arg, index + 1);
checkOptionUsage(longOption, optionValue, options, `--${longOption}`, strict, allowPositionals);
storeOption(longOption, optionValue, options, result.values);
const optionName = StringPrototypeSlice(arg, 2, index);
const value = StringPrototypeSlice(arg, index + 1);
ArrayPrototypePush(
elements,
{ kind: 'option', optionName, optionUsed: `--${optionName}`,
index, value, inlineValue: true });
continue;
}

// Anything left is a positional
if (!allowPositionals) {
throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(arg);
}

ArrayPrototypePush(result.positionals, arg);
ArrayPrototypePush(elements, { kind: 'positional', index, value: arg });
}

const result = {
values: { __proto__: null },
positionals: [],
// tokens: elements,
};
ArrayPrototypeForEach(elements, (element) => {
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
switch (element.kind) {
case 'option-terminator':
break;
case 'positional':
if (!allowPositionals) {
throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(element.value);
}
ArrayPrototypePush(result.positionals, element.value);
break;
case 'option':
checkOptionUsage(parseConfig, element);
checkOptionLikeValue(parseConfig, element);
storeOption(element.optionName, element.value, options, result.values);
break;
}
});

return result;
};

Expand Down