Skip to content

Commit

Permalink
feat: .usage() can now be used to configure a default command (#975)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: .usage() no longer accepts an options object as the second argument. It can instead be used as an alias for configuring a default command.
  • Loading branch information
bcoe committed Oct 18, 2017
1 parent 3757194 commit 7269531
Show file tree
Hide file tree
Showing 7 changed files with 387 additions and 111 deletions.
19 changes: 16 additions & 3 deletions docs/api.md
Expand Up @@ -1128,7 +1128,7 @@ usage information and exit.
The default behavior is to set the value of any key not followed by an
option value to `true`.

<a name="reset"></a>.reset()
<a name="reset"></a>.reset() [DEPRECATED]
--------

Reset the argument object built up so far. This is useful for
Expand Down Expand Up @@ -1285,14 +1285,27 @@ Options:
If you explicitly specify a `locale()`, you should do so *before* calling
`updateStrings()`.

.usage(message, [opts])
.usage(<message|command>, [desc], [builder], [handler])
---------------------

Set a usage message to show which commands to use. Inside `message`, the string
`$0` will get interpolated to the current script name or node command for the
present script similar to how `$0` works in bash or perl.

`opts` is optional and acts like calling `.options(opts)`.
If the optional `desc`/`builder`/`handler` are provided, `.usage()`
acts an an alias for [`.command()`](#commandmodule). This allows you to use
`.usage()` to configure the [default command](/docs/advanced.md#default-commands) that will be run as an entry-point to your application and allows you
to provide configuration for the positional arguments accepted by your program:

```js
const argv = require('yargs')
.usage('$0 <port>', 'start the application server', (yargs) => {
yargs.positional('port', {
describe: 'the port that your application should bind to',
type: 'number'
})
}).argv
```

<a name="version"></a>
.version()
Expand Down
65 changes: 47 additions & 18 deletions lib/command.js
Expand Up @@ -4,7 +4,7 @@ const inspect = require('util').inspect
const path = require('path')
const Parser = require('yargs-parser')

const DEFAULT_MARKER = /(\*)|(\$0)/
const DEFAULT_MARKER = /(^\*)|(^\$0)/

// handles parsing positional arguments,
// and populating argv with said positional
Expand Down Expand Up @@ -50,19 +50,8 @@ module.exports = function command (yargs, usage, validation) {
}
return true
})

// short-circuit if default with no aliases
if (isDefault && parsedAliases.length === 0) {
defaultCommand = {
original: cmd.replace(DEFAULT_MARKER, '').trim(),
handler,
builder: builder || {},
demanded: parsedCommand.demanded,
optional: parsedCommand.optional
}
handlers['*'] = defaultCommand
return
}
// standardize on $0 for default command.
if (parsedAliases.length === 0 && isDefault) parsedAliases.push('$0')

// shift cmd and aliases after filtering out '*'
if (isDefault) {
Expand All @@ -82,6 +71,7 @@ module.exports = function command (yargs, usage, validation) {

handlers[parsedCommand.cmd] = {
original: cmd,
description: description,
handler,
builder: builder || {},
demanded: parsedCommand.demanded,
Expand Down Expand Up @@ -182,7 +172,6 @@ module.exports = function command (yargs, usage, validation) {
let innerArgv = parsed.argv
let innerYargs = null
let positionalMap = {}

if (command) {
currentContext.commands.push(command)
currentContext.fullCommands.push(commandHandler.original)
Expand All @@ -196,8 +185,11 @@ module.exports = function command (yargs, usage, validation) {
// original command string as usage() for consistent behavior with
// options object below.
if (yargs.parsed === false) {
if (typeof yargs.getUsageInstance().getUsage() === 'undefined') {
yargs.usage(`$0 ${parentCommands.length ? `${parentCommands.join(' ')} ` : ''}${commandHandler.original}`)
if (shouldUpdateUsage(yargs)) {
yargs.getUsageInstance().usage(
usageFromParentCommandsCommandHandler(parentCommands, commandHandler),
commandHandler.description
)
}
innerArgv = innerYargs ? innerYargs._parseArgs(null, null, true, commandIndex) : yargs._parseArgs(null, null, true, commandIndex)
} else {
Expand All @@ -210,7 +202,12 @@ module.exports = function command (yargs, usage, validation) {
// as a short hand, an object can instead be provided, specifying
// the options that a command takes.
innerYargs = yargs.reset(parsed.aliases)
innerYargs.usage(`$0 ${parentCommands.length ? `${parentCommands.join(' ')} ` : ''}${commandHandler.original}`)
if (shouldUpdateUsage(innerYargs)) {
innerYargs.getUsageInstance().usage(
usageFromParentCommandsCommandHandler(parentCommands, commandHandler),
commandHandler.description
)
}
Object.keys(commandHandler.builder).forEach((key) => {
innerYargs.option(key, commandHandler.builder[key])
})
Expand Down Expand Up @@ -241,6 +238,38 @@ module.exports = function command (yargs, usage, validation) {
return innerArgv
}

function shouldUpdateUsage (yargs) {
return !yargs.getUsageInstance().getUsageDisabled() &&
yargs.getUsageInstance().getUsage().length === 0
}

function usageFromParentCommandsCommandHandler (parentCommands, commandHandler) {
const c = DEFAULT_MARKER.test(commandHandler.original) ? commandHandler.original.replace(DEFAULT_MARKER, '') : commandHandler.original
const pc = parentCommands.filter((c) => { return !DEFAULT_MARKER.test(c) })
pc.push(c)
return `$0 ${pc.join(' ')}`
}

self.runDefaultBuilderOn = function (yargs) {
if (shouldUpdateUsage(yargs)) {
// build the root-level command string from the default string.
const commandString = DEFAULT_MARKER.test(defaultCommand.original)
? defaultCommand.original : defaultCommand.original.replace(/^[^[\]<>]*/, '$0 ')
yargs.getUsageInstance().usage(
commandString,
defaultCommand.description
)
}
const builder = defaultCommand.builder
if (typeof builder === 'function') {
builder(yargs)
} else {
Object.keys(builder).forEach((key) => {
yargs.option(key, builder[key])
})
}
}

// transcribe all positional arguments "command <foo> <bar> [apple]"
// onto argv.
function populatePositionals (commandHandler, argv, context, yargs) {
Expand Down
60 changes: 45 additions & 15 deletions lib/usage.js
Expand Up @@ -67,22 +67,26 @@ module.exports = function usage (yargs, y18n) {
// methods for ouputting/building help (usage) message.
let usages = []
let usageDisabled = false
self.usage = (msg) => {
self.usage = (msg, description) => {
if (msg === null) {
usageDisabled = true
usages = []
return
}
usageDisabled = false
usages.push(msg)
usages.push([msg, description || ''])
return self
}
self.getUsage = () => {
if (usageDisabled) return null
return usages.reduce((usageMsg, msg) => `${usageMsg}${msg}\n`, '') || undefined
return usages
}
self.getUsageDisabled = () => {
return usageDisabled
}

const positionalGroupName = 'Positionals:'
self.getPositionalGroupName = () => positionalGroupName
self.getPositionalGroupName = () => {
return __('Positionals:')
}

let examples = []
self.example = (cmd, description) => {
Expand Down Expand Up @@ -143,6 +147,7 @@ module.exports = function usage (yargs, y18n) {
normalizeAliases()

// handle old demanded API
const base$0 = path.basename(yargs.$0)
const demandedOptions = yargs.getDemandedOptions()
const demandedCommands = yargs.getDemandedCommands()
const groups = yargs.getGroups()
Expand All @@ -165,9 +170,26 @@ module.exports = function usage (yargs, y18n) {
})

// the usage string.
if (usages.length && !usageDisabled) {
const u = self.getUsage().replace(/\$0/g, path.basename(yargs.$0))
ui.div(`${u}`)
if (!usageDisabled) {
if (usages.length) {
// user-defined usage.
usages.forEach((usage) => {
ui.div(`${usage[0].replace(/\$0/g, base$0)}`)
if (usage[1]) {
ui.div({text: `${usage[1]}`, padding: [1, 0, 0, 0]})
}
})
ui.div()
} else if (commands.length) {
let u = null
// demonstrate how commands are used.
if (demandedCommands._) {
u = `${base$0} <${__('command')}>\n`
} else {
u = `${base$0} [${__('command')}]\n`
}
ui.div(`${u}`)
}
}

// your application's commands, i.e., non-option
Expand All @@ -176,8 +198,13 @@ module.exports = function usage (yargs, y18n) {
ui.div(__('Commands:'))

commands.forEach((command) => {
const commandString = `${base$0} ${command[0].replace(/^\$0 ?/, '')}` // drop $0 from default commands.
ui.span(
{text: command[0], padding: [0, 2, 0, 2], width: maxWidth(commands, theWrap) + 4},
{
text: commandString,
padding: [0, 2, 0, 2],
width: maxWidth(commands, theWrap, base$0) + 4
},
{text: command[1]}
)
const hints = []
Expand Down Expand Up @@ -229,7 +256,7 @@ module.exports = function usage (yargs, y18n) {
.map(sw => {
// for the special positional group don't
// add '--' or '-' prefix.
if (groupName === positionalGroupName) return sw
if (groupName === self.getPositionalGroupName()) return sw
else return (sw.length > 1 ? '--' : '-') + sw
})
.join(', ')
Expand Down Expand Up @@ -276,7 +303,7 @@ module.exports = function usage (yargs, y18n) {
ui.div(__('Examples:'))

examples.forEach((example) => {
example[0] = example[0].replace(/\$0/g, path.basename(yargs.$0))
example[0] = example[0].replace(/\$0/g, base$0)
})

examples.forEach((example) => {
Expand Down Expand Up @@ -305,7 +332,7 @@ module.exports = function usage (yargs, y18n) {

// the usage string.
if (epilog) {
const e = epilog.replace(/\$0/g, path.basename(yargs.$0))
const e = epilog.replace(/\$0/g, base$0)
ui.div(`${e}\n`)
}

Expand All @@ -314,7 +341,7 @@ module.exports = function usage (yargs, y18n) {

// return the maximum width of a string
// in the left-hand column of a table.
function maxWidth (table, theWrap) {
function maxWidth (table, theWrap, modifier) {
let width = 0

// table might be of the form [leftColumn],
Expand All @@ -324,7 +351,10 @@ module.exports = function usage (yargs, y18n) {
}

table.forEach((v) => {
width = Math.max(stringWidth(v[0]), width)
width = Math.max(
stringWidth(modifier ? `${modifier} ${v[0]}` : v[0]),
width
)
})

// if we've enabled 'wrap' we should limit
Expand Down
51 changes: 42 additions & 9 deletions test/command.js
Expand Up @@ -515,8 +515,9 @@ describe('Command', () => {
r.should.have.property('errors').with.length(0)
r.should.have.property('logs')
r.logs.join('\n').split(/\n+/).should.deep.equal([
'usage [command]',
'Commands:',
' dream [command] [opts] Go to sleep and dream',
' usage dream [command] [opts] Go to sleep and dream',
'Options:',
' --help Show help [boolean]',
' --version Show version number [boolean]',
Expand All @@ -533,9 +534,10 @@ describe('Command', () => {
r.should.have.property('logs')
r.logs[0].split(/\n+/).should.deep.equal([
'command dream [command] [opts]',
'Go to sleep and dream',
'Commands:',
' of-memory <memory> Dream about a specific memory',
' within-a-dream [command] [opts] Dream within a dream',
' command of-memory <memory> Dream about a specific memory',
' command within-a-dream [command] [opts] Dream within a dream',
'Options:',
' --help Show help [boolean]',
' --version Show version number [boolean]',
Expand All @@ -553,11 +555,12 @@ describe('Command', () => {
r.should.have.property('errors').with.length(0)
r.should.have.property('logs')
r.logs.join('\n').split(/\n+/).should.deep.equal([
'usage [command]',
'Commands:',
' limbo [opts] Get lost in pure subconscious',
' inception [command] [opts] Enter another dream, where inception is possible',
' within-a-dream [command] [opts] Dream within a dream',
' dream [command] [opts] Go to sleep and dream',
' usage limbo [opts] Get lost in pure subconscious',
' usage inception [command] [opts] Enter another dream, where inception is possible',
' usage within-a-dream [command] [opts] Dream within a dream',
' usage dream [command] [opts] Go to sleep and dream',
'Options:',
' --help Show help [boolean]',
' --version Show version number [boolean]',
Expand Down Expand Up @@ -605,6 +608,7 @@ describe('Command', () => {
r.should.have.property('logs')
r.logs.join('\n').split(/\n+/).should.deep.equal([
'command cyclic',
'Attempts to (re)apply its own dir',
'Options:',
' --help Show help [boolean]',
' --version Show version number [boolean]',
Expand All @@ -620,8 +624,9 @@ describe('Command', () => {
r.should.have.property('errors').with.length(0)
r.should.have.property('logs')
r.logs.join('\n').split(/\n+/).should.deep.equal([
'usage [command]',
'Commands:',
' nameless Command name derived from module filename',
' usage nameless Command name derived from module filename',
'Options:',
' --help Show help [boolean]',
' --version Show version number [boolean]',
Expand Down Expand Up @@ -675,8 +680,9 @@ describe('Command', () => {

const expectedCmd = [
'command cmd <sub>',
'Try a command',
'Commands:',
' sub Run the subcommand',
' command sub Run the subcommand',
'Options:',
' --help Show help [boolean]',
' --version Show version number [boolean]',
Expand All @@ -685,6 +691,7 @@ describe('Command', () => {

const expectedSub = [
'command cmd sub',
'Run the subcommand',
'Options:',
' --help Show help [boolean]',
' --version Show version number [boolean]',
Expand Down Expand Up @@ -1369,4 +1376,30 @@ describe('Command', () => {
argv._.should.eql(['foo', 'bar'])
})
})

describe('usage', () => {
it('allows you to configure a default command', () => {
yargs()
.usage('$0 <port>', 'default command', (yargs) => {
yargs.positional('port', {
type: 'string'
})
})
.parse('33', (err, argv) => {
expect(err).to.equal(null)
argv.port.should.equal('33')
})
})

it('throws exception if default command does not have leading $0', () => {
expect(() => {
yargs()
.usage('<port>', 'default command', (yargs) => {
yargs.positional('port', {
type: 'string'
})
})
}).to.throw(/.*\.usage\(\) description must start with \$0.*/)
})
})
})

0 comments on commit 7269531

Please sign in to comment.