Socket
Socket
Sign inDemoInstall

jackspeak

Package Overview
Dependencies
Maintainers
1
Versions
58
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

jackspeak - npm Package Compare versions

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",

@@ -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.
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc