diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7597855 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/index.js b/index.js index 78cafa8..4610b04 100644 --- a/index.js +++ b/index.js @@ -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); } } diff --git a/test/bool.js b/test/bool.js index 930ccc6..34ec95a 100644 --- a/test/bool.js +++ b/test/bool.js @@ -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); diff --git a/test/num.js b/test/num.js index 074393e..c14a3e6 100644 --- a/test/num.js +++ b/test/num.js @@ -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(); +}); diff --git a/test/strict.js b/test/strict.js new file mode 100644 index 0000000..852d275 --- /dev/null +++ b/test/strict.js @@ -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(); +});