I wrote this library because I like unambiguity of the GNU style (and terseness of the POSIX style) of the gnu/linux ecosystem: I'm not interested in a parser that speaks --name val
instead of --name=val
for the former, and for the latter: I'd rather write -abc
than -a -b -c
.
Usage
var args = require('@ashnazg/args');
args.help.summary = 'SUMMARY TEXT';
args.help.noun = 'FISH';
args.define({gnu: 'name', type: 'string', desc: 'helpful stuff', default: 'trout'});
args.define({gnu: 'count', type: 'int', desc: 'helpful stuff', default: 42});
args.define({gnu: 'weight', type: 'number', desc: 'helpful stuff', default: 4.2});
args.define({gnu: 'kingdom', type: ['animal', 'vegetable'], default: 'mineral'}); // note that validators don't run on defaults, just user inputs.
args.define({gnu: 'male', type: 'flag', desc: 'helpful stuff'}); // defaults really shouldn't be used on flags.
args.define({gnu: 'nicknames', type: 'csv', desc: 'helpful stuff', default: ['flipper', 'fishstick']});
args.define({gnu: 'manifest', type: 'file_in', desc: 'helpful stuff'});
args.define({gnu: 'report', type: 'file_out', desc: 'helpful stuff', default: `report-${new Date().toISOString()}.csv`});
var {params, opts} = args();
console.log(opts.kingdom);
params.map(console.log):
use dash-dash to stop interpreting options
This library follows the convention that --
will cause all following terms to be parameters even if they look like options.
cli -a -- -b
results in option a
being turned on, and the one param is -b
.
later options overwrite early options
In the unixy world, most options follow the convention of allowing you to overrule an earlier setting: cli --mode=foo ...[many options and params]... --mode=baz
will pretend like foo never happened.
This is so that you can write stuff like aliases without needing a bunch of boilerplate wrapper code to allow you to turn an option off:
alias prettyprint="ls --color=always"
prettyprint /root/ --color=never
But in other cases, all values on the command line are preserved and processed. While this library defaults to 'overwrite', there's a multi
flag in define() to switch to preserving all. (And type:csv has this behavior implicitly.)
output
normal
If there's no --help
or errors, args() returns {opts:
For legacy support, you can also get at opts and params under their old weird name: args.opts
and args.params
bad inputs
If there was an unrecognized option or a failed validator, args() will print the error and exit(1).
--help
The library constructs a --help handler for you, using the defines(), plus summary
and noun
. If the caller adds --help
args() will print that and exit(0);
The help page generated by the above sample setup looks like:
USAGE: yourscriptname [-opts] FISH
SUMMARY TEXT
-n, --name=STRING helpful stuff
-c, --count=INT helpful stuff
-w, --weight=NUMBER helpful stuff
-k, --kingdom=ENUM one of: animal, vegetable
-m, --male helpful stuff
--nicknames=CSV helpful stuff
--manifest=FILE_IN helpful stuff
-r, --report=FILE_OUT helpful stuff
Note that posix names weren't added for nicknames or manifest; that's because there already was a gnu param that had used that first letter. You can add a posix: 'Z'
member to the define() config to pick an arbitrary letter.
Release 2.5.2
- bugfix:
default: ''
is no longer ignored.
Release 2.5.1
"--help" system no longer forces exitCode to be zero. This is in support of the following etiquette:
tool --help
is not a failed command.tool --key=nonsense
could just bail out, but for errors that are directly related to missing or invalid options use, I like printing the usage guide as well:FAIL: option A requires option B to be set USAGE: tool .... (blah blah blah; everything you see on --help)
- On error, (user-input or otherwise) the unixy thing to do is to return an error status.
- To support printing help on user input errors, args(['--help']) can be used, and as of 2.5.1, the following recipe doesn't incorrectly report "status=0"
function dieNoisily(why) { console.error(why); if (process.stdout.isTTY) { console.log(''); process.exitCode = 1; args(['--help']); } else { process.exit(1); } }
Release 2.5
Now supports showing examples below the option table:
args.help.script = 'appname';
args.help.noun = 'paramthing';
args.help.summary = 'does things with things';
args.help.examples = `
use thing as such
or like this`;
args.define({gnu: 'slicer'});
args(['--help']);
Will output:
USAGE: appname [-opts] paramthing
does things with things
-s, --slicer=STRING
EXAMPLES:
use thing as such
or like this
Release 2.4
- new optional key:
{set: 'foo'}
will cause all options with that key to output to 'foo' instead of their own name.- use cases:
--quiet --verbose --quiet
-> 'quiet 1', not 'quiet 2 verbose 1' or--markdown vs --jsonl
- 2.4.1 bugfix: the above didn't work when wired up backwards.
- use cases:
- tweak: you can disable auto-posix-naming by passing a falsy but !== undefined value.
- tweak: an explicit {posix: 'k'} overrides any previous opt keyed to that. (this is intended to override automatically selected ones.)
Release 2.3
- new {max: int} option for flags:
define({posix: 'v', type: 'flag', max: 2})
means thatcli -v -v -v
will only report two levels of verbosity instead of three.
Release 2.2
- unset flag options now return 0 instead of undefined
- bugfix: args.defaults was being polluted by user's actual choices; they're kept distinct now.
Release 2.1.1
- exposed args.UserError so I can instanceof in the client tools
Release 2.1.0
- Fixed a few bugs in args.load.{json,raw,lines,yaml} that was making that family of utils dead broken:
- unnecessary async keyword
- missing set-handling function
- reprocess wasn't calling the hook in set mode.
Release 2.0.1
removed erroneous line from documentation.
Release 2.0.0
- added "default:" and "env:" and refactored "multi:"
- breaking changes
- the posix defaulting behavior has reversed from 'later defines() trump early defines()' to first-come-first serve.
- You're app is immune to this change if you're avoiding collisions by either: specifying any colliding defines() with explicit posix fields or by not having params with the same first letter.
- Otherwise, this change could make posix-using calls to your script change behavior.
- In that case, you should add
posix:
fields to all your defines to pin the legacy choices in place, instead of using auto-picked posix letters.
- args() now returns
{params, opts}
and not justopts
- the posix defaulting behavior has reversed from 'later defines() trump early defines()' to first-come-first serve.
- added args.load.json and the others.
Release 1.1.0
v1.1.0 now allows you to define() your CLI options using {module: style} instead of the very weird positional style used til now. (Weird in that when you send less than all parameters, the way the library maps your actual params to its various formal signatures crosses even my eyes, and I wrote it.) That weird style is pushing signature overloads so deep into stupid-clever territory that it should burn, but I'm not removing support for legacy usage til either a future 2.0.0.
New signature
args.define({gnu, posix, validator, desc, suffix});
Config Defaults
- gnu is not optional; this is the name of the param in the output object as well as the --name form.
- posix will default to the first letter in 'gnu', unless that letter's already taken by a previously defined option, in which case it won't default to having a posix name at all.
- validator defaults to string.
- type is a synonym for 'validator'
- desc defaults to an empty string
- suffix is used during --help's option table printing. It defaults to a human friendly label for your validator (if you're not using a custom function validator.) Specifying suffix yourself means that --help will use
- loader defaults to disabled. if it's one of
json/yaml/lines/raw
, then the option's value will be run throughargs.load[loader]()
- multi defaults to false. if it's true, then the option will preserve all user inputs instead of having "last option wins"
- env defaults to disabled. if set to a string, then this option will default to that environment variable.
- if default and env are both present, it'll try to load the env first, and use the default if it's not set (or is empty string.)
- default sets the default value used if the user does not specify that option. The default default is:
- flags: 0
- csvs or where multi=true: []
- everything else: undefined
validators
- a validator can be a:
- function whose signature works with (user_provided_string_value, args_lib, invoked_as, dashes_used) (these last two are really there for the convenience of auto generating error messages that correctly reflect what the user said -- you don't want your -cFuzzbutt validation to say "error: --cat=Fuzzbutt is a dumb name" when the user didn't say --cat=.)
- a js typeof name like 'string' or 'number'
- one of the args.js defined types: 'int', 'flag', 'file_in', 'file_out', 'csv'
- an enum (expressed as a list of stringvalues: ['foo','bar'] means that --param=foo will be accepted but --param=baz will exit with an error message.)
flag
Flags don't accept values. The value of the option after parse is the count of times the flag was set. This allows cli -v -v
to be distinguished from cli -v
if needed.
Flags are allowed to be clumped together posix-style: -abc
could be three flags, in which case it's the same meaning as -a -b -c
, or b
could be a non-flag option, in which case option b has a value of c, making -abc
the same as -a -bc
. If -b
is the short form of your app's --bees=STRING
, then -abc
is parsed as equivalent to -a --bees=c
csv
- this has nothing to do with files; I literally mean 'comma separated values' here. An option of this type accepts one or more strings; it treats both of the following user behaviors as the same:
clitool --entry=a --entry=b
clitool --entry=a,b
This of course means you can't use validator:'csv'
if you want your entry values to include commas, as this will treat them as delimiters.
file_in and file_out
- file_in will error out if the file is not present or accessible
- file_out has no more validation behavior than a 'string' option would, BUT the --help display will use FILE_OUT as the value placeholder instead of STRING.
file_in utility
Because I'm often using file_in to read config files, I have some common file load-and-parse utilities. (If your file is too big to just pull into memory, don't use these.)
If a file a user specified
args.load.json('conf.json').then(obj => console.log(obj));
args.load.lines('file').then(lines => lines.map(console.log));
args.load.yaml('file.yml').then(yaml => console.log(yaml));
args.load.raw('file').then(rawstring => console.log("the whole file:", rawstring));
Each format() can take either a single filename as above, or an array of them, in which case it returns a map of filename:contents
args.load.json(['conf.json', 'manifest.json']).then(confs => {
console.log(confs['conf.json'].port);
console.log(confs['manifest.json'].rows);
});
TODO
- BUG: can't use a function as validator for a flag or value-optional option; providing a function implies that option requires a value.
- evaluate switching to https://github.com/busterjs/posix-argv-parser
- a way of setting all defaults on opts{} before (or at) parse time
- like: parse(argv, defaults) (and for parse(defaults), isArray is enough.)
- refactor out my atst's validation types and merge these two kits into one common package; that way, args.js can re-use the other custom validators like ISODATE, ISODATE_PAST and ISODATE_FUTURE
- support
--no-${option}
as a way to clear the value of a non-multi. - low priority todo: contrast my UserError definition with those at http://devdocs.io/javascript/global_objects/error and with my pubsub's DroppedMessageError
Legacy Signatures
args.define('c', 'cat', 'string', 'stuff', 'NAME');
args.define('c', 'cat', 'string', 'stuff');
args.define( 'cat', 'string', 'stuff');
args.define( 'cat', 'string');