Repository: yargs/yargs
Pull request #967 (2017-10-05 07:25:58)

feat: make positional arguments fully configurable
Introduces the `.positional()` method, which makes positional arguments fully configurable (but wait there's more, positional arguments will also be included separately in help output):

<img width="581" alt="screen shot 2017-10-05 at 12 08 46 am" src="https://user-images.githubusercontent.com/194609/31214713-62239ce4-a961-11e7-9165-2ad76dabf4ce.png">

TODO:

- [x]  make it so we default the settings `demanded` and `array` based on the command string.
- [x] accept the argument `$0` to represent a default command (so that it's more intuitive that commands can be used to handle top-level operations).
- [x] add documentation.
- [x] ~get translations for `Positionals:` (help wanted!).~ we can do this post-hoc.
- [x] finish adding tests.

see: #541, #570, #959, etc

cc: @tkarls, @hax, @sloanlance, @iarna, etc.

bcoe: @jeffijoe @ruimarinho @tkarls @atian25 @popomore @matatk @sloanlance

I just took on this major refactor of how we parse commands in yargs to try to:

1. make commands first class citizens that have most of the same parsing hints as flag options.
2. make the command API a bit less confusing (by improving documentation, and adding a logical command for configuring commands `.positional()`).

I would love your feedback -- I'm feeling pretty excited about this update to commands.
jeffijoe: @bcoe very nice, great work!

Patch:
diff --git a/.gitignore b/.gitignore
index 90d6638..3e4ea0e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ node_modules/
 *.swp
 test.js
 coverage
+package-lock.json
diff --git a/docs/advanced.md b/docs/advanced.md
index 7c901c7..47b79ea 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -8,13 +8,14 @@ In this section we cover some of the advanced features available in this API:
 
 ### Default Commands
 
-To specify a default command use the character `*`. A default command
+To specify a default command use the string `*` or `$0`. A default command
 will be run if the positional arguments provided match no known
-commands:
+commands. tldr; default commands allow you to define the entry point to your
+application using a similar API to subcommands.
 
 ```js
 const argv = require('yargs')
-  .command('*', 'the default command', () => {}, (argv) => {
+  .command('$0', 'the default command', () => {}, (argv) => {
     console.log('this command will be run by default')
   })
 ```
@@ -26,7 +27,7 @@ Default commands can also be used as a command alias, like so:
 
 ```js
 const argv = require('yargs')
-  .command(['serve', '*'], 'the serve command', () => {}, (argv) => {
+  .command(['serve', '$0'], 'the serve command', () => {}, (argv) => {
     console.log('this command will be run by default')
   })
 ```
@@ -73,6 +74,24 @@ yargs.command('download <url> [files..]', 'download several files')
   .argv
 ```
 
+#### Describing Positional Arguments
+
+You can use the method [`.positional()`](/docs/api.md#positionalkey-opt) in a command's builder function to describe and configure a positional argument:
+
+```js
+yargs.command('get <source> [proxy]', 'make a get HTTP request', (yargs) => {
+  yargs.positional('source', {
+    describe: 'URL to fetch content from',
+    type: 'string',
+    default: 'http://www.google.com'
+  }).positional('proxy', {
+    describe: 'optional proxy URL'
+  })
+})
+.help()
+.argv
+```
+
 ### Command Execution
 
 When a command is given on the command line, yargs will execute the following:
diff --git a/docs/api.md b/docs/api.md
index 9a1c9de..0dd237a 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1068,6 +1068,43 @@ as a configuration object.
 `cwd` can optionally be provided, the package.json will be read
 from this location.
 
+.positional(key, opt)
+------------
+
+`.positional()` allows you to configure a command's positional arguments
+with an API similar to [`.option()`](#optionkey-opt). `.positional()`
+should be called in a command's builder function, and is not
+available on the top-level yargs instance.
+
+> _you can describe top-level positional arguments using
+  [default commands](/docs/advanced.md#default-commands)._
+
+```js
+const argv = require('yargs')('run --help')
+  .command('run <port> <guid>', 'run the server', (yargs) => {
+    yargs.positional('guid', {
+      describe: 'a unique identifier for the server',
+      type: 'string'
+    })
+  }).argv
+console.log(argv)
+```
+
+Valid `opt` keys include:
+
+  - `alias`: string or array of strings, see [`alias()`](#alias)
+  - `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)
+  - `conflicts`: string or object, require certain keys not to be set, see [`conflicts()`](#conflicts)
+  - `default`: value, set a default value for the option, see [`default()`](#default)
+  - `desc`/`describe`/`description`: string, the option description for help content, see [`describe()`](#describe)
+  - `implies`: string or object, require certain keys to be set, see [`implies()`](#implies)
+  - `normalize`: boolean, apply `path.normalize()` to the option, see [`normalize()`](#normalize)
+  - `type`: one of the following strings
+      - `'boolean'`: synonymous for `boolean: true`, see [`boolean()`](#boolean)
+      - `'number'`: synonymous for `number: true`, see [`number()`](#number)
+      - `'string'`: synonymous for `string: true`, see [`string()`](#string)
+
 .recommendCommands()
 ---------------------------
 
diff --git a/lib/command.js b/lib/command.js
index ab6bcdc..b528df7 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -1,9 +1,10 @@
 'use strict'
-const path = require('path')
+
 const inspect = require('util').inspect
-const camelCase = require('camelcase')
+const path = require('path')
+const Parser = require('yargs-parser')
 
-const DEFAULT_MARKER = '*'
+const DEFAULT_MARKER = /(\*)|(\$0)/
 
 // handles parsing positional arguments,
 // and populating argv with said positional
@@ -43,7 +44,7 @@ module.exports = function command (yargs, usage, validation) {
     // check for default and filter out '*''
     let isDefault = false
     const parsedAliases = [parsedCommand.cmd].concat(aliases).filter((c) => {
-      if (c === DEFAULT_MARKER) {
+      if (DEFAULT_MARKER.test(c)) {
         isDefault = true
         return false
       }
@@ -59,6 +60,7 @@ module.exports = function command (yargs, usage, validation) {
         demanded: parsedCommand.demanded,
         optional: parsedCommand.optional
       }
+      handlers['*'] = defaultCommand
       return
     }
 
@@ -181,7 +183,10 @@ module.exports = function command (yargs, usage, validation) {
     let innerYargs = null
     let positionalMap = {}
 
-    if (command) currentContext.commands.push(command)
+    if (command) {
+      currentContext.commands.push(command)
+      currentContext.fullCommands.push(commandHandler.original)
+    }
     if (typeof commandHandler.builder === 'function') {
       // a function can be provided, which builds
       // up a yargs chain and possibly returns it.
@@ -226,7 +231,10 @@ module.exports = function command (yargs, usage, validation) {
       commandHandler.handler(innerArgv)
     }
 
-    if (command) currentContext.commands.pop()
+    if (command) {
+      currentContext.commands.pop()
+      currentContext.fullCommands.pop()
+    }
     numFiles = currentContext.files.length - numFiles
     if (numFiles > 0) currentContext.files.splice(numFiles * -1, numFiles)
 
@@ -245,59 +253,92 @@ module.exports = function command (yargs, usage, validation) {
 
     while (demanded.length) {
       const demand = demanded.shift()
-      populatePositional(demand, argv, yargs, positionalMap)
+      populatePositional(demand, argv, positionalMap)
     }
 
     while (optional.length) {
       const maybe = optional.shift()
-      populatePositional(maybe, argv, yargs, positionalMap)
+      populatePositional(maybe, argv, positionalMap)
     }
 
     argv._ = context.commands.concat(argv._)
+
+    postProcessPositionals(argv, positionalMap, self.cmdToParseOptions(commandHandler.original))
+
     return positionalMap
   }
 
-  // populate a single positional argument and its
-  // aliases onto argv.
-  function populatePositional (positional, argv, yargs, positionalMap) {
-    // "positional" consists of the positional.cmd, an array representing
-    // the positional's name and aliases, and positional.variadic
-    // indicating whether or not it is a variadic array.
-    let variadics = null
-    let value = null
-    for (let i = 0, cmd; (cmd = positional.cmd[i]) !== undefined; i++) {
-      if (positional.variadic) {
-        if (variadics) argv[cmd] = variadics.slice(0)
-        else argv[cmd] = variadics = argv._.splice(0)
-      } else {
-        if (!value && !argv._.length) continue
-        if (value) argv[cmd] = value
-        else argv[cmd] = value = argv._.shift()
-      }
-      positionalMap[cmd] = true
-      postProcessPositional(yargs, argv, cmd)
-      addCamelCaseExpansions(argv, cmd)
+  function populatePositional (positional, argv, positionalMap, parseOptions) {
+    const cmd = positional.cmd[0]
+    if (positional.variadic) {
+      positionalMap[cmd] = argv._.splice(0).map(String)
+    } else {
+      if (argv._.length) positionalMap[cmd] = [String(argv._.shift())]
     }
   }
 
-  // TODO move positional arg logic to yargs-parser and remove this duplication
-  function postProcessPositional (yargs, argv, key) {
-    const coerce = yargs.getOptions().coerce[key]
-    if (typeof coerce === 'function') {
-      try {
-        argv[key] = coerce(argv[key])
-      } catch (err) {
-        yargs.getUsageInstance().fail(err.message, err)
-      }
+  // we run yargs-parser against the positional arguments
+  // applying the same parsing logic used for flags.
+  function postProcessPositionals (argv, positionalMap, parseOptions) {
+    // combine the parsing hints we've inferred from the command
+    // string with explicitly configured parsing hints.
+    const options = Object.assign({}, yargs.getOptions())
+    options.default = Object.assign(parseOptions.default, options.default)
+    options.alias = Object.assign(parseOptions.alias, options.alias)
+    options.array = options.array.concat(parseOptions.array)
+
+    const unparsed = []
+    Object.keys(positionalMap).forEach((key) => {
+      [].push.apply(unparsed, positionalMap[key].map((value) => {
+        return `--${key} ${value}`
+      }))
+    })
+    const parsed = Parser.detailed(unparsed.join(' '), options)
+
+    if (parsed.error) {
+      yargs.getUsageInstance().fail(parsed.error.message, parsed.error)
+    } else {
+      Object.keys(parsed.argv).forEach((key) => {
+        if (key !== '_') argv[key] = parsed.argv[key]
+      })
     }
   }
 
-  function addCamelCaseExpansions (argv, option) {
-    if (/-/.test(option)) {
-      const cc = camelCase(option)
-      if (typeof argv[option] === 'object') argv[cc] = argv[option].slice(0)
-      else argv[cc] = argv[option]
+  self.cmdToParseOptions = function (cmdString) {
+    const parseOptions = {
+      array: [],
+      default: {},
+      alias: {},
+      demand: {}
     }
+
+    const parsed = self.parseCommand(cmdString)
+    parsed.demanded.forEach((d) => {
+      const cmds = d.cmd.slice(0)
+      const cmd = cmds.shift()
+      if (d.variadic) {
+        parseOptions.array.push(cmd)
+        parseOptions.default[cmd] = []
+      }
+      cmds.forEach((c) => {
+        parseOptions.alias[cmd] = c
+      })
+      parseOptions.demand[cmd] = true
+    })
+
+    parsed.optional.forEach((o) => {
+      const cmds = o.cmd.slice(0)
+      const cmd = cmds.shift()
+      if (o.variadic) {
+        parseOptions.array.push(cmd)
+        parseOptions.default[cmd] = []
+      }
+      cmds.forEach((c) => {
+        parseOptions.alias[cmd] = c
+      })
+    })
+
+    return parseOptions
   }
 
   self.reset = () => {
diff --git a/lib/usage.js b/lib/usage.js
index 16c4b19..c742f8d 100644
--- a/lib/usage.js
+++ b/lib/usage.js
@@ -80,6 +80,9 @@ module.exports = function usage (yargs, y18n) {
     return usages.reduce((usageMsg, msg) => `${usageMsg}${msg}\n`, '') || undefined
   }
 
+  const positionalGroupName = 'Positionals:'
+  self.getPositionalGroupName = () => positionalGroupName
+
   let examples = []
   self.example = (cmd, description) => {
     examples.push([cmd, description || ''])
@@ -222,7 +225,12 @@ module.exports = function usage (yargs, y18n) {
       // actually generate the switches string --foo, -f, --bar.
       const switches = normalizedKeys.reduce((acc, key) => {
         acc[key] = [ key ].concat(options.alias[key] || [])
-          .map(sw => (sw.length > 1 ? '--' : '-') + sw)
+          .map(sw => {
+            // for the special positional group don't
+            // add '--' or '-' prefix.
+            if (groupName === positionalGroupName) return sw
+            else return (sw.length > 1 ? '--' : '-') + sw
+          })
           .join(', ')
 
         return acc
diff --git a/package.json b/package.json
index 2e9774b..2ae64f6 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,6 @@
     "LICENSE"
   ],
   "dependencies": {
-    "camelcase": "^4.1.0",
     "cliui": "^3.2.0",
     "decamelize": "^1.1.1",
     "get-caller-file": "^1.0.1",
@@ -24,7 +23,7 @@
     "string-width": "^2.0.0",
     "which-module": "^2.0.0",
     "y18n": "^3.2.1",
-    "yargs-parser": "^7.0.0"
+    "yargs-parser": "^8.0.0"
   },
   "devDependencies": {
     "chai": "^3.4.1",
@@ -35,7 +34,7 @@
     "es6-promise": "^4.0.2",
     "hashish": "0.0.4",
     "mocha": "^3.0.1",
-    "nyc": "^10.3.0",
+    "nyc": "^11.2.1",
     "rimraf": "^2.5.0",
     "standard": "^8.6.0",
     "standard-version": "^4.2.0",
@@ -43,7 +42,7 @@
     "yargs-test-extends": "^1.0.1"
   },
   "scripts": {
-    "pretest": "standard",
+    "posttest": "standard",
     "test": "nyc --cache mocha --require ./test/before.js --timeout=8000 --check-leaks",
     "coverage": "nyc report --reporter=text-lcov | coveralls",
     "release": "standard-version"
diff --git a/yargs.js b/yargs.js
index fa3c23d..c578913 100644
--- a/yargs.js
+++ b/yargs.js
@@ -51,7 +51,7 @@ function Yargs (processArgs, cwd, parentRequire) {
 
   // use context object to keep track of resets, subcommand execution, etc
   // submodules should modify and check the state of context as necessary
-  const context = { resets: -1, commands: [], files: [] }
+  const context = { resets: -1, commands: [], fullCommands: [], files: [] }
   self.getContext = () => context
 
   // puts yargs back into an initial state. any keys
@@ -658,6 +658,44 @@ function Yargs (processArgs, cwd, parentRequire) {
   }
   self.getOptions = () => options
 
+  self.positional = function (key, opts) {
+    argsert('<string> <object>', [key, opts], arguments.length)
+    if (context.resets === 0) {
+      throw new YError(".positional() can only be called in a command's builder function")
+    }
+
+    // .positional() only supports a subset of the configuration
+    // options availble to .option().
+    const supportedOpts = ['default', 'implies', 'normalize',
+      'choices', 'conflicts', 'coerce', 'type', 'describe',
+      'desc', 'description', 'alias']
+    opts = objFilter(opts, (k, v) => {
+      let accept = supportedOpts.indexOf(k) !== -1
+      // type can be one of string|number|boolean.
+      if (k === 'type' && ['string', 'number', 'boolean'].indexOf(v) === -1) accept = false
+      return accept
+    })
+
+    // copy over any settings that can be inferred from the command string.
+    const fullCommand = context.fullCommands[context.fullCommands.length - 1]
+    const parseOptions = fullCommand ? command.cmdToParseOptions(fullCommand) : {
+      array: [],
+      alias: {},
+      default: {},
+      demand: {}
+    }
+    Object.keys(parseOptions).forEach((pk) => {
+      if (Array.isArray(parseOptions[pk])) {
+        if (parseOptions[pk].indexOf(key) !== -1) opts[pk] = true
+      } else {
+        if (parseOptions[pk][key] && !(pk in opts)) opts[pk] = parseOptions[pk][key]
+      }
+    })
+
+    self.group(key, usage.getPositionalGroupName())
+    return self.option(key, opts)
+  }
+
   self.group = function group (opts, groupName) {
     argsert('<string|array> <string>', [opts, groupName], arguments.length)
     const existing = preservedGroups[groupName] || groups[groupName]
@@ -954,7 +992,6 @@ function Yargs (processArgs, cwd, parentRequire) {
             return true
           })
         }
-
         // if there's a handler associated with a
         // command defer processing to it.
         const handlerKeys = command.getCommands()
@@ -985,9 +1022,6 @@ function Yargs (processArgs, cwd, parentRequire) {
           if (recommendCommands && firstUnknownCommand && !argv[helpOpt]) {
             validation.recommendCommands(firstUnknownCommand, handlerKeys)
           }
-        } else if (command.hasDefaultCommand() && !argv[helpOpt]) {
-          setPlaceholderKeys(argv)
-          return command.runCommand(null, self, parsed)
         }
 
         // generate a completion script for adding to ~/.bashrc.


Test patch:
diff --git a/test/command.js b/test/command.js
index 6bc28ae..d9e9555 100644
--- a/test/command.js
+++ b/test/command.js
@@ -1218,6 +1218,15 @@ describe('Command', () => {
         .argv
     })
 
+    it('allows $0 as an alias for a default command', (done) => {
+      yargs('9999')
+        .command('$0 [port]', 'default command', noop, (argv) => {
+          argv.port.should.equal(9999)
+          return done()
+        })
+        .argv
+    })
+
     it('does not execute default command if another command is provided', (done) => {
       yargs('run bcoe --foo bar')
         .command('*', 'default command', noop, (argv) => {})
diff --git a/test/usage.js b/test/usage.js
index 5ed522f..78592e0 100644
--- a/test/usage.js
+++ b/test/usage.js
@@ -2556,4 +2556,146 @@ describe('usage tests', () => {
       ])
     })
   })
+
+  describe('positional', () => {
+    it('should display help section for positionals', () => {
+      const r = checkUsage(() => yargs('--help list')
+        .command('list [pattern]', 'List key-value pairs for pattern', (yargs) => {
+          yargs.positional('pattern', {
+            describe: 'the pattern to list keys for'
+          })
+        })
+        .argv
+      )
+
+      r.logs[0].split('\n').should.deep.equal([
+        './usage list [pattern]',
+        '',
+        'Positionals:',
+        '  pattern  the pattern to list keys for',
+        '',
+        'Options:',
+        '  --help     Show help                                                 [boolean]',
+        '  --version  Show version number                                       [boolean]',
+        ''
+      ])
+    })
+
+    it('shows that variadic positional arguments are arrays', () => {
+      const r = checkUsage(() => yargs('--help list')
+        .command('list [pattern...]', 'List key-value pairs for pattern', (yargs) => {
+          yargs.positional('pattern', {
+            describe: 'the pattern to list keys for'
+          })
+        })
+        .argv
+      )
+
+      r.logs[0].split('\n').should.deep.equal([
+        './usage list [pattern...]',
+        '',
+        'Positionals:',
+        '  pattern  the pattern to list keys for                    [array] [default: []]',
+        '',
+        'Options:',
+        '  --help     Show help                                                 [boolean]',
+        '  --version  Show version number                                       [boolean]',
+        ''
+      ])
+    })
+
+    it('indicates that <foo> positional arguments are required', () => {
+      const r = checkUsage(() => yargs('--help list')
+        .command('list <pattern>', 'List key-value pairs for pattern', (yargs) => {
+          yargs.positional('pattern', {
+            describe: 'the pattern to list keys for'
+          })
+        })
+        .argv
+      )
+
+      r.logs[0].split('\n').should.deep.equal([
+        './usage list <pattern>',
+        '',
+        'Positionals:',
+        '  pattern  the pattern to list keys for                               [required]',
+        '',
+        'Options:',
+        '  --help     Show help                                                 [boolean]',
+        '  --version  Show version number                                       [boolean]',
+        ''
+      ])
+    })
+
+    it('displays aliases appropriately', () => {
+      const r = checkUsage(() => yargs('--help list')
+        .command('list [pattern|thingy]', 'List key-value pairs for pattern', (yargs) => {
+          yargs.positional('pattern', {
+            describe: 'the pattern to list keys for'
+          })
+        })
+        .argv
+      )
+
+      r.logs[0].split('\n').should.deep.equal([
+        './usage list [pattern|thingy]',
+        '',
+        'Positionals:',
+        '  pattern, thingy  the pattern to list keys for',
+        '',
+        'Options:',
+        '  --help     Show help                                                 [boolean]',
+        '  --version  Show version number                                       [boolean]',
+        ''
+      ])
+    })
+
+    it('displays type information', () => {
+      const r = checkUsage(() => yargs('--help list')
+        .command('list [pattern]', 'List key-value pairs for pattern', (yargs) => {
+          yargs.positional('pattern', {
+            describe: 'the pattern to list keys for',
+            type: 'string'
+          })
+        })
+        .argv
+      )
+
+      r.logs[0].split('\n').should.deep.equal([
+        './usage list [pattern]',
+        '',
+        'Positionals:',
+        '  pattern  the pattern to list keys for                                 [string]',
+        '',
+        'Options:',
+        '  --help     Show help                                                 [boolean]',
+        '  --version  Show version number                                       [boolean]',
+        ''
+      ])
+    })
+
+    it('displays choices array', () => {
+      const r = checkUsage(() => yargs('--help list')
+        .command('list [pattern]', 'List key-value pairs for pattern', (yargs) => {
+          yargs.positional('pattern', {
+            describe: 'the pattern to list keys for',
+            choices: ['foo', 'bar']
+          })
+        })
+        .argv
+      )
+
+      r.logs[0].split('\n').should.deep.equal([
+        './usage list [pattern]',
+        '',
+        'Positionals:',
+        '  pattern  the pattern to list keys for                  [choices: "foo", "bar"]',
+        '',
+        'Options:',
+        '  --help     Show help                                                 [boolean]',
+        '  --version  Show version number                                       [boolean]',
+        ''
+      ])
+    })
+  })
 })
diff --git a/test/yargs.js b/test/yargs.js
index 66dd67b..c41cb7e 100644
--- a/test/yargs.js
+++ b/test/yargs.js
@@ -1904,44 +1904,215 @@ describe('yargs dsl tests', () => {
       argv._[0].should.equal('--grep=foobar')
     })
   })
-})
 
-describe('yargs context', () => {
-  beforeEach(() => {
-    delete require.cache[require.resolve('../')]
-    yargs = require('../')
-  })
+  describe('yargs context', () => {
+    beforeEach(() => {
+      delete require.cache[require.resolve('../')]
+      yargs = require('../')
+    })
 
-  it('should begin with initial state', () => {
-    const context = yargs.getContext()
-    context.resets.should.equal(0)
-    context.commands.should.deep.equal([])
-  })
+    it('should begin with initial state', () => {
+      const context = yargs.getContext()
+      context.resets.should.equal(0)
+      context.commands.should.deep.equal([])
+    })
 
-  it('should track number of resets', () => {
-    const context = yargs.getContext()
-    yargs.reset()
-    context.resets.should.equal(1)
-    yargs.reset()
-    yargs.reset()
-    context.resets.should.equal(3)
-  })
+    it('should track number of resets', () => {
+      const context = yargs.getContext()
+      yargs.reset()
+      context.resets.should.equal(1)
+      yargs.reset()
+      yargs.reset()
+      context.resets.should.equal(3)
+    })
 
-  it('should track commands being executed', () => {
-    let context
-    yargs('one two')
-      .command('one', 'level one', (yargs) => {
-        context = yargs.getContext()
-        context.commands.should.deep.equal(['one'])
-        return yargs.command('two', 'level two', (yargs) => {
-          context.commands.should.deep.equal(['one', 'two'])
+    it('should track commands being executed', () => {
+      let context
+      yargs('one two')
+        .command('one', 'level one', (yargs) => {
+          context = yargs.getContext()
+          context.commands.should.deep.equal(['one'])
+          return yargs.command('two', 'level two', (yargs) => {
+            context.commands.should.deep.equal(['one', 'two'])
+          }, (argv) => {
+            context.commands.should.deep.equal(['one', 'two'])
+          })
         }, (argv) => {
-          context.commands.should.deep.equal(['one', 'two'])
+          context.commands.should.deep.equal(['one'])
         })
-      }, (argv) => {
-        context.commands.should.deep.equal(['one'])
-      })
-      .argv
-    context.commands.should.deep.equal([])
+        .argv
+      context.commands.should.deep.equal([])
+    })
+  })
+
+  describe('positional', () => {
+    it('defaults array with no arguments to []', () => {
+      const args = yargs('cmd')
+        .command('cmd [foo..]', 'run the cmd', (yargs) => {
+          yargs.positional('foo', {
+            describe: 'foo positionals'
+          })
+        })
+        .argv
+      args.foo.should.eql([])
+    })
+
+    it('populates array with appropriate arguments', () => {
+      const args = yargs('cmd /tmp/foo/bar a b')
+        .command('cmd <file> [foo..]', 'run the cmd', (yargs) => {
+          yargs
+            .positional('file', {
+              describe: 'the required bit'
+            })
+            .positional('foo', {
+              describe: 'the variadic bit'
+            })
+        })
+        .argv
+      args.file.should.equal('/tmp/foo/bar')
+      args.foo.should.eql(['a', 'b'])
+    })
+
+    it('allows a conflicting argument to be specified', (done) => {
+      yargs()
+        .command('cmd <hero>', 'a command', (yargs) => {
+          yargs.positional('hero', {
+            conflicts: 'conflicting'
+          })
+        }).parse('cmd batman --conflicting', (err) => {
+          err.message.should.equal(
+            'Arguments hero and conflicting are mutually exclusive'
+          )
+          return done()
+        })
+    })
+
+    it('allows a default to be set', () => {
+      const argv = yargs('cmd')
+        .command('cmd [heroes...]', 'a command', (yargs) => {
+          yargs.positional('heroes', {
+            default: ['batman', 'Iron Man']
+          })
+        }).argv
+      argv.heroes.should.eql(['batman', 'Iron Man'])
+    })
+
+    it('allows an implied argument to be specified', (done) => {
+      yargs()
+        .command('cmd <hero>', 'a command', (yargs) => {
+          yargs.positional('hero', {
+            implies: 'universe'
+          })
+        }).parse('cmd batman', (err) => {
+          err.message.should.match(/hero -> universe/)
+          return done()
+        })
+    })
+
+    it('allows an alias to be provided', () => {
+      const argv = yargs('cmd')
+        .command('cmd [heroes...]', 'a command', (yargs) => {
+          yargs.positional('heroes', {
+            alias: 'do-gooders',
+            default: ['batman', 'robin']
+          })
+        }).argv
+      argv.heroes.should.eql(['batman', 'robin'])
+      argv.doGooders.should.eql(['batman', 'robin'])
+      argv['do-gooders'].should.eql(['batman', 'robin'])
+    })
+
+    it('allows normalize to be specified', () => {
+      const argv = yargs('cmd /tmp/awesome/../ /tmp/awesome/b/../')
+        .command('cmd <files...>', 'a command', (yargs) => {
+          yargs.positional('files', {
+            normalize: true
+          })
+        }).argv
+      argv.files.should.eql([
+        '/tmp/'.replace(/\//g, path.sep),
+        '/tmp/awesome/'.replace(/\//g, path.sep)
+      ])
+    })
+
+    it('allows a choices array to be specified', (done) => {
+      yargs()
+        .command('cmd <hero>', 'a command', (yargs) => {
+          yargs.positional('hero', {
+            choices: ['batman', 'Iron Man', 'robin']
+          })
+        }).parse('cmd joker', (err) => {
+          err.message.should.match(
+            /Argument: hero, Given: "joker", Choices: "batman"/
+          )
+          return done()
+        })
+    })
+
+    it('allows a coerce method to be provided', () => {
+      const argv = yargs('cmd batman')
+        .command('cmd <hero>', 'a command', (yargs) => {
+          yargs.positional('hero', {
+            coerce: function (arg) {
+              return arg.toUpperCase()
+            },
+            alias: 'do-gooder'
+          })
+        }).argv
+      argv.hero.should.equal('BATMAN')
+      argv.doGooder.should.equal('BATMAN')
+    })
+
+    it('allows a boolean type to be specified', () => {
+      const argv = yargs('cmd false')
+        .command('cmd [run]', 'a command', (yargs) => {
+          yargs.positional('run', {
+            type: 'boolean'
+          })
+        }).argv
+      argv.run.should.equal(false)
+    })
+
+    it('allows a number type to be specified', () => {
+      const argv = yargs('cmd nan')
+        .command('cmd [count]', 'a command', (yargs) => {
+          yargs.positional('count', {
+            type: 'number'
+          })
+        }).argv
+      isNaN(argv.count).should.equal(true)
+    })
+
+    it('allows a string type to be specified', () => {
+      const argv = yargs('cmd 33')
+        .command('cmd [str]', 'a command', (yargs) => {
+          yargs.positional('str', {
+            type: 'string'
+          })
+        }).argv
+      argv.str.should.equal('33')
+    })
+
+    it('allows positional arguments for subcommands to be configured', () => {
+      const argv = yargs('cmd subcommand 33')
+        .command('cmd', 'a command', (yargs) => {
+          yargs.command('subcommand [str]', 'a subcommand', (yargs) => {
+            yargs.positional('str', {
+              type: 'string'
+            })
+          })
+        }).argv
+
+      argv.str.should.equal('33')
+    })
+
+    it("can only be used as part of a command's builder function", () => {
+      expect(() => {
+        yargs('foo')
+          .positional('foo', {
+            describe: 'I should not work'
+          })
+      }).to.throw(/\.positional\(\) can only be called/)
+    })
   })
 })