diff --git a/README.md b/README.md index 6d2f07d..f7a0df8 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,10 @@ $ npm install zarg ## Usage -`zarg()` takes 1-3 arguments: +`zarg()` takes 1 or 2 arguments: 1. An array of CLI arguments (_Optional_, defaults to `process.argv.slice(2)`) 2. Options argument (see below) -3. Function to call for unknown options (_Optional_, raises a descriptive error by default) It returns an object with any values present on the command-line (missing options are thus missing from the resulting object). Zarg performs no validation/requirement checking - we @@ -35,42 +34,56 @@ in which case an empty array is returned). ```javascript const zarg = require('zarg'); -const args = zarg([argument_array,] options [, unknown_callback_fn]); +const args = zarg([argument_array,] options); ``` For example: +```console +$ node ./hello.js --port=1234 -n 'My name' foo bar --tag qux --tag=qix -- --foobar +``` + ```javascript -// node ./hello.js --port=1234 -n 'My name' foo bar +// hello.js const zarg = require('zarg'); const args = zarg({ - help: Boolean, // --help - version: [Boolean, '-v'], // --version or -v - port: Number, // --port or --port= - name: [String, '-n', '--label'] // --name , --name=, -n , - // --label , or --label= + // Types + '--help': Boolean, + '--version': Boolean, + '--port': Number, // --port or --port= + '--name': String, // --name or --name= + '--tag': [String], // --tag or --tag= + + // Aliases + '-v': '--version', + '-n': '--name', // -n ; result is stored in --name + '--label': '--name' // --label or --label=; + // result is stored in --name }); console.log(args); /* { - _: ["foo", "bar"], - port: 1234, - name: "My name" + _: ["foo", "bar", "--foobar"], + '--port': 1234, + '--name': "My name", + '--tag': ["qux", "qix"] } */ ``` -The options object defaults to having its keys as long arguments. - -The values for each key=>value pair is either a type function or an array. +The values for each key=>value pair is either a type (function or [function]) or a string (indicating an alias). - In the case of a function, the string value of the argument's value is passed to it, and the return value is used as the ultimate value. -- In the case of an array, the first element _must_ be a type function, - and any subsequent strings are used as aliases. +- In the case of an array, the only element _must_ be a type function. Array types indicate + that the argument may be passed multiple times, and as such the resulting value in the returned + object is an array with all of the values that were passed using the specified flag. + +- In the case of a string, an alias is established. If a flag is passed that matches the _key_, + then the _value_ is substituted in its place. Type functions are passed three arguments: @@ -80,20 +93,6 @@ Type functions are passed three arguments: This means the built-in `String`, `Number`, and `Boolean` type constructors "just work" as type functions. -If a parameter is present in the argument array but isn't configured in the options object, -the function supplied in the third argument (if present) is called with two arguments: - -1. The name of the option that was unknown -2. The argument value (only if the option was formatted as `--long-name=something`) - -For example, this is the default unknown handler: - -```javascript -function defaultUnknownHandler(name, /* , val */) { - throw new Error(`Unknown or unexpected option: ${name}`); -} -``` - # License Copyright © 2017 by ZEIT, Inc. Released under the [MIT License](LICENSE.md). diff --git a/index.js b/index.js index 7765049..4b71d05 100644 --- a/index.js +++ b/index.js @@ -1,51 +1,36 @@ -function defaultUnknownHandler(name, /* , val */) { - throw new Error(`Unknown or unexpected option: ${name}`); -} - -function zarg(argv, opts, unknownHandler) { - if (!Array.isArray(argv)) { - unknownHandler = opts; - opts = argv; - argv = null; - } +function zarg(argv, opts) { + const result = {_: []}; - if (typeof opts === 'function') { - unknownHandler = opts; - opts = null; + /* eslint-disable default-case */ + switch (arguments.length) { + case 0: + return result; + case 1: + opts = argv; + argv = null; + break; } + /* eslint-enable default-case */ argv = argv || process.argv.slice(2); - opts = opts || {}; - unknownHandler = unknownHandler || defaultUnknownHandler; + const aliases = {}; const handlers = {}; - const setType = (name, type, dest) => { - if (name in handlers) { - const odest = handlers[name][1]; - const extended = `--${dest}` === name ? '' : `alias for --${dest}, `; - throw new Error(`Duplicate option configuration: ${name} (${extended}originally for --${odest})`); - } - - handlers[name] = [type, dest]; - }; for (const key of Object.keys(opts)) { - const [type, aliases] = Array.isArray(opts[key]) ? [opts[key][0], opts[key].slice(1)] : [opts[key], []]; - - const name = `--${key}`; - - if (!type || typeof type !== 'function') { - throw new Error(`Type missing or not a function: ${name}`); + if (typeof opts[key] === 'string') { + aliases[key] = opts[key]; + continue; } - setType(name, type, key); + const type = opts[key]; - for (const alias of aliases) { - setType(alias, type, key); + if (!type || (typeof type !== 'function' && !(Array.isArray(type) && type.length === 1 && typeof type[0] === 'function'))) { + throw new Error(`Type missing or not a function or valid array type: ${key}`); } - } - const result = {_: []}; + handlers[key] = type; + } for (let i = 0, len = argv.length; i < len; i++) { const arg = argv[i]; @@ -61,27 +46,46 @@ function zarg(argv, opts, unknownHandler) { } if (arg[0] === '-') { - const [argName, argStr] = arg[1] === '-' ? arg.split('=', 2) : [arg, undefined]; + const [originalArgName, argStr] = arg[1] === '-' ? arg.split('=', 2) : [arg, undefined]; + + let argName = originalArgName; + while (argName in aliases) { + argName = aliases[argName]; + } if (!(argName in handlers)) { - unknownHandler(argName, argStr); - continue; + throw new Error(`Unknown or unexpected option: ${originalArgName}`); } - const [type, dest] = handlers[argName]; + /* eslint-disable operator-linebreak */ + const [type, isArray] = Array.isArray(handlers[argName]) + ? [handlers[argName][0], true] + : [handlers[argName], false]; + /* eslint-enable operator-linebreak */ + let value; if (type === Boolean) { - result[dest] = true; + value = true; } else if (argStr === undefined) { if (argv.length < i + 2 || (argv[i + 1].length > 1 && argv[i + 1][0] === '-')) { - const extended = `--${dest}` === argName ? '' : ` (alias for --${dest})`; - throw new Error(`Option requires argument: ${argName}${extended}`); + const extended = originalArgName === argName ? '' : ` (alias for ${argName})`; + throw new Error(`Option requires argument: ${originalArgName}${extended}`); } - result[dest] = type(argv[i + 1], argName, result[dest]); + value = type(argv[i + 1], argName, result[argName]); ++i; } else { - result[dest] = type(argStr, argName, result[dest]); + value = type(argStr, argName, result[argName]); + } + + if (isArray) { + if (result[argName]) { + result[argName].push(value); + } else { + result[argName] = [value]; + } + } else { + result[argName] = value; } } else { result._.push(arg); diff --git a/test.js b/test.js index 687102a..89e5977 100644 --- a/test.js +++ b/test.js @@ -5,220 +5,133 @@ const expect = require('chai').expect; const zarg = require('.'); test('basic parses arguments from process.argv', () => { - const curArgs = process.argv.slice(0); - process.argv.splice(2, 1000, '--foo', '1337', '-B', 'hello', '--mcgee'); + const curArgs = process.argv; + process.argv = ['node', 'test.js', '--foo', '1337', '-B', 'hello', '--mcgee']; try { - const args = zarg({foo: Number, bar: [String, '-B'], mcgee: Boolean}); + const args = zarg({ + '--foo': Number, + '--bar': String, + '--mcgee': Boolean, + '-B': '--bar' + }); + expect(args).to.exist; - expect(args.foo).to.equal(1337); - expect(args.bar).to.equal('hello'); - expect(args.mcgee).to.equal(true); + expect(args['--foo']).to.equal(1337); + expect(args['--bar']).to.equal('hello'); + expect(args['--mcgee']).to.equal(true); } finally { - process.argv.splice(0, 1000, ...curArgs); + process.argv = curArgs; } }); +test('zarg with no arguments', () => { + expect(zarg()).to.deep.equal({_: []}); +}); + test('basic extra arguments parsing', () => { const argv = ['hi', 'hello', 'there', '-']; - - const forms = [ - zarg(argv), - zarg(argv, {}), - zarg(argv, () => {}) - ]; - - for (const args of forms) { - expect(args).to.deep.equal({_: argv}); - } + expect(zarg(argv, {})).to.deep.equal({_: argv}); }); test('basic string parsing', () => { const argv = ['hey', '--foo', 'hi', 'hello']; - - const forms = [ - zarg(argv, {foo: String}), - zarg(argv, {foo: String}, () => {}) - ]; - - for (const args of forms) { - expect(args).to.deep.equal({_: ['hey', 'hello'], foo: 'hi'}); - } + expect(zarg(argv, {'--foo': String})).to.deep.equal({_: ['hey', 'hello'], '--foo': 'hi'}); }); test('basic string parsing (equals long-arg)', () => { const argv = ['hey', '--foo=hi', 'hello']; - - const forms = [ - zarg(argv, {foo: String}), - zarg(argv, {foo: String}, () => {}) - ]; - - for (const args of forms) { - expect(args).to.deep.equal({_: ['hey', 'hello'], foo: 'hi'}); - } + expect(zarg(argv, {'--foo': String})).to.deep.equal({_: ['hey', 'hello'], '--foo': 'hi'}); }); test('basic number parsing', () => { const argv = ['hey', '--foo', '1234', 'hello']; - - const forms = [ - zarg(argv, {foo: Number}), - zarg(argv, {foo: Number}, () => {}) - ]; - - for (const args of forms) { - expect(args).to.deep.equal({_: ['hey', 'hello'], foo: 1234}); - } + expect(zarg(argv, {'--foo': Number})).to.deep.equal({_: ['hey', 'hello'], '--foo': 1234}); }); test('basic boolean parsing', () => { const argv = ['hey', '--foo', '1234', 'hello']; - - const forms = [ - zarg(argv, {foo: Boolean}), - zarg(argv, {foo: Boolean}, () => {}) - ]; - - for (const args of forms) { - expect(args).to.deep.equal({_: ['hey', '1234', 'hello'], foo: true}); - } + expect(zarg(argv, {'--foo': Boolean})).to.deep.equal({_: ['hey', '1234', 'hello'], '--foo': true}); }); test('basic custom type parsing', () => { const argv = ['hey', '--foo', '1234', 'hello']; - const customType = (val, name) => `:${name}:${val}:`; - - const forms = [ - zarg(argv, {foo: customType}), - zarg(argv, {foo: customType}, () => {}) - ]; - - for (const args of forms) { - expect(args).to.deep.equal({_: ['hey', 'hello'], foo: ':--foo:1234:'}); - } + expect(zarg(argv, {'--foo': customType})).to.deep.equal({_: ['hey', 'hello'], '--foo': ':--foo:1234:'}); }); test('basic string parsing (array)', () => { - const argv = ['hey', '--foo', 'hi', 'hello']; - - const forms = [ - zarg(argv, {foo: [String]}), - zarg(argv, {foo: [String]}, () => {}) - ]; - - for (const args of forms) { - expect(args).to.deep.equal({_: ['hey', 'hello'], foo: 'hi'}); - } + const argv = ['hey', '--foo', 'hi', 'hello', '--foo', 'hey']; + expect(zarg(argv, {'--foo': [String]})).to.deep.equal({_: ['hey', 'hello'], '--foo': ['hi', 'hey']}); }); test('basic number parsing (array)', () => { - const argv = ['hey', '--foo', '1234', 'hello']; - - const forms = [ - zarg(argv, {foo: [Number]}), - zarg(argv, {foo: [Number]}, () => {}) - ]; - - for (const args of forms) { - expect(args).to.deep.equal({_: ['hey', 'hello'], foo: 1234}); - } + const argv = ['hey', '--foo', '1234', 'hello', '--foo', '5432']; + expect(zarg(argv, {'--foo': [Number]})).to.deep.equal({_: ['hey', 'hello'], '--foo': [1234, 5432]}); }); test('basic boolean parsing (array)', () => { - const argv = ['hey', '--foo', '1234', 'hello']; - - const forms = [ - zarg(argv, {foo: [Boolean]}), - zarg(argv, {foo: [Boolean]}, () => {}) - ]; - - for (const args of forms) { - expect(args).to.deep.equal({_: ['hey', '1234', 'hello'], foo: true}); - } + const argv = ['hey', '--foo', '1234', 'hello', '--foo', 'hallo']; + expect(zarg(argv, {'--foo': [Boolean]})).to.deep.equal({_: ['hey', '1234', 'hello', 'hallo'], '--foo': [true, true]}); }); test('basic custom type parsing (array)', () => { - const argv = ['hey', '--foo', '1234', 'hello']; - + const argv = ['hey', '--foo', '1234', 'hello', '--foo', '8911hi']; const customType = (val, name) => `:${name}:${val}:`; - - const forms = [ - zarg(argv, {foo: [customType]}), - zarg(argv, {foo: [customType]}, () => {}) - ]; - - for (const args of forms) { - expect(args).to.deep.equal({_: ['hey', 'hello'], foo: ':--foo:1234:'}); - } + expect(zarg(argv, {'--foo': [customType]})).to.deep.equal({_: ['hey', 'hello'], '--foo': [':--foo:1234:', ':--foo:8911hi:']}); }); test('basic alias parsing', () => { const argv = ['--foo', '1234', '-B', '-', 'hello', '--not-foo-or-bar', 'ohai']; const opts = { - foo: Number, - bar: [String, '-B'], - 'another-arg': [Boolean, '-a', '--not-foo-or-bar'] + '--foo': Number, + '--bar': String, + '--another-arg': Boolean, + '-a': '--another-arg', + '--not-foo-or-bar': '--another-arg', + '-B': '--bar' }; - const forms = [ - zarg(argv, opts), - zarg(argv, opts, () => {}) - ]; - - for (const args of forms) { - expect(args).to.deep.equal({ - _: ['hello', 'ohai'], - foo: 1234, - bar: '-', - 'another-arg': true - }); - } + expect(zarg(argv, opts)).to.deep.equal({ + _: ['hello', 'ohai'], + '--foo': 1234, + '--bar': '-', + '--another-arg': true + }); }); test('double-dash parsing', () => { const argv = ['--foo', '1234', 'hi', '--foo', '5678', 'there', '--', '--foo', '2468']; - expect(zarg(argv, {foo: Number})).to.deep.equal({_: ['hi', 'there', '--foo', '2468'], foo: 5678}); + expect(zarg(argv, {'--foo': Number})).to.deep.equal({_: ['hi', 'there', '--foo', '2468'], '--foo': 5678}); }); test('error: invalid option', () => { const argv = ['--foo', '1234', '--bar', '8765']; - expect(() => zarg(argv, {foo: Number})).to.throw('Unknown or unexpected option: --bar'); -}); - -test('error: invalid option (custom handler)', () => { - const argv = ['--foo', '1234', '--bar', '8765']; - expect(zarg(argv, {foo: Number}, () => null)).to.deep.equal({_: ['8765'], foo: 1234}); + expect(() => zarg(argv, {'--foo': Number})).to.throw('Unknown or unexpected option: --bar'); }); test('error: expected argument', () => { const argv = ['--foo', '--bar', '1234']; - expect(() => zarg(argv, {foo: String, bar: Number})).to.throw('Option requires argument: --foo'); + expect(() => zarg(argv, {'--foo': String, '--bar': Number})).to.throw('Option requires argument: --foo'); }); test('error: expected argument (end flag)', () => { const argv = ['--foo', '--bar']; - expect(() => zarg(argv, {foo: Boolean, bar: Number})).to.throw('Option requires argument: --bar'); + expect(() => zarg(argv, {'--foo': Boolean, '--bar': Number})).to.throw('Option requires argument: --bar'); }); test('error: expected argument (alias)', () => { const argv = ['--foo', '--bar', '1234']; - expect(() => zarg(argv, {realfoo: [String, '--foo'], bar: Number})).to.throw('Option requires argument: --foo (alias for --realfoo)'); + expect(() => zarg(argv, {'--realfoo': String, '--foo': '--realfoo', '--bar': Number})).to.throw('Option requires argument: --foo (alias for --realfoo)'); }); test('error: expected argument (end flag) (alias)', () => { const argv = ['--foo', '--bar']; - expect(() => zarg(argv, {foo: Boolean, realbar: [Number, '--bar']})).to.throw('Option requires argument: --bar (alias for --realbar)'); + expect(() => zarg(argv, {'--foo': Boolean, '--realbar': Number, '--bar': '--realbar'})).to.throw('Option requires argument: --bar (alias for --realbar)'); }); test('error: non-function type', () => { - expect(() => zarg([], {foo: 10})).to.throw('Type missing or not a function: --foo'); - expect(() => zarg([], {foo: null})).to.throw('Type missing or not a function: --foo'); - expect(() => zarg([], {foo: undefined})).to.throw('Type missing or not a function: --foo'); -}); - -test('error: duplicate handler', () => { - expect(() => zarg([], {foo: [String, '--foo']})).to.throw('Duplicate option configuration: --foo (originally for --foo)'); - expect(() => zarg([], {foo: Number, bar: [String, '--foo']})).to.throw('Duplicate option configuration: --foo (alias for --bar, originally for --foo)'); + expect(() => zarg([], {'--foo': 10})).to.throw('Type missing or not a function or valid array type: --foo'); + expect(() => zarg([], {'--foo': null})).to.throw('Type missing or not a function or valid array type: --foo'); + expect(() => zarg([], {'--foo': undefined})).to.throw('Type missing or not a function or valid array type: --foo'); });