@benkenawell/parseargs
Version:
Utilize node's arg parsing function for command line utilities
180 lines (152 loc) • 5.51 kB
JavaScript
if (process.argv.length === 2) {
const { basename } = await import('node:path');
process.stdout.write(`
USAGE
Use "--" to separate arguments to this command from arguments you want to parse.
Passing no separator will print the created config to stdout.
--format [header|json] default: json. the output format of the command.
json is useful for jq, headers for awk
--config <string> JSON config to pass to nodejs parseArgs. Overridden by other options.
--option <key>=<type> easy way to set an option key for parsing. type can be "string" or "boolean"
--option <key>=<type>,<short> easy way to set an option key for parsing.
type can be "string" or "boolean".
short must be a single letter
--[no-]positional whether to allow positionals or not
--[no-]negative whether to allow negatives or not
--[no-]strict whether to set the config to strict or not.
Config Reference
If you'd like, you can write a json config
that conforms to the NodeJS parseArgs library. The other command line options take precedence over --config
Pass the options you want parsed as other arguments, after a "--".
Use jq to parse further on the command line
EXAMPLES
Example:
> ${basename(process.argv[1])} --option one=boolean,o --no-strict -- -otwo --three=four
Example:
> ${basename(process.argv[1])} --option one=string --no-strict -- --one two --three=four
Example:
> config=$(jq -n '{"options": {"test": {"type": "string"}}, "strict": false}')
> ${basename(process.argv[1])} --config "$config" -- --test "what a lovely day" | jq .
Example:
> config=$(jq -n '{"options": {"test": {"type": "string"}}, "strict": false}')
> args=$(${basename(process.argv[1])} --config "$config" -- "$@")
> jq .values.test <<<"$args"
Formats:
JSON { "values": {<key>: <value>}, "positionals": <array> }
HEADERS
<key>:<value>
<key>:<value>
<key>:<value>
positional one
positional two
...
Config Reference:
https://nodejs.org/api/util.html#utilparseargsconfig
`);
process.exit(0)
}
const { parseArgs } = await import('node:util')
// TODO: read from stdin if isTTY is falsy.
// remove the --config option? Then I could call it like =
// parseargs --option test=string,t | parseargs "$@" | jq .
// OR: parseargs "$@" <<<"$(parseargs --option test=string,t)" | jq .
function parseConfig(args) {
// parse config out of the arguments
const { values } = parseArgs({
allowNegative: true,
options: {
config: {
short: 'c',
type: "string",
},
option: {
short: "o",
type: "string",
multiple: true,
},
positional: {
type: 'boolean',
},
negative: {
type: 'boolean',
},
strict: {
type: 'boolean',
},
format: {
type: 'string',
short: 'f'
}
},
args
});
// merge the config together, preferring command line args over config arg
let config;
if (values.config) config = JSON.parse(values.config)
if (!config) config = {}
if (!config.options) config.options = {}
for (const option of (values.option ?? [])) {
const [key, optionString] = option.split("=")
const [type, short] = optionString.split(",");
// check type is a real type
if (!['string', 'boolean'].includes(type))
throw new Error(`Type of ${key} is incorrect`)
config.options[key] = { type }
// parse short option into config
if (!!short) {
if (short.length > 1) throw new Error(`Short option ${short} for option ${key} must be one character`)
config.options[key].short = short;
}
}
if (values.positional) config.allowPositionals = values.positional;
if (values.negative) config.allowNegative = values.negative;
if (values.strict) config.strict = values.strict;
return { format: values.format, config };
}
const splitIndex = process.argv.findIndex((val) => val === "--")
let configArgs;
if (splitIndex < 0) configArgs = process.argv.slice(2)
else configArgs = process.argv.slice(2, splitIndex)
let config, format;
try {
({ config, format } = parseConfig(configArgs));
} catch (err) {
process.stderr.write("Unable to parse your config\n")
if (typeof err === 'object' && !!err && 'message' in err)
process.stderr.write(`${err.message}\n`);
process.exit(1)
}
// if -- wasn't found, print the config out
if (splitIndex < 0) {
process.stdout.write(JSON.stringify(config))
if (process.stdout.isTTY) process.stdout.write('\n')
process.exit(0)
}
const args = process.argv.slice(splitIndex + 1);
if (args.length <= 0) {
process.stderr.write('no arguments provided\n')
process.exit(1)
}
try {
const output = parseArgs({ ...config, args })
// Format headers have values in header format. Positionals are separated by \n\n
// <option>: <value>
// <option>: <value>
//
// positional one
// positional two
if (format === 'headers') {
for (const [key, value] of Object.entries(output.values))
process.stdout.write(`${key}: ${value}\n`)
process.stdout.write('\n')
for (const value of output.positionals)
process.stdout.write(`${value}\n`)
} else {
process.stdout.write(JSON.stringify(output))
if (process.stdout.isTTY) process.stdout.write('\n')
}
} catch {
process.stderr.write("unable to parse your arguments. Check your config\n")
process.exit(1)
}