'use strict'
const EOL = '\n'
const noop = () => {}
module.exports = {
command,
hiddenCommand,
header,
footer,
argv,
flag,
hiddenFlag,
arg,
rest,
validate,
summary,
description,
sloppy,
bail
}
class Parser {
constructor(argv) {
this.argv = argv
this.i = 0
this.multi = null
}
rest() {
const rest = this.argv.slice(this.i)
this.lasti = this.i
this.i = this.argv.length
return rest
}
next() {
while (true) {
const r = {
flag: null,
rest: null,
arg: null
}
if (this.multi && this.multi.length) {
const name = this.multi.pop()
r.flag = { long: false, name, value: '' }
return r
}
this.multi = null
if (this.i >= this.argv.length) return null
this.lasti = this.i
const a = this.argv[this.i++]
if (a === '--') {
r.rest = this.rest()
return r
}
if (a.length > 2 && a[0] === '-' && a[1] === '-') {
let j = a.indexOf('=')
if (j === -1) j = a.length
const inverse = a.startsWith('--no-')
const name = inverse ? a.slice(5, j) : a.slice(2, j)
const value = a.slice(j + 1)
r.flag = { long: true, name, value, inverse }
return r
}
if (a.length > 1 && a[0] === '-') {
this.multi = []
for (let i = 1; i < a.length; i++) this.multi.push(a[i])
this.multi.reverse()
continue
}
r.arg = a
return r
}
}
}
class Command {
constructor(parent, name) {
this.parent = parent
this.name = name
this.hidden = false
this.description = ''
this.summary = ''
this.header = ''
this.footer = ''
this.flags = {}
this.args = {}
this.positionals = []
this.rest = null
this.bailed = null
this.indices = { flags: {}, args: {}, positionals: [], rest: undefined }
this.running = null
this.argv = null
this._validators = []
this._runner = null
this._onbail = null
this._indent = ''
this._strictFlags = true
this._strictArgs = true
this._labels = {
arguments: 'Arguments:',
flags: 'Flags:',
commands: 'Commands:'
}
this._definedCommands = new Map()
this._definedFlags = new Map()
this._definedArgs = []
this._definedRest = null
}
hide() {
this.hidden = true
return this
}
bail(bail) {
try {
this.bailed = { output: this._bail(bail), bail }
} catch (err) {
this.bailed = { error: err, bail }
throw err
}
return this.bailed
}
overview({ full = false } = {}) {
let s = ''
if (this.header) s += this.header + EOL + EOL
for (const [name, command] of this._definedCommands) {
if (command.hidden) continue
if (full) {
command._indent = this.header ? ' ' : ''
s += command._indent + command._oneliner(false) + EOL + command._aligned() + EOL
command._indent = ''
} else s += ' ' + this.name + ' ' + name + ' ~ ' + command.summary + EOL
}
if (this.footer) {
s += EOL + (Object.hasOwn(this.footer, 'overview') ? this.footer.overview : this.footer) + EOL
}
return s
}
help(...args) {
let s = ''
if (this.header) s += this.header + EOL + EOL
this._indent = this.header ? ' ' : ''
const output = this.usage(...args)
s += typeof output === 'string' ? output : ''
this._indent = ''
if (this.footer) {
s += EOL + (Object.hasOwn(this.footer, 'help') ? this.footer.help : this.footer) + EOL
}
return s
}
usage(subcommand, ...args) {
if (subcommand) {
const sub = this._definedCommands.get(subcommand)
if (!sub) return this.bail(createBail(this, 'UNKNOWN_ARG', null, { value: subcommand }))
sub.parent = this
const str = sub.usage(...args)
return str
}
let s = this._indent + this._oneliner(false) + EOL
s += this._aligned()
return s
}
parse(input = argv(), opts = { run: true }) {
const { sync = false, bails = true } = opts
const p = new Parser(input)
let c = this._reset()
let bail = null
const visited = [c]
while (bail === null) {
if (
c._definedRest !== null &&
c.positionals.length === c._definedArgs.length &&
c._definedArgs.length > 0
) {
bail = c._onrest(p.rest(), p)
break
}
const n = p.next()
if (n === null) break
if (n.flag) {
bail = c._onflag(n.flag, p)
continue
}
if (n.arg && c._definedArgs.length > 0) {
bail = c._onarg(n.arg, p)
continue
}
if (c._definedCommands.size > 0) {
const cmd = c._getCommand(n.arg)
if (cmd !== null) {
c = cmd._reset()
visited.push(c)
continue
}
}
p.i--
bail = c._onrest(p.rest(), p)
}
if (c !== this && !c.parent) c.parent = this
if (c.parent) c.parent.current = c
if (!bail) {
const hasHelpFlag = 'help' in this.indices.flags
const missing = this._definedArgs.filter(
(arg) => !hasHelpFlag && !arg.optional && !(arg.name in c.args)
)
if (missing.length > 0) {
bail = createBail(this, 'MISSING_ARG', null, { value: missing[0].help })
}
}
if (!bail) {
if (c.flags.help) {
if (!opts.silent) console.log(c.help())
return null
}
for (const v of c._validators) {
bail = runValidation(v, c)
if (bail) {
if (bails) c.bail(bail)
return null
}
}
if (c._runner !== null && opts.run !== false) {
c.argv = input
if (sync) runSync(c)
else c.running = runAsync(c)
}
return c
}
if (bails) c.bail(bail)
return null
}
_oneliner(short, relative) {
const stack = [this]
if (!relative) {
while (stack[stack.length - 1].parent) stack.push(stack[stack.length - 1].parent)
}
const run = stack.reverse().map((c) => c.name || 'app')
if (!short) {
if (this._definedFlags.size > 0) run.push('[flags]')
for (const arg of this._definedArgs) run.push(arg.help)
if (this._definedCommands.size > 0) run.push('[command]')
if (this._definedRest !== null) run.push(this._definedRest.help)
}
return run.join(' ')
}
_aligned(padding = 0, color = false) {
const l = []
const visited = new Set()
const indent = this._indent
if (this.summary) {
l.push(['', ''])
l.push(...this.summary.split(EOL).map((line) => [this._indent + line, '']))
}
if (this.description) {
l.push(['', ''])
l.push(...this.description.split(EOL).map((line) => [this._indent + line, '']))
}
if (this._definedArgs.length > 0 || this._definedRest !== null) {
l.push(['', ''])
l.push([indent + this._labels.arguments, ''])
for (const arg of this._definedArgs) {
l.push([indent + ' ' + arg.help, arg.usage()])
}
}
if (this._definedRest) {
l.push([indent + ' ' + this._definedRest.help, this._definedRest.description])
}
if (this._definedFlags.size) {
l.push(['', ''])
l.push([indent + this._labels.flags, ''])
for (const flag of this._definedFlags.values()) {
if (visited.has(flag)) continue
visited.add(flag)
if (!flag.hidden) l.push([indent + ' ' + flag.help, flag.description])
}
}
if (this._definedCommands.size) {
l.push(['', ''])
l.push([indent + this._labels.commands, ''])
for (const c of this._definedCommands.values()) {
if (c.hidden) continue
l.push([indent + ' ' + c._oneliner(true, true), c.summary || c.description])
}
}
if (padding === 0) {
for (const [left, right] of l) {
if (!right) continue
if (left.length > padding) padding = left.length
}
}
if (color === false) {
return this._aligned(padding, true)
}
let s = ''
for (const [left, right] of l) {
s += (left.padEnd(padding, ' ') + ' ' + right).trimEnd() + EOL
}
s = s.trimEnd()
return s ? s + EOL : ''
}
add(...args) {
const c = this
for (const a of args) {
if (a instanceof Command) {
c._addCommand(a)
} else if (a instanceof Flag) {
c._addFlag(a)
} else if (a instanceof Arg) {
c._addArg(a)
} else if (a instanceof Rest) {
c._addRest(a)
} else if (a instanceof Data) {
c._addData(a)
} else if (a instanceof Validation) {
c._addValidation(a)
} else if (typeof a === 'function') {
c._addRunner(a)
} else if (a !== null && Array.isArray(a) === false && typeof a === 'object') {
c._addDefinition(a)
} else {
throw new Error('Unknown arg: ' + a)
}
}
c._addFlag(new Flag('--help|-h', 'Show help'))
if (!c._runner) c._addRunner(noop)
return c
}
_addDefinition(c) {
for (const [def, dsc] of Object.entries(c)) {
const [definer, ...definition] = def.split(' ')
const define = module.exports[definer]
if (typeof define === 'function') {
const [description, modifiers = {}] = Array.isArray(dsc) ? dsc : [dsc]
const mod =
definition.length === 0 ? define(description) : define(definition.join(' '), description)
for (const [modifier, value] of Object.entries(modifiers)) {
if (typeof mod[modifier] === 'function') {
if (value === true) mod[modifier]()
else if (value !== false) mod[modifier](value)
}
}
this.add(mod)
}
}
}
_addCommand(c) {
if (!c.header) c.header = this.header
if (!c.footer) c.footer = this.footer
if (!c.parent) c.parent = this
this._definedCommands.set(c.name, c)
}
_addFlag(f) {
this._definedFlags.set(f.name, f)
for (const alias of f.aliases) {
if (alias !== f.name) {
this._definedFlags.set(alias, f)
}
}
}
_addArg(a) {
this._definedArgs.push(a)
}
_addRest(a) {
this._definedRest = a
}
_addData(d) {
switch (d.type) {
case 'bail': {
if (this._onbail !== null) throw new Error('onbail already set')
this._onbail = d.value
break
}
case 'name': {
this.name = d.value
break
}
case 'summary': {
this.summary = d.value
break
}
case 'description': {
this.description = d.value
break
}
case 'header': {
this.header = unindent(d.value)
break
}
case 'footer': {
this.footer =
d.value !== null && typeof d.value === 'object'
? { help: unindent(d.value.help || ''), overview: d.value.overview }
: unindent(d.value)
break
}
case 'sloppy': {
this._strictFlags = !d.value?.flags
this._strictArgs = !d.value?.args
break
}
}
}
_addValidation(_validator) {
this._validators.push(_validator)
}
_addRunner(_runner) {
this._runner = _runner
}
_reset() {
this.flags = {}
this.args = {}
this.positionals = []
this.rest = null
this.indices = { flags: {}, args: {}, positionals: [], rest: undefined }
this.running = null
this.argv = null
for (const [name, { value }] of this._definedFlags) {
if (name === snakeToCamel(name)) {
this.flags[name] = value
}
}
return this
}
_getFlag(name, flag) {
let f = this._definedFlags.get(name)
if (f && flag.long && f.aliases.indexOf(name) > 0) f = undefined
if (f === undefined && this._strictFlags === false) f = defaultFlag(name)
return f || null
}
_getCommand(name) {
return this._definedCommands.get(name) || null
}
_onflag(flag, parser) {
const def = this._getFlag(flag.name, flag)
if (def === null) return createBail(this, 'UNKNOWN_FLAG', flag, null)
if (def.boolean === true) {
if (flag.value) return createBail(this, 'INVALID_FLAG', flag, null)
this.flags[def.name] = !flag.inverse
if (def.aliases[1]) this.flags[def.aliases[1]] = this.flags[def.name]
def.aliases.forEach((e) => {
this.indices.flags[e] = parser.lasti
})
if (!def.aliases.includes(def.name)) this.indices.flags[def.name] = parser.lasti
return null
}
if (def.boolean === false) {
if (flag.value) {
if (def.valueChoices && !def.valueChoices.includes(flag.value)) {
return createBail(this, 'INVALID_FLAG', flag, null)
}
const value = def.multi ? (this.flags[def.name] || []).concat(flag.value) : flag.value
this.flags[def.name] = value
if (def.aliases[1]) this.flags[def.aliases[1]] = value
def.aliases.forEach((e) => {
this.indices.flags[e] = parser.lasti
})
if (!def.aliases.includes(def.name)) this.indices.flags[def.name] = parser.lasti
return null
}
const next = parser.next()
const argless = next === null || !next.arg
if (def.valueRequired && argless && !def.hasDefault) {
return createBail(this, 'INVALID_FLAG', flag, null)
}
const nextValue = !next?.arg && def.hasDefault ? def.value : next?.arg
if (def.valueChoices && !def.valueChoices.includes(nextValue)) {
return createBail(this, 'INVALID_FLAG', flag, null)
}
const value = def.multi ? (this.flags[def.name] || []).concat(nextValue) : nextValue
this.flags[def.name] = value
if (def.aliases[1]) this.flags[def.aliases[1]] = value
if (def.multi) {
def.aliases.forEach((e) => {
this.indices.flags[e] = (this.indices.flags[e] || []).concat(parser.lasti)
})
if (!def.aliases.includes(def.name)) {
this.indices.flags[def.name] = (this.indices.flags[def.name] || []).concat(parser.lasti)
}
} else {
def.aliases.forEach((e) => {
this.indices.flags[e] = parser.lasti
})
if (!def.aliases.includes(def.name)) this.indices.flags[def.name] = parser.lasti
}
}
return null
}
_onarg(arg, parser) {
const info = { index: this.positionals.length, value: arg }
if (this._definedArgs.length <= this.positionals.length) {
if (this._strictArgs === false) {
this.positionals.push(arg)
this.indices.positionals.push(parser.lasti)
return null
}
return createBail(this, 'UNKNOWN_ARG', null, info)
}
const def = this._definedArgs[this.positionals.length]
this.positionals.push(arg)
this.indices.positionals.push(parser.lasti)
this.args[def.name] = arg
this.indices.args[def.name] = parser.lasti
return null
}
_onrest(rest, parser) {
if (this._definedRest === null && this._strictArgs) {
return createBail(this, 'UNKNOWN_ARG', null, {
index: this.positionals.length,
value: rest.join(' ')
})
}
this.rest = rest
this.indices.rest = parser.lasti
return null
}
_bail(bail) {
if (typeof this._onbail === 'function') return this._onbail(bail)
if (this.parent) return this.parent._bail(bail)
if (bail.flag) throw new Bail(bail.reason + ': ' + bail.flag.name)
if (bail.arg) throw new Bail(bail.reason + ': ' + bail.arg.value)
throw new Bail(bail.reason)
}
}
class Flag {
constructor(spec, description = '') {
const { longName, shortName, aliases, boolean, help, value, valueRequired } = parseFlag(spec)
this.name = snakeToCamel(longName || shortName)
this.aliases = aliases
this.boolean = boolean
this.help = help
this.description = description
this.hidden = false
this.multi = false
this.valueChoices = undefined
this.hasDefault = false
this.value = value
this.valueRequired = valueRequired
}
default(val) {
this.hasDefault = true
this.value = val
this.description += ` (default: ${val})`
return this
}
multiple() {
this.multi = true
return this
}
choices(valueChoices) {
this.valueChoices = valueChoices
this.description += ` (choices: ${valueChoices.join(', ')})`
return this
}
hide() {
this.hidden = true
return this
}
}
class Arg {
constructor(help, description = '') {
this.optional = help.startsWith('[')
this.name = snakeToCamel(help)
this.help = help
this.description = description
this.hidden = false
}
hide() {
this.hidden = true
return this
}
usage() {
if (!this.optional) return this.description
const first = this.description.slice(0, 1)
return (
(this.description && first.toLowerCase() === first ? 'optional. ' : 'Optional. ') +
this.description
)
}
}
class Rest {
constructor(help, description = '') {
this.help = help
this.description = description
}
}
class Data {
constructor(type, value) {
this.type = type
this.value = value
}
}
class Validation {
constructor(validator, description = 'invalid command') {
if (typeof validator === 'string') [description, validator] = [validator, description]
this.validator = validator
this.description = description
}
}
function argv() {
return typeof process === 'undefined' ? global.Bare.argv.slice(2) : process.argv.slice(2)
}
function command(name, ...args) {
if (
name !== null &&
typeof name === 'object' &&
Object.getPrototypeOf(name) === Object.prototype
) {
const arg = name
args.unshift(arg)
name = arg.name
}
const c = new Command(null, name)
c.add(...args)
return c
}
// deprecated, removed soon, use.hide()
function hiddenCommand(name, ...args) {
return command(name, ...args).hide()
}
function bail(fn) {
return new Data('bail', fn)
}
function sloppy(opts) {
return new Data('sloppy', opts)
}
function summary(desc) {
return new Data('summary', desc)
}
function description(desc, ...values) {
if (Array.isArray(desc)) return description(dedent(desc, values))
return new Data('description', desc)
}
function header(desc) {
return new Data('header', desc)
}
function footer(desc) {
return new Data('footer', desc)
}
// deprecated, removed soon, use .hide()
function hiddenFlag(help, description) {
return flag(help, description).hide()
}
function flag(help, description) {
return new Flag(help, description)
}
function rest(help, description) {
return new Rest(help, description)
}
function arg(help, description) {
return new Arg(help, description)
}
function validate(validator, description) {
return new Validation(validator, description)
}
function unindent(info) {
const lines = info.split('\n').filter(trimLine)
if (!lines.length) return ''
const indent = lines[0].match(/^(\s*)/)[1]
let s = ''
for (const line of lines) s += line.slice(indent.length).trimEnd() + '\n'
return s.trimEnd()
}
function trimLine(line) {
return line.trim()
}
function snakeToCamel(name) {
const parts = name.match(/([a-zA-Z0-9-]+)/)[1].split(/-+/)
for (let i = 1; i < parts.length; i++) {
parts[i] = parts[i].slice(0, 1).toUpperCase() + parts[i].slice(1)
}
return parts.join('')
}
function parseFlag(help) {
const parts = help.split(/[| ]/)
const result = {
longName: null,
shortName: null,
aliases: [],
boolean: true,
help,
value: null,
inverse: false,
valueRequired: false
}
for (const p of parts) {
result.inverse = p.startsWith('--no-')
if (p.startsWith('--')) {
const name = result.inverse ? trimFlag(p).slice(3) : trimFlag(p)
if (result.longName === null) result.longName = name
result.value = result.inverse
result.aliases.push(name)
continue
}
if (p.startsWith('-')) {
const name = trimFlag(p)
if (result.shortName === null) result.shortName = name
result.value = false
result.aliases.push(name)
continue
}
const valueRequired = p.startsWith('<')
const stringFlag = p.startsWith('[') || valueRequired
if (stringFlag) {
result.boolean = false
result.value = undefined
result.valueRequired = valueRequired
}
}
return result
}
function trimFlag(s) {
return s.replace(/(^[^0-9\w]+)|([^0-9\w]+$)/g, '')
}
function createBail(command, reason, flag, arg, err) {
return { command, reason, flag, arg, err }
}
function defaultFlag(name) {
return {
name,
aliases: [name],
boolean: false,
help: '',
description: '',
value: ''
}
}
function dedent(strings, values) {
const raw = String.raw(strings, ...values)
const lines = raw.split('\n')
let minIndent = Infinity
for (const line of lines) {
let i = 0
while (i < line.length && line[i] === ' ') i++
if (i < line.length) minIndent = Math.min(minIndent, i)
}
if (minIndent === Infinity) return raw.trim()
let result = ''
for (const line of lines) {
result += line.length >= minIndent ? line.slice(minIndent) + '\n' : '\n'
}
return result.trim()
}
function runValidation(v, c) {
try {
const isValid = v.validator({
args: c.args,
flags: c.flags,
positionals: c.positionals,
rest: c.rest,
indices: c.indices,
command: c
})
if (!isValid) {
const err = new Error(v.description)
err.code = 'ERR_INVALID'
err.bail = { reason: v.description }
throw err
}
return null
} catch (err) {
return createBail(c, err.bail?.reason ?? err.stack, err.bail?.flag, err.bail?.arg, err)
}
}
function runSync(c) {
try {
c._runner({
args: c.args,
flags: c.flags,
positionals: c.positionals,
rest: c.rest,
indices: c.indices,
argv: c.argv,
command: c
})
} catch (err) {
c.bail(createBail(c, err.stack, null, null, err))
}
}
async function runAsync(c) {
try {
await c._runner({
args: c.args,
flags: c.flags,
positionals: c.positionals,
rest: c.rest,
indices: c.indices,
argv: c.argv,
command: c
})
} catch (err) {
c.bail(createBail(c, err.stack, null, null, err))
}
}
class Bail extends Error {
name = 'Bail'
}