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!: switch type:string option arguments to greedy, but with error for suspect cases in strict mode #88

Merged
merged 24 commits into from
Apr 19, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a28864d
First cut at greedy with error for suspect value
shadowspawn Apr 3, 2022
831e6da
Fix checkSafeOptionValue
shadowspawn Apr 3, 2022
de21b0f
Add some unit tests for new strict error for suspect option value
shadowspawn Apr 3, 2022
2afaebc
Static example usage
shadowspawn Apr 3, 2022
5fe37b7
Tweak parameter names in response to feedback
shadowspawn Apr 9, 2022
f9ff015
Merge branch 'main' into feature/greedy
shadowspawn Apr 10, 2022
05967d4
Merge branch 'main' into feature/greedy
shadowspawn Apr 15, 2022
a7556dd
Rename routine, use custom error
shadowspawn Apr 15, 2022
da9fbcf
Add short example, and use function style matching other PR in flight.
shadowspawn Apr 15, 2022
6439e99
Refactor tests and fix for new short message
shadowspawn Apr 15, 2022
ff03bcd
Switch to node compatible test format
shadowspawn Apr 15, 2022
bc77eb2
Add end-to-end greedy tests
shadowspawn Apr 15, 2022
3b7ac89
Merge branch 'main' into feature/greedy
shadowspawn Apr 15, 2022
8487167
Move checking routines together
shadowspawn Apr 15, 2022
65c0833
Merge branch 'main' into feature/greedy
shadowspawn Apr 16, 2022
120d95a
Merge remote-tracking branch 'upstream/main' into feature/greedy
shadowspawn Apr 17, 2022
62726b8
Tweak error
shadowspawn Apr 17, 2022
4362750
Go with ambiguous, matches having two informative messages.
shadowspawn Apr 17, 2022
0eac39d
Merge branch 'main' into feature/greedy
shadowspawn Apr 17, 2022
f4ceff3
Fix more tests for null prototype
shadowspawn Apr 17, 2022
22745ac
Merge branch 'main' into feature/greedy
bcoe Apr 18, 2022
f731241
Merge remote-tracking branch 'upstream/main' into feature/greedy
shadowspawn Apr 18, 2022
e477243
Merge branch 'feature/greedy' of github.com:shadowspawn/parseargs int…
shadowspawn Apr 18, 2022
836bd9a
Move greedy related tests into index.js
shadowspawn Apr 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const {
isLoneShortOption,
isLongOptionAndValue,
isOptionValue,
isSafeOptionValue,
isShortOptionAndValue,
isShortOptionGroup
} = require('./utils');
Expand Down Expand Up @@ -102,6 +103,7 @@ function storeOptionValue(options, longOption, value, result) {

const parseArgs = ({
args = getMainArgs(),
strict = false,
options = {}
} = {}) => {
validateArray(args, 'args');
Expand Down Expand Up @@ -129,6 +131,14 @@ const parseArgs = ({
}
);

const checkSafeOptionValue = (dashedOption, longOption, value) => {
if (strict && !isSafeOptionValue(value)) {
const errorMessage = `Did you forget to specify the option argument for '${dashedOption}'?
To specify an option argument starting with a dash use '--${longOption}=-EXAMPLE'.`;
throw new Error(errorMessage);
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
}
};

const result = {
flags: {},
values: {},
Expand Down Expand Up @@ -159,6 +169,7 @@ const parseArgs = ({
if (options[longOption]?.type === 'string' && isOptionValue(nextArg)) {
// e.g. '-f', 'bar'
optionValue = ArrayPrototypeShift(remainingArgs);
checkSafeOptionValue(arg, longOption, optionValue);
}
storeOptionValue(options, longOption, optionValue, result);
continue;
Expand Down Expand Up @@ -202,6 +213,7 @@ const parseArgs = ({
if (options[longOption]?.type === 'string' && isOptionValue(nextArg)) {
// e.g. '--foo', 'bar'
optionValue = ArrayPrototypeShift(remainingArgs);
checkSafeOptionValue(arg, longOption, optionValue);
}
storeOptionValue(options, longOption, optionValue, result);
continue;
Expand Down
35 changes: 26 additions & 9 deletions test/is-option-value.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
const test = require('tape');
const { isOptionValue } = require('../utils.js');

// Options are greedy so simple behaviour, but run through the interesting possibilities.

test('isOptionValue: when passed plain text then returns true', (t) => {
t.true(isOptionValue('abc'));
t.end();
Expand All @@ -25,28 +27,43 @@ test('isOptionValue: when passed dash then returns true', (t) => {
t.end();
});

// Supporting undefined so can pass element off end of array without checking
test('isOptionValue: when passed -- then returns true', (t) => {
t.true(isOptionValue('--'));
t.end();
});

// Checking undefined so can pass element off end of array.
test('isOptionValue: when passed undefined then returns false', (t) => {
t.false(isOptionValue(undefined));
t.end();
});

test('isOptionValue: when passed short option then returns false', (t) => {
t.false(isOptionValue('-a'));
test('isOptionValue: when passed short option then returns true', (t) => {
t.true(isOptionValue('-a'));
t.end();
});

test('isOptionValue: when passed short option digit then returns true', (t) => {
t.true(isOptionValue('-1'));
t.end();
});

test('isOptionValue: when passed negative number then returns true', (t) => {
t.true(isOptionValue('-123'));
t.end();
});

test('isOptionValue: when passed short option group of short option with value then returns false', (t) => {
t.false(isOptionValue('-abd'));
test('isOptionValue: when passed short option group of short option with value then returns true', (t) => {
t.true(isOptionValue('-abd'));
t.end();
});

test('isOptionValue: when passed long option then returns false', (t) => {
t.false(isOptionValue('--foo'));
test('isOptionValue: when passed long option then returns true', (t) => {
t.true(isOptionValue('--foo'));
t.end();
});

test('isOptionValue: when passed long option with value then returns false', (t) => {
t.false(isOptionValue('--foo=bar'));
test('isOptionValue: when passed long option with value then returns true', (t) => {
t.true(isOptionValue('--foo=bar'));
t.end();
});
69 changes: 69 additions & 0 deletions test/is-safe-option-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use strict';
/* eslint max-len: 0 */

const test = require('tape');
const { isSafeOptionValue } = require('../utils.js');

// Basically rejecting values starting with a dash, but run through the interesting possibilities.

test('isSafeOptionValue: when passed plain text then returns true', (t) => {
t.true(isSafeOptionValue('abc'));
t.end();
});

test('isSafeOptionValue: when passed digits then returns true', (t) => {
t.true(isSafeOptionValue(123));
t.end();
});

test('isSafeOptionValue: when passed empty string then returns true', (t) => {
t.true(isSafeOptionValue(''));
t.end();
});

// Special case, used as stdin/stdout et al and not reason to reject
test('isSafeOptionValue: when passed dash then returns true', (t) => {
t.true(isSafeOptionValue('-'));
t.end();
});

test('isSafeOptionValue: when passed -- then returns false', (t) => {
t.false(isSafeOptionValue('--'));
t.end();
});

// Supporting undefined so can pass element off end of array without checking
test('isSafeOptionValue: when passed undefined then returns false', (t) => {
t.false(isSafeOptionValue(undefined));
t.end();
});

test('isSafeOptionValue: when passed short option then returns false', (t) => {
t.false(isSafeOptionValue('-a'));
t.end();
});

test('isSafeOptionValue: when passed short option digit then returns false', (t) => {
t.false(isSafeOptionValue('-1'));
t.end();
});

test('isSafeOptionValue: when passed negative number then returns false', (t) => {
t.false(isSafeOptionValue('-123'));
t.end();
});

test('isSafeOptionValue: when passed short option group of short option with value then returns false', (t) => {
t.false(isSafeOptionValue('-abd'));
t.end();
});

test('isSafeOptionValue: when passed long option then returns false', (t) => {
t.false(isSafeOptionValue('--foo'));
t.end();
});

test('isSafeOptionValue: when passed long option with value then returns false', (t) => {
t.false(isSafeOptionValue('--foo=bar'));
t.end();
});
95 changes: 95 additions & 0 deletions test/strict-value-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use strict';
/* eslint max-len: 0 */

const test = require('tape');
const { parseArgs } = require('../index.js');

test('strict: when candidate option value is plain text then does not throw', (t) => {
const passedArgs = ['--with', 'abc'];
const knownOptions = { with: { type: 'string' } };

t.doesNotThrow(() => {
parseArgs({ args: passedArgs, options: knownOptions, strict: true });
});
t.end();
});

test("strict: when candidate option value is '-' then does not throw", (t) => {
const passedArgs = ['--with', '-'];
const knownOptions = { with: { type: 'string' } };

t.doesNotThrow(() => {
parseArgs({ args: passedArgs, options: knownOptions, strict: true });
});
t.end();
});

test("strict: when candidate option value is '--' then throws", (t) => {
const passedArgs = ['--with', '--'];
const knownOptions = { with: { type: 'string' } };

t.throws(() => {
parseArgs({ args: passedArgs, options: knownOptions, strict: true });
});
t.end();
});

test('strict: when candidate option value is short option then throws', (t) => {
const passedArgs = ['--with', '-a'];
const knownOptions = { with: { type: 'string' } };

t.throws(() => {
parseArgs({ args: passedArgs, options: knownOptions, strict: true });
});
t.end();
});

test('strict: when candidate option value is short option digit then throws', (t) => {
const passedArgs = ['--with', '-1'];
const knownOptions = { with: { type: 'string' } };

t.throws(() => {
parseArgs({ args: passedArgs, options: knownOptions, strict: true });
});
t.end();
});

test('strict: when candidate option value is long option then throws', (t) => {
const passedArgs = ['--with', '--foo'];
const knownOptions = { with: { type: 'string' } };

t.throws(() => {
parseArgs({ args: passedArgs, options: knownOptions, strict: true });
});
t.end();
});

test('strict: when short option and suspect value then throws with short option in error message', (t) => {
const passedArgs = ['-w', '--foo'];
const knownOptions = { with: { type: 'string', short: 'w' } };

t.throws(() => {
parseArgs({ args: passedArgs, options: knownOptions, strict: true });
}, /for '-w'/);
t.end();
});

test('strict: when long option and suspect value then throws with long option in error message', (t) => {
const passedArgs = ['--with', '--foo'];
const knownOptions = { with: { type: 'string' } };

t.throws(() => {
parseArgs({ args: passedArgs, options: knownOptions, strict: true });
}, /for '--with'/);
t.end();
});

test('strict: when long option and suspect value then throws with whole expected message', (t) => {
const passedArgs = ['--with', '--foo'];
const knownOptions = { with: { type: 'string' } };

t.throws(() => {
parseArgs({ args: passedArgs, options: knownOptions, strict: true });
}, /Did you forget to specify the option argument for '--with'\?\nTo specify an option argument starting with a dash use '--with=-EXAMPLE'\./);
t.end();
});
28 changes: 18 additions & 10 deletions utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,31 @@ const {

/**
* Determines if the argument may be used as an option value.
* NB: We are choosing not to accept option-ish arguments.
* @example
* isOptionValue('V']) // returns true
* isOptionValue('-v') // returns false
* isOptionValue('--foo') // returns false
* isOptionValue('V') // returns true
* isOptionValue('-v') // returns true (greedy)
* isOptionValue('--foo') // returns true (greedy)
* isOptionValue(undefined) // returns false
*/
function isOptionValue(value) {
if (value == null) return false;
if (value === '-') return true; // e.g. representing stdin/stdout for file

// Open Group Utility Conventions are that an option-argument
// is the argument after the option, and may start with a dash.
// However, we are currently rejecting these and prioritising the
// option-like appearance of the argument. Rejection allows more error
// detection for strict:true, but comes at the cost of rejecting intended
// values starting with a dash, especially negative numbers.
return !StringPrototypeStartsWith(value, '-');
return true; // greedy!
}

/**
* Detect whether there is possible confusion and user may have omitted
* the option argument, like `--port --verbose` when `port` of type:string.
* In strict mode we throw errors if value is not safe.
*/
function isSafeOptionValue(value) {
if (value == null) return false;
if (value === '-') return true;
if (value === '--') return false;

return StringPrototypeCharAt(value, 0) !== '-';
}

/**
Expand Down Expand Up @@ -155,6 +162,7 @@ module.exports = {
isLoneShortOption,
isLongOptionAndValue,
isOptionValue,
isSafeOptionValue,
isShortOptionAndValue,
isShortOptionGroup
};