Skip to content

Commit

Permalink
feat: allow implies and conflicts to accept array values (#922)
Browse files Browse the repository at this point in the history
  • Loading branch information
MattSturgeon authored and bcoe committed Sep 3, 2017
1 parent a9f03e7 commit abdc7da
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 45 deletions.
7 changes: 5 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,8 @@ Where `nyc-babel-config` is a package that exports configuration in its index.
<a name="conflicts"></a>.conflicts(x, y)
----------------------------------------------

Given the key `x` is set, the key `y` must not be set.
Given the key `x` is set, the key `y` must not be set. `y` can either be a single
string or an array of argument names that `x` conflicts with.

Optionally `.conflicts()` can accept an object specifying multiple conflicting keys.

Expand Down Expand Up @@ -798,7 +799,9 @@ var yargs = require("yargs")(['--info'])
<a name="implies"></a>.implies(x, y)
--------------

Given the key `x` is set, it is required that the key `y` is set.
Given the key `x` is set, it is required that the key `y` is set. `y` can either
be the name of an argument to imply, a number indicating a number indicating the
position of an argument or an array of multiple implications to associate with `x`.

Optionally `.implies()` can accept an object specifying multiple implications.

Expand Down
109 changes: 69 additions & 40 deletions lib/validation.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict'
const argsert = require('./argsert')
const objFilter = require('./obj-filter')
const specialKeys = ['$0', '--', '_']

Expand Down Expand Up @@ -228,13 +229,22 @@ module.exports = function validation (yargs, usage, y18n) {
// check implications, argument foo implies => argument bar.
let implied = {}
self.implies = function implies (key, value) {
argsert('<string|object> [array|number|string]', [key, value], arguments.length)

if (typeof key === 'object') {
Object.keys(key).forEach((k) => {
self.implies(k, key[k])
})
} else {
yargs.global(key)
implied[key] = value
if (!implied[key]) {
implied[key] = []
}
if (Array.isArray(value)) {
value.forEach((i) => self.implies(key, i))
} else {
implied[key].push(value)
}
}
}
self.getImplied = function getImplied () {
Expand All @@ -245,48 +255,55 @@ module.exports = function validation (yargs, usage, y18n) {
const implyFail = []

Object.keys(implied).forEach((key) => {
let num
const origKey = key
let value = implied[key]

// convert string '1' to number 1
num = Number(key)
key = isNaN(num) ? key : num

if (typeof key === 'number') {
// check length of argv._
key = argv._.length >= key
} else if (key.match(/^--no-.+/)) {
// check if key doesn't exist
key = key.match(/^--no-(.+)/)[1]
key = !argv[key]
} else {
// check if key exists
key = argv[key]
}
if (implied[key]) {
implied[key].forEach((value) => {
let num
const origKey = key

// convert string '1' to number 1
num = Number(key)
key = isNaN(num) ? key : num

if (typeof key === 'number') {
// check length of argv._
key = argv._.length >= key
} else if (key.match(/^--no-.+/)) {
// check if key doesn't exist
key = key.match(/^--no-(.+)/)[1]
key = !argv[key]
} else {
// check if key exists
key = argv[key]
}

num = Number(value)
value = isNaN(num) ? value : num
num = Number(value)
value = isNaN(num) ? value : num

if (typeof value === 'number') {
value = argv._.length >= value
} else if (value.match(/^--no-.+/)) {
value = value.match(/^--no-(.+)/)[1]
value = !argv[value]
} else {
value = argv[value]
}
if (typeof value === 'number') {
value = argv._.length >= value
} else if (value.match(/^--no-.+/)) {
value = value.match(/^--no-(.+)/)[1]
value = !argv[value]
} else {
value = argv[value]
}

if (key && !value) {
implyFail.push(origKey)
if (key && !value) {
implyFail.push(origKey)
}
})
}
})

if (implyFail.length) {
let msg = `${__('Implications failed:')}\n`

implyFail.forEach((key) => {
msg += (` ${key} -> ${implied[key]}`)
if (implied[key]) {
implied[key].forEach((value) => {
msg += (` ${key} -> ${value}`)
})
}
})

usage.fail(msg)
Expand All @@ -295,24 +312,36 @@ module.exports = function validation (yargs, usage, y18n) {

let conflicting = {}
self.conflicts = function conflicts (key, value) {
argsert('<string|object> [array|string]', [key, value], arguments.length)

if (typeof key === 'object') {
Object.keys(key).forEach((k) => {
self.conflicts(k, key[k])
})
} else {
yargs.global(key)
conflicting[key] = value
if (!conflicting[key]) {
conflicting[key] = []
}
if (Array.isArray(value)) {
value.forEach((i) => self.conflicts(key, i))
} else {
conflicting[key].push(value)
}
}
}
self.getConflicting = () => conflicting

self.conflicting = function conflictingFn (argv) {
const args = Object.getOwnPropertyNames(argv)
args.forEach((arg) => {
// we default keys to 'undefined' that have been configured, we should not
// apply conflicting check unless they are a value other than 'undefined'.
if (conflicting[arg] && args.indexOf(conflicting[arg]) !== -1 && argv[arg] !== undefined && argv[conflicting[arg]] !== undefined) {
usage.fail(__('Arguments %s and %s are mutually exclusive', arg, conflicting[arg]))
Object.keys(argv).forEach((key) => {
if (conflicting[key]) {
conflicting[key].forEach((value) => {
// we default keys to 'undefined' that have been configured, we should not
// apply conflicting check unless they are a value other than 'undefined'.
if (value && argv[key] !== undefined && argv[value] !== undefined) {
usage.fail(__(`Arguments ${key} and ${value} are mutually exclusive`))
}
})
}
})
}
Expand Down
47 changes: 46 additions & 1 deletion test/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,35 @@ describe('validation tests', () => {
.argv
})

it('fails if either implied argument is not set', (done) => {
let fail1 = false
let fail2 = false
yargs(['--foo', '-a'])
.boolean('foo')
.implies({
'foo': ['a', 'b']
})
.fail((msg1) => {
fail1 = true
yargs(['--foo', '-b'])
.boolean('foo')
.implies({
'foo': ['a', 'b']
})
.fail((msg2) => {
fail2 = true
msg1.should.match(/Implications failed/)
msg2.should.match(/Implications failed/)
return done()
})
.argv
})
.argv
// Prevent timeouts
fail1.should.be.true
fail2.should.be.true
})

it("fails if --no-foo's implied argument is not set", (done) => {
yargs([])
.implies({
Expand Down Expand Up @@ -150,9 +179,25 @@ describe('validation tests', () => {
.argv
})

it('fails if argument is supplied along with either conflicting argument', (done) => {
yargs(['-f', '-b'])
.conflicts('f', ['b', 'c'])
.fail((msg1) => {
yargs(['-f', '-c'])
.conflicts('f', ['b', 'c'])
.fail((msg2) => {
msg1.should.equal('Arguments f and b are mutually exclusive')
msg2.should.equal('Arguments f and c are mutually exclusive')
return done()
})
.argv
})
.argv
})

it('should not fail if no conflicting arguments are provided', () => {
yargs(['-b', '-c'])
.conflicts('f', 'b')
.conflicts('f', ['b', 'c'])
.fail((msg) => {
expect.fail()
})
Expand Down
4 changes: 2 additions & 2 deletions yargs.js
Original file line number Diff line number Diff line change
Expand Up @@ -416,13 +416,13 @@ function Yargs (processArgs, cwd, parentRequire) {
}

self.implies = function (key, value) {
argsert('<string|object> [string]', [key, value], arguments.length)
argsert('<string|object> [number|string|array]', [key, value], arguments.length)
validation.implies(key, value)
return self
}

self.conflicts = function (key1, key2) {
argsert('<string|object> [string]', [key1, key2], arguments.length)
argsert('<string|object> [string|array]', [key1, key2], arguments.length)
validation.conflicts(key1, key2)
return self
}
Expand Down

0 comments on commit abdc7da

Please sign in to comment.