Comparing version 0.1.0 to 1.0.0
563
index.js
'use strict' | ||
// XXX help and usage output | ||
// XXX required options? or just attach a usage() method on result? | ||
@@ -19,12 +17,17 @@ | ||
const _flag = Symbol('flag') | ||
const _list = Symbol('list') | ||
const _opt = Symbol('opt') | ||
const flag = options => ({ | ||
[_env]: false, | ||
[_list]: false, | ||
...(options || {}), | ||
[_num]: false, | ||
[_opt]: false, | ||
[_flag]: true | ||
}) | ||
const isFlag = arg => arg[_flag] | ||
const _opt = Symbol('opt') | ||
const opt = options => ({ | ||
[_num]: false, | ||
[_list]: false, | ||
[_env]: false, | ||
...(options || {}), | ||
@@ -34,11 +37,37 @@ [_flag]: false, | ||
}) | ||
const isOpt = arg => arg[_opt] | ||
const _num = Symbol('num') | ||
const num = options => (opt({ | ||
...(options || {}), | ||
[_num]: true, | ||
})) | ||
const isNum = arg => arg[_num] | ||
const isArg = arg => isOpt(arg) || isFlag(arg) | ||
const _env = Symbol('env') | ||
const env = options => ({ | ||
[_flag]: false, | ||
[_list]: false, | ||
[_opt]: true, | ||
[_num]: false, | ||
...(options || {}), | ||
[_env]: true, | ||
}) | ||
const isEnv = arg => arg[_env] | ||
const _list = Symbol('list') | ||
const list = options => ({ | ||
[_flag]: false, | ||
[_opt]: true, | ||
[_num]: false, | ||
[_env]: false, | ||
...(options || {}), | ||
[_list]: true | ||
}) | ||
const isList = arg => arg[_list] | ||
const count = options => list(flag(options)) | ||
const isCount = arg => isList(arg) && isFlag(arg) | ||
@@ -54,150 +83,337 @@ const trim = string => string | ||
const ranUsage = Symbol('ranUsage') | ||
const usage = options => { | ||
options[ranUsage] = true | ||
const usageMemo = Symbol('usageMemo') | ||
const usage = j => { | ||
if (j[usageMemo]) | ||
return j[usageMemo] | ||
const ui = cliui() | ||
if (!/^Usage:/.test(j.help[0].text)) { | ||
ui.div('Usage:') | ||
ui.div({ | ||
text: `${$0} <options>`, | ||
padding: [0, 0, 1, 2] | ||
}) | ||
} | ||
let maxWidth = 0 | ||
const table = Object.keys(options).filter( | ||
k => k !== 'argv' && | ||
k !== 'help' && | ||
k !== 'usage' | ||
).map(k => { | ||
const arg = options[k] | ||
const val = arg[_opt] ? `[${arg.hint || k}]` : '' | ||
const short = arg.short ? ` -${arg.short}${val}` : '' | ||
const desc = trim(arg.description || '[no description provided]') | ||
const mult = arg[_list] ? `${ | ||
desc.indexOf('\n') === -1 ? '\n' : '\n\n' | ||
}Can be set multiple times` : '' | ||
// XXX negated counters and shorthands | ||
const row = [ | ||
`--${k}${val ? `=${val}`: ''}${short}`, | ||
`${desc}${mult}` | ||
] | ||
maxWidth = Math.min(30, Math.max(row[0].length + 4, maxWidth)) | ||
return row | ||
j.help.forEach(row => { | ||
if (row.left) | ||
maxWidth = Math.min(30, Math.max(row.left.length + 4, maxWidth)) | ||
}) | ||
const ui = cliui() | ||
ui.div('Usage:') | ||
ui.div({ | ||
text: (options.usage || `${$0} <options>`), | ||
padding: [0, 0, 1, 2] | ||
j.help.forEach(row => { | ||
if (row.left) { | ||
ui.div({ | ||
text: row.left, | ||
padding: [0, 2, 0, 2], | ||
width: maxWidth, | ||
}, { text: row.text }) | ||
ui.div() | ||
} else | ||
ui.div(row) | ||
}) | ||
if (options.help) | ||
ui.div({text: options.help, paddign: [1, 0, 0, 0]}) | ||
return j[usageMemo] = ui.toString() | ||
} | ||
if (table.length) | ||
ui.div({text: 'Options:', padding: [1, 0, 1, 0]}) | ||
// parse each section | ||
// Resulting internal object looks like: | ||
// { | ||
// help: [ | ||
// { text, padding, left }, ... | ||
// ], | ||
// // single-char shorthands | ||
// shortOpts: { char: name, ... } | ||
// shortFlags: { char: name, ... } | ||
// options: { all opts and flags } | ||
// main: mainFn or null, | ||
// argv: argument list being parsed, | ||
// result: parsed object passed to main function and returned | ||
// } | ||
const jack = (...sections) => execute(parse_(buildParser({ | ||
help: [], | ||
shortOpts: {}, | ||
shortFlags: {}, | ||
options: {}, | ||
result: { _: [] }, | ||
main: null, | ||
argv: null, | ||
env: null, | ||
[usageMemo]: false, | ||
}, sections))) | ||
table.forEach(row => { | ||
ui.div({ | ||
text: row[0], | ||
width: maxWidth, | ||
padding: [0, 2, 0, 2] | ||
}, { text: row[1] }) | ||
ui.div() | ||
}) | ||
return ui.toString() | ||
const execute = j => { | ||
if (j.result.help) | ||
console.log(usage(j)) | ||
else if (j.main) | ||
j.main(j.result) | ||
return j.result | ||
} | ||
const jack = options_ => { | ||
// don't monkey with the originals | ||
const options = { ...options_ } | ||
const buildParser = (j, sections) => { | ||
sections.forEach(section => { | ||
if (Array.isArray(section)) | ||
section = { argv: section } | ||
if (section.argv && !isArg(section.argv)) { | ||
!j.argv || assert(false, 'argv specified multiple times') | ||
j.argv = section.argv | ||
} | ||
// validate the options | ||
const opts = {} | ||
const flags = {} | ||
const short = {} | ||
const shortFlags = {} | ||
const shortOpts = {} | ||
const result = { _ : [] } | ||
if (section.env && !isArg(section.env)) { | ||
!j.env || assert(false, 'env specified multiple times\n' + | ||
'(did you set it after defining some environment args?)') | ||
j.env = section.env | ||
} | ||
const names = Object.keys(options) | ||
for (let n = 0; n < names.length; n++) { | ||
const name = names[n] | ||
switch (name) { | ||
case 'main': | ||
assert(typeof options[name] === 'function', | ||
`${name} should be a function, if specified`) | ||
continue | ||
if (section.usage && !isArg(section.usage)) { | ||
const val = section.usage | ||
typeof val === 'string' || Array.isArray(val) || assert(false, | ||
'usage must be a string or array') | ||
j.help.push({ text: 'Usage:' }) | ||
j.help.push.apply(j.help, [].concat(val).map(text => ({ | ||
text, padding: [0, 0, 0, 2] | ||
}))) | ||
j.help.push({ text: '' }) | ||
} | ||
case '_': | ||
assert(false, '_ is reserved for positional arguments') | ||
if (section.description && !isArg(section.description)) { | ||
typeof section.description === 'string' || assert(false, | ||
'description must be string') | ||
j.help.push({ | ||
text: trim(`${section.description}:`), | ||
padding: [0, 0, 1, 0] | ||
}) | ||
} | ||
case 'argv': | ||
assert(Array.isArray(options[name]), | ||
`${name} should be the array of arguments, if specified`) | ||
continue | ||
if (section.help && !isArg(section.help)) { | ||
typeof section.help === 'string' || assert(false, 'help must be a string') | ||
j.help.push({ text: trim(section.help) + '\n' }) | ||
} | ||
case 'usage': | ||
case 'help': | ||
assert(typeof options[name] === 'string', | ||
`${name} should be a string, not an argument type`) | ||
continue | ||
if (section.main && !isArg(section.main)) | ||
addMain(j, section.main) | ||
default: // ok it's an argument type! | ||
const arg = options[name] | ||
assert(arg[_flag] || arg[_opt], `${name} neither flag nor opt`) | ||
if (arg[_flag]) { | ||
if (name.substr(0, 3) !== 'no-') { | ||
assert(!options[`no-${name}`], | ||
`flag '${name}' specified, but 'no-${name}' already defined`) | ||
// just to pick up the description and short arg | ||
options[`no-${name}`] = flag({ | ||
...(arg.negate || {}), | ||
[_list]: arg[_list] | ||
}) | ||
names.push(`no-${name}`) | ||
} | ||
} | ||
if (arg.short) { | ||
assert(arg.short.length === 1, | ||
`${name} short alias must be 1 char, found '${arg.short}'`) | ||
assert(!short[arg.short], | ||
`short ${arg.short} used for ${name} and ${short[arg.short]}`) | ||
assert(arg.short !== 'h', | ||
`${name} using 'h' short arg, reserved for --help`) | ||
assert(arg.short !== '?', | ||
`${name} using '?' short arg, reserved for --help`) | ||
!section._ || assert(false, '_ is reserved for positional arguments') | ||
short[arg.short] = name | ||
if (arg[_flag]) | ||
shortFlags[arg.short] = name | ||
else | ||
shortOpts[arg.short] = name | ||
} | ||
if (arg[_flag]) { | ||
flags[name] = arg | ||
} else { | ||
opts[name] = arg | ||
} | ||
const names = Object.keys(section) | ||
for (let n = 0; n < names.length; n++) { | ||
const name = names[n] | ||
const val = section[name] | ||
if (!arg.alias && (!arg[_flag] || name.slice(0, 3) !== 'no-')) { | ||
result[name] = arg[_list] ? ( | ||
arg[_flag] ? 0 : [] | ||
) : arg[_flag] ? arg.default || false | ||
: arg.default | ||
if (isEnv(val)) | ||
addEnv(j, name, val) | ||
else if (isArg(val)) | ||
addArg(j, name, val) | ||
else { | ||
switch (name) { | ||
case 'argv': | ||
case 'description': | ||
case 'usage': | ||
case 'help': | ||
case 'main': | ||
case 'env': | ||
continue | ||
default: | ||
assert(false, `${name} not flag, opt, or env`) | ||
} | ||
continue | ||
} | ||
} | ||
}) | ||
// if not already mentioned, add the note about -h and `--` ending parsing | ||
if (!j.options.help) | ||
addArg(j, 'help', flag({ description: 'Show this helpful output' })) | ||
if (!j.options['--']) { | ||
addHelpText(j, '', flag({ | ||
description: `Stop parsing flags and options, treat any additional | ||
command line arguments as positional arguments.` | ||
})) | ||
} | ||
// start the parsing | ||
const args = [...(options.argv || process.argv)] | ||
return j | ||
} | ||
if (args[0] === process.execPath) { | ||
args.shift() | ||
if (args[0] === require.main.filename) | ||
args.shift() | ||
const envToNum = (name, spec) => e => { | ||
if (e === '') | ||
return undefined | ||
return toNum(e, `environment variable ${name}`, spec) | ||
} | ||
const envToBool = name => e => { | ||
e === '' || e === '1' || e === '0' || typeof e === 'number' || assert(false, | ||
`Environment variable ${name} must be set to 0 or 1 only`) | ||
return e === '' ? false : !!+e | ||
} | ||
const countBools = l => l.reduce((v, a) => v ? a + 1 : a - 1, 0) | ||
const addEnv = (j, name, val) => { | ||
assertNotDefined(j, name) | ||
if (!j.env) | ||
j.env = process.env | ||
const def = val.default | ||
const has = Object.prototype.hasOwnProperty.call(j.env, name) | ||
const e = has && j.env[name] !== '' ? j.env[name] | ||
: def !== undefined ? def | ||
: '' | ||
if (isList(val)) { | ||
val.delimiter || assert(false, `env list ${name} lacks delimiter`) | ||
if (!has) | ||
j.result[name] = [] | ||
else { | ||
const split = e.split(val.delimiter) | ||
j.result[name] = isFlag(val) ? countBools(split.map(envToBool(name))) | ||
: isNum(val) ? split.map(envToNum(name, val)).filter(e => e !== undefined) | ||
: split | ||
} | ||
} else if (isFlag(val)) | ||
j.result[name] = envToBool(name)(e) | ||
else if (isNum(val)) | ||
j.result[name] = envToNum(name, val)(e) | ||
else | ||
j.result[name] = e | ||
addHelpText(j, name, val) | ||
} | ||
const assertNotDefined = (j, name) => | ||
!j.options[name] && !j.shortOpts[name] && !j.shortFlags[name] || | ||
assert(false, `${name} defined multiple times`) | ||
const addArg = (j, name, val) => { | ||
assertNotDefined(j, name) | ||
if (isFlag(val)) | ||
addFlag(j, name, val) | ||
else | ||
addOpt(j, name, val) | ||
} | ||
const addOpt = (j, name, val) => { | ||
addShort(j, name, val) | ||
j.options[name] = val | ||
addHelpText(j, name, val) | ||
if (!val.alias) | ||
j.result[name] = isList(val) ? [] : val.default | ||
} | ||
const addFlag = (j, name, val) => { | ||
if (name === '--') { | ||
addHelpText(j, '', flag(val)) | ||
j.options['--'] = true | ||
return | ||
} | ||
// the positional args passed on at the end | ||
const argv = result._ = [] | ||
if (name === 'help' && !val.short) | ||
val.short = 'h' | ||
for (let i = 0; i < args.length; i++) { | ||
const arg = args[i] | ||
const negate = name.substr(0, 3) === 'no-' | ||
// aliases can't be negated | ||
if (!negate && !val.alias) | ||
!j.options[`no-${name}`] || assert(false, | ||
`flag '${name}' specified, but 'no-${name}' already defined`) | ||
addShort(j, name, val) | ||
j.options[name] = val | ||
if (!negate && !val.alias) | ||
j.result[name] = isList(val) ? 0 : (val.default || false) | ||
addHelpText(j, name, val) | ||
// pick up the description and short arg | ||
if (!negate && !val.alias) | ||
addFlag(j, `no-${name}`, flag({ | ||
description: `${ | ||
isCount(val) ? 'decrement' : 'switch off' | ||
} the --${name} flag`, | ||
hidden: val.hidden, | ||
...(val.negate || {}), | ||
[_list]: isList(val) | ||
})) | ||
} | ||
const addHelpText = (j, name, val) => { | ||
// help text | ||
if (val.hidden) | ||
return | ||
const desc = trim(val.description || ( | ||
val.alias ? `Alias for ${[].concat(val.alias).join(' ')}` | ||
: '[no description provided]' | ||
)) | ||
const mult = isList(val) ? `${ | ||
desc.indexOf('\n') === -1 ? '\n' : '\n\n' | ||
}Can be set multiple times` : '' | ||
const text = `${desc}${mult}` | ||
const hint = val.hint || name | ||
const short = val.short ? ( | ||
isFlag(val) ? `, -${val.short}` | ||
: `, -${val.short}<${hint}>` | ||
) : '' | ||
const left = isEnv(val) ? name | ||
: isFlag(val) ? `--${name}${short}` | ||
: `--${name}=<${hint}>${short}` | ||
j.help.push({ text, left }) | ||
} | ||
const addShort = (j, name, val) => { | ||
if (!val.short) | ||
return | ||
assertNotDefined(j, val.short) | ||
val.short !== 'h' || name === 'help' || assert(false, | ||
`${name} using 'h' short val, reserved for --help`) | ||
if (isFlag(val)) | ||
j.shortFlags[val.short] = name | ||
else | ||
j.shortOpts[val.short] = name | ||
} | ||
const addMain = (j, main) => { | ||
typeof main === 'function' || assert(false, 'main must be function') | ||
!j.main || assert(false, 'main function specified multiple times') | ||
j.main = result => { | ||
main(result) | ||
return result | ||
} | ||
} | ||
const getArgv = j => { | ||
const argv = [...(j.argv || process.argv)] | ||
if (argv[0] === process.execPath) { | ||
argv.shift() | ||
if (argv[0] === require.main.filename) | ||
argv.shift() | ||
} | ||
return argv | ||
} | ||
const toNum = (val, key, spec) => { | ||
!isNaN(val) || assert(false, `non-number '${val}' given for numeric ${key}`) | ||
val = +val | ||
isNaN(spec.max) || val <= spec.max || assert(false, | ||
`value ${val} for ${key} exceeds max (${spec.max})`) | ||
isNaN(spec.min) || val >= spec.min || assert(false, | ||
`value ${val} for ${key} below min (${spec.min})`) | ||
return val | ||
} | ||
const parse_ = j => { | ||
const argv = getArgv(j) | ||
for (let i = 0; i < argv.length; i++) { | ||
const arg = argv[i] | ||
if (arg.charAt(0) !== '-' || arg === '-') { | ||
result._.push(arg) | ||
j.result._.push(arg) | ||
continue | ||
@@ -207,4 +423,4 @@ } | ||
if (arg === '--') { | ||
result._ = result._.concat(args.slice(i + 1)) | ||
i = args.length | ||
j.result._ = j.result._.concat(argv.slice(i + 1)) | ||
i = argv.length | ||
continue | ||
@@ -218,6 +434,6 @@ } | ||
const fc = arg.charAt(f) | ||
const sf = shortFlags[fc] | ||
const so = shortOpts[fc] | ||
const sf = j.shortFlags[fc] | ||
const so = j.shortOpts[fc] | ||
if (sf) | ||
expand.push('--' + sf) | ||
expand.push(`--${sf}`) | ||
else if (so) { | ||
@@ -228,10 +444,10 @@ const soslice = arg.slice(f + 1) | ||
expand.push('--' + so + soval) | ||
expand.push(`--${so}${soval}`) | ||
f = arg.length | ||
} else if (arg !== '-' + fc) | ||
} else if (arg !== `-${fc}`) | ||
// this will trigger a failure with the appropriate message | ||
expand.push('-' + fc) | ||
expand.push(`-${fc}`) | ||
} | ||
if (expand.length) { | ||
args.splice.apply(args, [i, 1].concat(expand)) | ||
argv.splice.apply(argv, [i, 1].concat(expand)) | ||
i-- | ||
@@ -244,31 +460,27 @@ continue | ||
const literalKey = argsplit.shift() | ||
const key = literalKey.replace(/^--?/, '') | ||
let val = argsplit.length ? argsplit.join('=') : null | ||
if (literalKey === '-h' || | ||
literalKey === '--help' || | ||
literalKey === '-?') { | ||
if (!options[ranUsage]) | ||
console.log(usage(options)) | ||
continue | ||
} | ||
// check if there's a >1 char shortopt/flag for this key, | ||
// and de-reference it as an alias | ||
const spec = options[key] | ||
const k = literalKey.replace(/^--?/, '') | ||
// pick up shorts that aren't single-char | ||
const key = j.shortOpts[k] || j.shortFlags[k] || k | ||
let val = argsplit.length ? argsplit.join('=') : null | ||
if (!spec) | ||
throw new Error(`invalid argument: ${literalKey}`) | ||
const spec = j.options[key] | ||
if (spec[_flag] && val !== null) | ||
throw new Error(`value provided for boolean flag: ${key}`) | ||
spec || assert(false, `invalid argument: ${literalKey}`) | ||
!isFlag(spec) || val === null || assert(false, | ||
`value provided for boolean flag: ${key}`) | ||
if (spec[_opt] && val === null) { | ||
val = args[++i] | ||
if (val === undefined) | ||
throw new Error(`no value provided for option: ${key}`) | ||
if (isOpt(spec) && val === null) { | ||
val = argv[++i] | ||
val !== undefined || assert(false, | ||
`no value provided for option: ${key}`) | ||
} | ||
if (spec.alias) { | ||
const alias = spec[_flag] ? spec.alias | ||
const alias = isFlag(spec) ? spec.alias | ||
: [].concat(spec.alias).map(a => a.replace(/\$\{value\}/g, val)) | ||
args.splice.apply(args, [i, 1].concat(alias)) | ||
argv.splice.apply(argv, [i, 1].concat(alias)) | ||
i-- | ||
@@ -278,27 +490,44 @@ continue | ||
const negate = spec[_flag] && key.substr(0, 3) === 'no-' | ||
const negate = isFlag(spec) && key.substr(0, 3) === 'no-' | ||
const name = negate ? key.substr(3) : key | ||
if (isNum(spec)) | ||
val = toNum(val, `arg ${literalKey}`, spec) | ||
if (spec[_list]) { | ||
if (spec[_opt]) { | ||
result[name].push(val) | ||
if (isList(spec)) { | ||
if (isOpt(spec)) { | ||
j.result[name].push(val) | ||
} else { | ||
result[name] = result[name] || 0 | ||
j.result[name] = j.result[name] || 0 | ||
if (negate) | ||
result[name]-- | ||
j.result[name]-- | ||
else | ||
result[name]++ | ||
j.result[name]++ | ||
} | ||
} else { | ||
// either flag or opt | ||
result[name] = spec[_flag] ? !negate : val | ||
j.result[name] = isFlag(spec) ? !negate : val | ||
} | ||
} | ||
if (options.main) | ||
options.main(result) | ||
Object.defineProperty(j.result._, 'usage', { | ||
value: () => console.log(usage(j)) | ||
}) | ||
Object.defineProperty(j.result._, 'parsed', { value: argv }) | ||
return result | ||
return j | ||
} | ||
module.exports = { jack, flag, opt, list, count } | ||
// just parse the arguments and return the result | ||
const parse = (...sections) => parse_(buildParser({ | ||
help: [], | ||
shortOpts: {}, | ||
shortFlags: {}, | ||
options: {}, | ||
result: { _: [] }, | ||
main: null, | ||
argv: null, | ||
env: null, | ||
[usageMemo]: false, | ||
}, sections)).result | ||
module.exports = { jack, flag, opt, list, count, env, parse, num } |
{ | ||
"name": "jackspeak", | ||
"version": "0.1.0", | ||
"version": "1.0.0", | ||
"description": "A very strict and proper argument parser.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
238
README.md
@@ -7,5 +7,227 @@ # jackspeak | ||
Pass one or more objects into the exported `jack(...)` function. Each | ||
object can have the following fields, and would typically represent a | ||
"section" in a usage/help output. | ||
Using multiple sections allows for using some of the "special" fields | ||
as argument names as well; just put them in different sections. | ||
- `main` Function | ||
May only appear once. If provided, will be called with the resulting | ||
parsed object. | ||
- `usage` String or Array | ||
The `Usage: ...` bits that go at the top of the help output | ||
- `description` String | ||
A heading for the section. Something like `File Options` to | ||
preface all of the options for working with files. | ||
- `help` String | ||
A longer-form (multi-paragraph) section of text that explains the | ||
stuff in more details. | ||
- `argv` Array | ||
A list of arguments to parse. If not provided, jackspeak will | ||
pull form `process.argv`. It knows how to skip over the node binary | ||
and main script filename. | ||
If a section is just an array, then it'll be treated as the argv. | ||
- `env` Object | ||
A set of key-value pairs to pull environment variables from. If | ||
not specified, jackspeak will pull from `process.env`. | ||
Note that environs are parsed and loaded right away when they are | ||
defined, so you must put `env` on a jackspeak object before | ||
definint any environent | ||
- One or more argument definition objects. These can be formed using | ||
the functions exported by `require('jackspeak')`. The key is the | ||
full canonical name of the argument as it appears in the parsed | ||
result set. | ||
Note that the `--help` flag with the `-h` shorthand will be added | ||
by default, and that `--` will always stop parsing and treat the | ||
rest of the argv as positional arguments. However, `help` and | ||
`--` _may_ be added to a jack section to customize the usage text. | ||
All types can have the following options: | ||
- `description` - Help text for this option. | ||
- `hidden` - Do not show this value in the help output. | ||
The types are: | ||
- `flag(options)` - A boolean value which can be set or unset, but | ||
not given a value. | ||
Flags can have the following options: | ||
- `default` - Either `true` or `false`. If unspecified, flags | ||
default to `false`. | ||
- `short` - A "short" form of the value which is indicated | ||
with a single dash. If `short` is a single character, then | ||
it can be combined gnu-style with other short flags. | ||
- `negate` - An object defining how the `--no-<whatever>` form | ||
of the flag works. It can have any options that would be | ||
passed to a flag, other than `negate`. | ||
For example, it can specify the help text for the negated | ||
form, or provide a different shorthand character. So, for | ||
example, `--color` could have `-c` as a shorthand, and | ||
`--no-color` could be shorthanded to `-C`. | ||
- `alias` - Either a string or array of what this flag expands | ||
to. This means that the flag key won't have a value, but | ||
will instead be expanded to its alias. To expand an alias | ||
to multiple arguments, use an array. For example, in the | ||
`rsync` program, `-m` expands to `-r -N -l inf | ||
--no-remove-listing` | ||
- `opt(options)` - An argument which takes a value. | ||
Opts can have the following options: | ||
- `default` - A default value. If unspecified, opts default | ||
to `undefined`. | ||
- `alias` - A string or array of options that this option | ||
expands to when used. This works the same as flag aliases, | ||
with the exception that you may include the string | ||
`${value}` in the alias string(s) to substitute in the value | ||
provided to this opt. | ||
For example, `--big=<n>` could be an alias for | ||
`--font-size=<n> --bold` by doing: | ||
```js | ||
jack({ | ||
big: opt({ | ||
alias: ['--font-size=${value}', '--bold'] | ||
}) | ||
}) | ||
``` | ||
- `hint` - A string to use in the help output as the value | ||
provided to the opt. For example, if you wanted to print | ||
`--output=<file>`, then you'd set `hint: 'file'` here. | ||
Defaults to the opt name. | ||
- `short` - A "short" form of the opt which is indicated | ||
with a single dash. If `short` is a single character, then | ||
it can be combined gnu-style with short flags, and take a | ||
value without an `=` character. | ||
For example, in [tap](https://www.node-tap.org), `-bRspec` | ||
is equivalent to `--bail --reporter=spec`. | ||
- `num(options)` - An `opt` that is a number. This will be | ||
provided in the result as an actual number (rather than a | ||
string) and will raise an error if given a non-numeric value. | ||
This is numericized by using the `+` operator, so any | ||
JavaScript number represenation will do. | ||
All of the `opt()` options are supported, plus these: | ||
- `min` - A number that this value cannot be smaller than. | ||
- `max` - A number that this value cannot be larger than. | ||
- `list(options)` - An option which can take multiple values by | ||
being specified multiple times, and is represented in the result | ||
object as an array of values. If the list is not present in the | ||
arguments, then it will be an empty array. | ||
- `count(options)` - A flag which can be set multiple times to | ||
increase a value. Unsetting decrements the value, setting | ||
increments it. This can be useful for things like `-v` to set a | ||
verbosity level, or `-d` to set a debug level. | ||
Counts always default to 0. | ||
Note that a `count` is actually a flag that can be set | ||
multiple times. Thus, it is a composition of the `list` and | ||
`flag` types. | ||
- `env(options)` - An environment variable that the program is | ||
interested in. | ||
All environment variables will be present in the result | ||
object. `env()` can be composed with other types to change | ||
how the environment variable is handled. | ||
- Compose with `flag()` to define an environment variable that | ||
is set to `'1'` to mean `true`, or `'0'` or `''` to mean | ||
`false`. Presented in the result object as a boolean value. | ||
For example: | ||
```js | ||
jack({ | ||
FOO: env(flag({ | ||
description: 'Set to "1" to enable the foo flag' | ||
})) | ||
}) | ||
``` | ||
- Compose with `list()` to define an environment variable that | ||
is set to multiple values separated by a delimiter. For | ||
example: | ||
```js | ||
jack({ | ||
NODE_DEBUG: env(list({ | ||
delimiter: ',', | ||
description: 'Define which bits to debug' | ||
})) | ||
}) | ||
``` | ||
This can be further composed with `num` to pass in a list | ||
of numbers separated by a delimiter. | ||
When composed with `count` (which is the composition of | ||
`list` and `flag`), you would pass in a delimited list of | ||
`1` and `0` characters, and it'd count up the `1` values. | ||
I don't know why you'd ever do this, but it works. | ||
- Compose with `num()` to parse the environ as a numeric | ||
value, and raise an error if it is non-numeric. | ||
### Type Composition | ||
Compose types by applying more than one function to the arg | ||
definition options. For example, for a numeric environment | ||
variable, you can do: | ||
```js | ||
const { jack, flag, opt, list, count } = require('jackspeak') | ||
jack({ | ||
HOW_MANY_FOOS: env(num({ | ||
description: 'set to define the number of foos' | ||
max: 10, | ||
min: 2, | ||
default: 5, | ||
})) | ||
}) | ||
``` | ||
The order of composition does not matter in normal cases, but note | ||
that some compositions will contradict one another. For example, | ||
composing `flag` (an argument that does not take a value) with `opt` | ||
(an argument that _does_ take a value) will result in the outermost | ||
function taking precedence. | ||
## Some Example Code | ||
```js | ||
const { jack, flag, opt, list, count, num } = require('jackspeak') | ||
jack({ | ||
@@ -62,5 +284,11 @@ // Optional | ||
// Options that take a value are specified with `opt()` | ||
jobs: opt({ | ||
reporter: opt({ | ||
short: 'R', | ||
description: 'the style of report to display', | ||
}) | ||
// if you want a number, say so, and jackspeak will enforce it | ||
jobs: num({ | ||
short: 'j', | ||
description: 'number of jobs to run in parallel', | ||
description: 'how many jobs to run in parallel', | ||
default: 1 | ||
@@ -77,3 +305,3 @@ }), | ||
// you can also set defaults with an environ of course | ||
timeout: opt({ | ||
timeout: num({ | ||
short: 't', | ||
@@ -139,2 +367,2 @@ default: +process.env.TAP_TIMEOUT || 30, | ||
flexible. "Jackspeak" is the slang of the royal navy. This module | ||
does not have all the features, and is rigid by design. | ||
does not have all the features. It is declarative and rigid by design. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
26210
444
1
365
2
1