123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- /*
- * TODO:
- * - Validate options object and configuration
- * - Add support for repeat arguments
- * - Support list type arguments
- * - Support optional arguments
- */
- /*
- * Intro
- *
- * Very simple implementation of an option parser. It supports long and short
- * options, bundled options (-avd) and options with values. It receives an
- * argument object describing the options, their types (not to be confused with
- * their value types, i.e Integer, String, etc), a default value (if any), and,
- * possibly, functions for value validation and convertion. It returns an object
- * contanining key-value pairs of the defined long options.
- *
- * Options of type 'flag' will have their values set to 'true' if the option is
- * found at the command-line, otherwise they will be 'null'.
- *
- * This 'getOpt' also forces the definition of long option names, this is in
- * attempt to make the returned option object more readable. But nothing is
- * stopping the user from defining single letter long option names (i.e '--l').
- *
- * It also has optiona support for "bundling" (by default is turned on) both
- * options and option arguments. To turn bundling off, pass an object containing
- * { bundling: false } as the second argument.
- *
- * This implementation tests each entry of the bundled argument for the type
- * 'flag'. Each matching letter is parsed as an 'flag' option until until a
- * 'value' type option is found, at which point the remainder of the option is
- * evaluated as an argument.
- *
- * Brief example
- *
- * {
- * 'has-value':
- * {
- * type: 'value',
- * short: 'h',
- * convert: v => parseInt(v),
- * validate: v => v > 1
- * },
- * verbose: {
- * type: 'flag',
- * short: 'v'
- * }
- * }
- *
- * If a command with the options of '--has-value 5 --verbose' or '-h 5 -v' was
- * supplied, it would generate and the options object: { 'has-value': 5,
- * verbose: true }.
- *
- * One could also make use of bundling and supply a the argument: '-vh5', which
- * produce the same object.
- *
- * Argument Object
- *
- * type
- *
- * There are two types of arguments supported: arguments with user defined
- * values ('type: value') and arguments that act as switches ('type: flag') and
- * carry no user specified value. They have two strategies for parsing: type
- * 'value' consumes the next immediate argument; and type 'flag' doesn't consume
- * anything on the argument parsing loop.
- *
- * short
- *
- * A short version of the option, meant to be used with a single dash ('-')
- * character. Short options can be bundled together ('-abc' or '-p3000'), but
- * only if they are all of type 'flag', or if the there is a single 'value' type
- * option on the end ('-abp3000').
- *
- * default
- *
- * Default value for option in case none is specified.
- *
- * convert
- *
- * A optional function to perform any required operations on the extracted
- * value. Its executed before the 'validate' function.
- *
- * validate
- *
- * An optional function that tests the value of value. Its exectued after the
- * 'convert' function and must return a falsy or truthy value.
- *
- * Quirks
- *
- * An option that requires an argument, but that is immediately followed by
- * another option, will have this option set at its argument.
- */
- "use strict";
- import { die } from './die.js';
- export function getOpt(argObj, config) {
- const optKeys = Object.keys(argObj);
- let argv = process.argv.slice(2)
- let opts = {};
- let notOpts = [];
-
- let bundle = true;
- bundle = config.bundle;
-
- let arg;
- while ((arg = argv.shift())) {
- // Skip non-option args
- if (!arg.startsWith('-')) {
- notOpts.push(arg);
- continue;
- }
-
- // End of options
- if (arg == '--') break;
- let argOpt;
- let argKey;
- if (arg.startsWith('--')) { // long option
- argOpt = arg.slice(2);
- argKey = optKeys.find(k => k == argOpt);
- if (!argKey)
- die(`Unrecognized option: ${arg}`);
- } else { // short option
- argOpt = arg.slice(1);
- const opt = argOpt[0];
- const key = optKeys.find(k => argObj[k].short == opt);
- if (!key)
- die(`Unrecognized option '${argOpt}'`);
- if (Object.keys(opts).includes(key))
- die(`Option '-${opt}' already specified`)
-
- argKey = key;
-
- if (argOpt.length > 1) {
- // If bundling, either schedule remainder to be parsed as
- // options or arguments dependinng or option type
- if (bundle) {
- let asOpt = '-';
- if (argObj[argKey].type == 'value') asOpt = '';
- argv.unshift(`${asOpt}${argOpt.slice(1)}`);
- } else
- die(`Unrecognized option: '${arg}'`);
- }
- }
-
- const type = argObj[argKey].type;
-
- switch (type) {
- case 'value':
- let val = argv.shift();
-
- const validateFunc = argObj[argKey].validate;
- const convertFunc = argObj[argKey].convert;
- if (convertFunc) val = convertFunc(val)
- if (validateFunc) {
- if (!validateFunc(val)) throw '';
- }
- opts[argKey] = val;
- break;
- case 'flag':
- opts[argKey] = true;
- break;
- default:
- die(`Invalid ${type} for key ${argKey}`);
- }
- }
- if (notOpts.length >= 1)
- process.argv = process.argv.slice(0, 2).concat(notOpts);
- // Fill empty options with their default values
- Object.keys(argObj).forEach(k => {
- if (!opts[k]) {
- if (argObj[k].default)
- opts[k] = argObj[k].default;
- else opts[k] = null;
- }
- });
- return opts;
- }
|