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

Add support for strict mode #41

Closed
wants to merge 15 commits into from
Closed
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# EditorConfig is awesome:
http://EditorConfig.org

# top-most EditorConfig file
root = true

[*.js]
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
87 changes: 81 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
@@ -25,6 +25,8 @@ module.exports = function (args, opts) {

var flags = {
bools: {},
// known: {},
numbers: {},
strings: {},
unknownFn: null,
};
@@ -64,6 +66,7 @@ module.exports = function (args, opts) {
});
});

// populating flags.strings with explicit keys and aliases
[].concat(opts.string).filter(Boolean).forEach(function (key) {
flags.strings[key] = true;
if (aliases[key]) {
@@ -73,15 +76,36 @@ module.exports = function (args, opts) {
}
});

// populating flags.numbers with explicit keys and aliases
[].concat(opts.number).filter(Boolean).forEach(function (key) {
flags.numbers[key] = true;
if (aliases[key]) {
[].concat(aliases[key]).forEach(function (k) {
flags.numbers[k] = true;
});
}
});

// [].concat(opts.known).filter(Boolean).forEach(function (key) {
// flags.known[key] = true;
// });

var defaults = opts.default || {};

var argv = { _: [] };

function argDefined(key, arg) {
return (flags.allBools && (/^--[^=]+$/).test(arg))
|| flags.strings[key]
function keyDefined(key) {
return flags.strings[key]
|| flags.numbers[key]
|| flags.bools[key]
|| aliases[key];
// || flags.known[key];
}

function argDefined(key, arg) {
// legacy test for whether to call unknownFn
return (flags.allBools && (/^--[^=]+$/).test(arg))
|| keyDefined(key);
}

function setKey(obj, keys, value) {
@@ -120,14 +144,42 @@ module.exports = function (args, opts) {
}
}

function checkStrictVal(key, val) {
// Have a separate routine from setArg to avoid affecting non-strict results,
// as the strict checks need less processed values.
if (opts.strict) {
if (flags.strings[key] && val === true) {
throw new Error('Missing option value for option "' + key + '"');
}
if (flags.numbers[key] && !(isNumber(val) || val === false)) {
throw new Error('Expecting number value for option "' + key + '"');
}
if (isBooleanKey(key) && typeof val === 'string' && !(/^(true|false)$/).test(val)) {
throw new Error('Unexpected option value for option "' + key + '"');
}
if (!keyDefined(key)) {
throw new Error('Unknown option "' + key + '"');
}
}
}

function setArg(key, val, arg) {
if (arg && flags.unknownFn && !argDefined(key, arg)) {
if (flags.unknownFn(arg) === false) { return; }
}

var value = !flags.strings[key] && isNumber(val)
? Number(val)
: val;
var value = val;
if (flags.numbers[key]) {
if (isNumber(val)) {
value = Number(val);
} else if (value === false) {
value = val; // --no-foo
} else {
value = NaN;
}
} else if (!flags.strings[key] && isNumber(val)) {
value = Number(val);
}
setKey(argv, key.split('.'), value);

(aliases[key] || []).forEach(function (x) {
@@ -162,12 +214,14 @@ module.exports = function (args, opts) {
var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
key = m[1];
var value = m[2];
checkStrictVal(key, value);
if (isBooleanKey(key)) {
value = value !== 'false';
}
setArg(key, value, arg);
} else if ((/^--no-.+/).test(arg)) {
key = arg.match(/^--no-(.+)/)[1];
checkStrictVal(key, false);
setArg(key, false, arg);
} else if ((/^--.+/).test(arg)) {
key = arg.match(/^--(.+)/)[1];
@@ -178,12 +232,20 @@ module.exports = function (args, opts) {
&& !isBooleanKey(key)
&& !flags.allBools
) {
checkStrictVal(key, next);
setArg(key, next, arg);
i += 1;
} else if ((/^(true|false)$/).test(next)) {
checkStrictVal(key, next);
setArg(key, next === 'true', arg);
i += 1;
} else if (flags.numbers[key] && isNumber(next)) {
// This is a second look to pick up negative numbers.
checkStrictVal(key, next);
setArg(key, next, arg);
i += 1;
} else {
checkStrictVal(key, true);
setArg(key, flags.strings[key] ? '' : true, arg);
}
} else if ((/^-[^-]+/).test(arg)) {
@@ -194,11 +256,13 @@ module.exports = function (args, opts) {
next = arg.slice(j + 2);

if (next === '-') {
checkStrictVal(letters[j], next);
setArg(letters[j], next, arg);
continue;
}

if ((/[A-Za-z]/).test(letters[j]) && next[0] === '=') {
checkStrictVal(letters[j], next.slice(1));
setArg(letters[j], next.slice(1), arg);
broken = true;
break;
@@ -208,16 +272,19 @@ module.exports = function (args, opts) {
(/[A-Za-z]/).test(letters[j])
&& (/-?\d+(\.\d*)?(e-?\d+)?$/).test(next)
) {
checkStrictVal(letters[j], next);
setArg(letters[j], next, arg);
broken = true;
break;
}

if (letters[j + 1] && letters[j + 1].match(/\W/)) {
checkStrictVal(letters[j], arg.slice(j + 2));
setArg(letters[j], arg.slice(j + 2), arg);
broken = true;
break;
} else {
checkStrictVal(letters[j], true);
setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg);
}
}
@@ -229,12 +296,20 @@ module.exports = function (args, opts) {
&& !(/^(-|--)[^-]/).test(args[i + 1])
&& !isBooleanKey(key)
) {
checkStrictVal(key, args[i + 1]);
setArg(key, args[i + 1], arg);
i += 1;
} else if (args[i + 1] && (/^(true|false)$/).test(args[i + 1])) {
checkStrictVal(key, args[i + 1]);
setArg(key, args[i + 1] === 'true', arg);
i += 1;
} else if (flags.numbers[key] && isNumber(args[i + 1])) {
// This is a second look to pick up negative numbers.
checkStrictVal(key, args[i + 1]);
setArg(key, args[i + 1], arg);
i += 1;
} else {
checkStrictVal(key, true);
setArg(key, flags.strings[key] ? '' : true, arg);
}
}
49 changes: 49 additions & 0 deletions test/bool.js
Original file line number Diff line number Diff line change
@@ -143,6 +143,55 @@ test('boolean --boool=false', function (t) {
t.end();
});

test('boolean --boool=other', function (t) {
// legacy edge case
var parsed = parse(['--boool=other'], {
default: {
boool: false,
},
boolean: ['boool'],
});

t.same(parsed.boool, true);
t.end();
});

test('boolean -b=true', function (t) {
var parsed = parse(['-b=true'], {
default: {
b: false,
},
boolean: ['b'],
});

t.same(parsed.b, 'true'); // [sic] legacy behaviour
t.end();
});

test('boolean -b=false', function (t) {
var parsed = parse(['-b=false'], {
default: {
b: true,
},
boolean: ['b'],
});

t.same(parsed.b, 'false'); // [sic] legacy behaviour
t.end();
});

test('boolean -b=other', function (t) {
var parsed = parse(['-b=other'], {
default: {
b: false,
},
boolean: ['b'],
});

t.same(parsed.b, 'other'); // [sic] legacy behaviour
t.end();
});

test('boolean using something similar to true', function (t) {
var opts = { boolean: 'h' };
var result = parse(['-h', 'true.txt'], opts);
78 changes: 77 additions & 1 deletion test/num.js
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
var parse = require('../');
var test = require('tape');

test('nums', function (t) {
test('implicit nums', function (t) {
var argv = parse([
'-x', '1234',
'-y', '5.67',
@@ -36,3 +36,79 @@ test('already a number', function (t) {
t.deepEqual(typeof argv._[0], 'number');
t.end();
});

test('number type: short option', function (t) {
var options = { number: 'n' };
var argv = parse(['-n', '123'], options);
t.deepEqual(argv, { n: 123, _: [] });

argv = parse(['-n', '-123'], options);
t.deepEqual(argv, { n: -123, _: [] });

argv = parse(['-n=123'], options);
t.deepEqual(argv, { n: 123, _: [] });

argv = parse(['-n', 'xyz'], options);
t.deepEqual(argv, { n: NaN, _: [] });

argv = parse(['-n=xyz'], options);
t.deepEqual(argv, { n: NaN, _: [] });

// Special case of missing argument value
argv = parse(['-n'], options);
t.deepEqual(argv, { n: NaN, _: [] });

t.end();
});

test('number type: long option', function (t) {
var options = { number: 'num' };
var argv = parse(['--num', '123'], options);
t.deepEqual(argv, { num: 123, _: [] });

argv = parse(['--num', '-123'], options);
t.deepEqual(argv, { num: -123, _: [] });

argv = parse(['--num=123'], options);
t.deepEqual(argv, { num: 123, _: [] });

argv = parse(['--num', 'xyz'], options);
t.deepEqual(argv, { num: NaN, _: [] });

argv = parse(['--num=xyz'], options);
t.deepEqual(argv, { num: NaN, _: [] });

// Special case of missing argument value
argv = parse(['--num'], options);
t.deepEqual(argv, { num: NaN, _: [] });

// Special case of negated
argv = parse(['--no-num'], options);
t.deepEqual(argv, { num: false, _: [] });

t.end();
});

test('number: alias', function (t) {
var options = { number: 'num', alias: { num: 'n' } };
var argv = parse(['-n', '123'], options);
t.deepEqual(argv, { n: 123, num: 123, _: [] });

// argv = parse(['-n', '-123'], options);
// t.deepEqual(argv, { n: -123, num: 123, _: [] });

argv = parse(['-n=123'], options);
t.deepEqual(argv, { n: 123, num: 123, _: [] });

argv = parse(['-n', 'xyz'], options);
t.deepEqual(argv, { n: NaN, num: NaN, _: [] });

argv = parse(['-n=xyz'], options);
t.deepEqual(argv, { n: NaN, num: NaN, _: [] });

// Special case of missing argument value
argv = parse(['-n'], options);
t.deepEqual(argv, { n: NaN, num: NaN, _: [] });

t.end();
});
199 changes: 199 additions & 0 deletions test/strict.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
'use strict';

var parse = require('../');
var test = require('tape');

function throwsWhenStrict(args, parseOptions, testOptions) {
// does not throw by default
testOptions.t.doesNotThrow(function () {
parse(args, parseOptions);
});
// throws when strict
var strictOptions = JSON.parse(JSON.stringify(parseOptions));
strictOptions.strict = true;
testOptions.t.throws(function () {
parse(args, strictOptions);
}, testOptions.expected);
}

var kMissingString = /Missing option value/;
var kMissingNumber = /Expecting number value/;
var kBooleanWithValue = /Unexpected option value/;
var kUnknownOption = /Unknown option/;

// missing option value

test('strict missing option value: long string option used alone', function (t) {
throwsWhenStrict(['--str'], { string: ['str'] }, { t: t, expected: kMissingString });
t.end();
});

test('strict missing option value: short string option used alone', function (t) {
throwsWhenStrict(['-s'], { string: ['s'] }, { t: t, expected: kMissingString });
t.end();
});

test('strict missing option value: string option alias used alone', function (t) {
throwsWhenStrict(['-s'], { string: ['str'], alias: { str: 's' } }, { t: t, expected: kMissingString });
t.end();
});

test('strict missing option value: string option followed by option (rather than value)', function (t) {
throwsWhenStrict(['--str', '-a'], { string: ['str'] }, { t: t, expected: kMissingString });
t.end();
});

test('strict missing option value: short string option used before end of short option group', function (t) {
throwsWhenStrict(['-sb'], { string: ['s'], boolean: 'b' }, { t: t, expected: kMissingString });
t.end();
});

test('strict missing option value: empty string is ok value', function (t) {
t.doesNotThrow(function () {
parse(['--str', ''], { string: ['str'] });
});
t.end();
});

test('strict missing option value: implied empty string is ok (--str=)', function (t) {
t.doesNotThrow(function () {
parse(['--str='], { string: ['str'] });
});
t.end();
});

// missing number value

test('strict missing number value: long number option used alone', function (t) {
throwsWhenStrict(['--num'], { number: ['num'] }, { t: t, expected: kMissingNumber });
t.end();
});

test('strict missing number value: short number option used alone', function (t) {
throwsWhenStrict(['-n'], { number: ['n'] }, { t: t, expected: kMissingNumber });
t.end();
});

test('strict missing number value: number option alias used alone', function (t) {
throwsWhenStrict(['-n'], { number: ['num'], alias: { num: 'n' } }, { t: t, expected: kMissingNumber });
t.end();
});

test('strict missing number value: long number option followed by non-number', function (t) {
throwsWhenStrict(['--num', 'xyz'], { number: ['num'] }, { t: t, expected: kMissingNumber });
t.end();
});

test('strict missing number value: short number option followed by non-number', function (t) {
throwsWhenStrict(['-n', 'xyz'], { number: ['n'] }, { t: t, expected: kMissingNumber });
t.end();
});

test('strict missing number value: long number option = non-number', function (t) {
throwsWhenStrict(['--num=xyz'], { number: ['num'] }, { t: t, expected: kMissingNumber });
t.end();
});

test('strict missing number value: short number option = non-number', function (t) {
throwsWhenStrict(['-n=xyz'], { number: ['n'] }, { t: t, expected: kMissingNumber });
t.end();
});

test('strict missing number value: number option followed by option (rather than value)', function (t) {
throwsWhenStrict(['--num', '-a'], { number: ['num'] }, { t: t, expected: kMissingNumber });
t.end();
});

test('strict missing number value: short number option used before end of short option group', function (t) {
throwsWhenStrict(['-nb'], { number: ['n'], boolean: 'b' }, { t: t, expected: kMissingNumber });
t.end();
});

test('strict missing number value: number does not throw', function (t) {
t.doesNotThrow(function () {
parse(['--num', '123'], { number: ['num'] });
});
t.end();
});

// unexpected option value

test('strict unexpected option value: long boolean option given value (other than true/false)', function (t) {
throwsWhenStrict(['--bool=x'], { boolean: ['bool'] }, { t: t, expected: kBooleanWithValue });
t.end();
});

test('strict unexpected option value: long boolean option given true is ok', function (t) {
t.doesNotThrow(function () {
parse(['--bool=true'], { boolean: ['bool'] });
});
t.end();
});

test('strict unexpected option value: long boolean option given false is ok', function (t) {
t.doesNotThrow(function () {
parse(['--bool=false'], { boolean: ['bool'] });
});
t.end();
});

test('strict unexpected option value: short boolean option given value (other than true/false)', function (t) {
throwsWhenStrict(['--b=x'], { boolean: ['b'] }, { t: t, expected: kBooleanWithValue });
t.end();
});

test('strict unexpected option value: short boolean option given value', function (t) {
t.doesNotThrow(function () {
parse(['--b=true'], { boolean: ['b'] });
});
t.end();
});

test('strict unexpected option value: short boolean option given value', function (t) {
t.doesNotThrow(function () {
parse(['--b=false'], { boolean: ['b'] });
});
t.end();
});

test('strict unknown option: unknown option', function (t) {
throwsWhenStrict(['-u'], { }, { t: t, expected: kUnknownOption });
throwsWhenStrict(['--long'], { }, { t: t, expected: kUnknownOption });
throwsWhenStrict(['-u=x'], { }, { t: t, expected: kUnknownOption });
throwsWhenStrict(['--long=x'], { }, { t: t, expected: kUnknownOption });
t.end();
});

test('strict unknown option: opt.boolean is known', function (t) {
t.doesNotThrow(function () {
parse(['--bool'], { boolean: ['bool'], strict: true });
parse(['-b'], { boolean: ['b'], strict: true });
});
t.end();
});

test('strict unknown option: opt.string is known', function (t) {
t.doesNotThrow(function () {
parse(['--str', 'SSS'], { string: ['str'], strict: true });
parse(['-s', 'SSS'], { string: ['s'], strict: true });
});
t.end();
});

test('strict unknown option: opt.number is known', function (t) {
t.doesNotThrow(function () {
parse(['--num', '123'], { number: ['num'], strict: true });
parse(['-n', '123'], { number: ['n'], strict: true });
});
t.end();
});

test('strict unknown option: opt.alias is known', function (t) {
t.doesNotThrow(function () {
var options = { alias: { aaa: ['a', 'AAA'] }, strict: true };
parse(['--aaa'], options);
parse(['-a'], options);
parse(['--AAA'], options);
});
t.end();
});