Skip to content

Commit

Permalink
feat: add coerce api (yargs#586)
Browse files Browse the repository at this point in the history
* feat: add coerce api

* support coercion of positional args in command

* docs: add .coerce() method
  • Loading branch information
nexdrew authored and bcoe committed Aug 13, 2016
1 parent 0aaa68b commit 1d53ccb
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 5 deletions.
48 changes: 46 additions & 2 deletions README.md
@@ -1,4 +1,4 @@
yargs
yargs
========

Yargs be a node.js library fer hearties tryin' ter parse optstrings.
Expand Down Expand Up @@ -452,6 +452,49 @@ var argv = require('yargs')
.argv
```

<a name="coerce"></a>.coerce(key, fn)
----------------

Provide a synchronous function to coerce or transform the value(s) given on the
command line for `key`.

The coercion function should accept one argument, representing the parsed value
from the command line, and should return a new value or throw an error. The
returned value will be used as the value for `key` (or one of its aliases) in
`argv`. If the function throws, the error will be treated as a validation
failure, delegating to either a custom [`.fail()`](#fail) handler or printing
the error message in the console.

```js
var argv = require('yargs')
.coerce('file', function (arg) {
return require('fs').readFileSync(arg, 'utf8')
})
.argv
```

Optionally `.coerce()` can take an object that maps several keys to their
respective coercion function.

```js
var argv = require('yargs')
.coerce({
date: Date.parse,
json: JSON.parse
})
.argv
```

You can also map the same function to several keys at one time. Just pass an
array of keys as the first argument to `.coerce()`:

```js
var path = require('path')
var argv = require('yargs')
.coerce(['src', 'dest'], path.resolve)
.argv
```

.command(cmd, desc, [builder], [handler])
-----------------------------------------
.command(cmd, desc, [module])
Expand Down Expand Up @@ -1005,7 +1048,7 @@ By default, yargs exits the process when the user passes a help flag, uses the
`.exitProcess(false)` disables this behavior, enabling further actions after
yargs have been validated.

.fail(fn)
<a name="fail"></a>.fail(fn)
---------

Method to execute when a failure occurs, rather than printing the failure message.
Expand Down Expand Up @@ -1324,6 +1367,7 @@ Valid `opt` keys include:
- `array`: boolean, interpret option as an array, see [`array()`](#array)
- `boolean`: boolean, interpret option as a boolean flag, see [`boolean()`](#boolean)
- `choices`: value or array of values, limit valid option arguments to a predefined set, see [`choices()`](#choices)
- `coerce`: function, coerce or transform parsed command line values into another value, see [`coerce()`](#coerce)
- `config`: boolean, interpret option as a path to a JSON config file, see [`config()`](#config)
- `configParser`: function, provide a custom config parsing function, see [`config()`](#config)
- `count`: boolean, interpret option as a count of boolean flags, see [`count()`](#count)
Expand Down
18 changes: 16 additions & 2 deletions lib/command.js
Expand Up @@ -154,7 +154,7 @@ module.exports = function (yargs, usage, validation) {
innerArgv = innerArgv.argv
}

populatePositional(commandHandler, innerArgv, currentContext)
populatePositional(commandHandler, innerArgv, currentContext, yargs)

if (commandHandler.handler) {
commandHandler.handler(innerArgv)
Expand All @@ -163,7 +163,7 @@ module.exports = function (yargs, usage, validation) {
return innerArgv
}

function populatePositional (commandHandler, argv, context) {
function populatePositional (commandHandler, argv, context, yargs) {
argv._ = argv._.slice(context.commands.length) // nuke the current commands
var demanded = commandHandler.demanded.slice(0)
var optional = commandHandler.optional.slice(0)
Expand All @@ -176,6 +176,7 @@ module.exports = function (yargs, usage, validation) {
if (!argv._.length) break
if (demand.variadic) argv[demand.cmd] = argv._.splice(0)
else argv[demand.cmd] = argv._.shift()
postProcessPositional(yargs, argv, demand.cmd)
}

while (optional.length) {
Expand All @@ -184,11 +185,24 @@ module.exports = function (yargs, usage, validation) {
if (!argv._.length) break
if (maybe.variadic) argv[maybe.cmd] = argv._.splice(0)
else argv[maybe.cmd] = argv._.shift()
postProcessPositional(yargs, argv, maybe.cmd)
}

argv._ = context.commands.concat(argv._)
}

// TODO move positional arg logic to yargs-parser and remove this duplication
function postProcessPositional (yargs, argv, key) {
var coerce = yargs.getOptions().coerce[key]
if (typeof coerce === 'function') {
try {
argv[key] = coerce(argv[key])
} catch (err) {
yargs.getUsageInstance().fail(err.message, err)
}
}
}

self.reset = function () {
handlers = {}
return self
Expand Down
134 changes: 134 additions & 0 deletions test/yargs.js
Expand Up @@ -203,6 +203,7 @@ describe('yargs dsl tests', function () {
.alias('foo', 'bar')
.string('foo')
.choices('foo', ['bar', 'baz'])
.coerce('foo', function (foo) { return foo + 'bar' })
.implies('foo', 'snuh')
.group('foo', 'Group:')
.strict()
Expand All @@ -220,6 +221,7 @@ describe('yargs dsl tests', function () {
narg: {},
defaultDescription: {},
choices: {},
coerce: {},
requiresArg: [],
skipValidation: [],
count: [],
Expand Down Expand Up @@ -1258,6 +1260,138 @@ describe('yargs dsl tests', function () {
q.logs[0].split('\n').should.deep.equal(expected)
})
})

describe('.coerce()', function () {
it('supports string and function args (as option key and coerce function)', function () {
var argv = yargs(['--file', path.join(__dirname, 'fixtures', 'package.json')])
.coerce('file', function (arg) {
return JSON.parse(fs.readFileSync(arg, 'utf8'))
})
.argv
expect(argv.file).to.have.property('version').and.equal('9.9.9')
})

it('supports object arg (as map of multiple options)', function () {
var argv = yargs('--expand abc --range 1..3')
.coerce({
expand: function (arg) {
return arg.split('')
},
range: function (arg) {
var arr = arg.split('..').map(Number)
return { begin: arr[0], end: arr[1] }
}
})
.argv
expect(argv.expand).to.deep.equal(['a', 'b', 'c'])
expect(argv.range).to.have.property('begin').and.equal(1)
expect(argv.range).to.have.property('end').and.equal(3)
})

it('supports array and function args (as option keys and coerce function)', function () {
var argv = yargs(['--src', 'in', '--dest', 'out'])
.coerce(['src', 'dest'], function (arg) {
return path.resolve(arg)
})
.argv
argv.src.should.match(/in/).and.have.length.above(2)
argv.dest.should.match(/out/).and.have.length.above(3)
})

it('allows an error to be handled by fail() handler', function () {
var msg
var err
yargs('--json invalid')
.coerce('json', function (arg) {
return JSON.parse(arg)
})
.fail(function (m, e) {
msg = m
err = e
})
.argv
expect(msg).to.match(/Unexpected token i/)
expect(err).to.exist
})

it('supports an option alias', function () {
var argv = yargs('-d 2016-08-12')
.coerce('date', Date.parse)
.alias('date', 'd')
.argv
argv.date.should.equal(1470960000000)
})

it('supports a global option within command', function () {
var regex
yargs('check --regex x')
.global('regex')
.coerce('regex', RegExp)
.command('check', 'Check something', {}, function (argv) {
regex = argv.regex
})
.argv
expect(regex).to.be.an.instanceof(RegExp)
regex.toString().should.equal('/x/')
})

it('is supported by .option()', function () {
var argv = yargs('--env SHELL=/bin/bash')
.option('env', {
coerce: function (arg) {
var arr = arg.split('=')
return { name: arr[0], value: arr[1] || '' }
}
})
.argv
expect(argv.env).to.have.property('name').and.equal('SHELL')
expect(argv.env).to.have.property('value').and.equal('/bin/bash')
})

it('supports positional and variadic args for a command', function () {
var age
var dates
yargs('add 30days 2016-06-13 2016-07-18')
.command('add <age> [dates..]', 'Testing', function (yargs) {
return yargs
.coerce('age', function (arg) {
return parseInt(arg, 10) * 86400000
})
.coerce('dates', function (arg) {
return arg.map(function (str) {
return new Date(str)
})
})
}, function (argv) {
age = argv.age
dates = argv.dates
})
.argv
expect(age).to.equal(2592000000)
expect(dates).to.have.lengthOf(2)
dates[0].toString().should.equal(new Date('2016-06-13').toString())
dates[1].toString().should.equal(new Date('2016-07-18').toString())
})

it('allows an error from positional arg to be handled by fail() handler', function () {
var msg
var err
yargs('throw ball')
.command('throw <msg>', false, function (yargs) {
return yargs
.coerce('msg', function (arg) {
throw new Error(arg)
})
.fail(function (m, e) {
msg = m
err = e
})
})
.argv
expect(msg).to.equal('ball')
expect(err).to.exist
})
})
})

describe('yargs context', function () {
Expand Down
17 changes: 16 additions & 1 deletion yargs.js
Expand Up @@ -98,7 +98,7 @@ function Yargs (processArgs, cwd, parentRequire) {

var objectOptions = [
'narg', 'key', 'alias', 'default', 'defaultDescription',
'config', 'choices', 'demanded'
'config', 'choices', 'demanded', 'coerce'
]

arrayOptions.forEach(function (k) {
Expand Down Expand Up @@ -244,6 +244,19 @@ function Yargs (processArgs, cwd, parentRequire) {
return self
}

self.coerce = function (key, fn) {
if (typeof key === 'object' && !Array.isArray(key)) {
Object.keys(key).forEach(function (k) {
self.coerce(k, key[k])
})
} else {
[].concat(key).forEach(function (k) {
options.coerce[k] = fn
})
}
return self
}

self.count = function (counts) {
options.count.push.apply(options.count, [].concat(counts))
return self
Expand Down Expand Up @@ -403,6 +416,8 @@ function Yargs (processArgs, cwd, parentRequire) {
self.normalize(key)
} if ('choices' in opt) {
self.choices(key, opt.choices)
} if ('coerce' in opt) {
self.coerce(key, opt.coerce)
} if ('group' in opt) {
self.group(key, opt.group)
} if (opt.global) {
Expand Down

0 comments on commit 1d53ccb

Please sign in to comment.