argparse.js (3676B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // Command-line argument parser, modeled after but not identical to Python's 6 // argparse. 7 8 var ArgParser = class { 9 constructor(desc) { 10 this._params = []; 11 this._doc = desc; 12 13 this.add_argument("--help", { 14 help: "display this help message", 15 }); 16 } 17 18 // name is '--foo', '-f', or an array of aliases. 19 // 20 // spec is an options object with keys: 21 // dest: key name to store the result in (optional for long options) 22 // default: value to use if not passed on command line (optional) 23 // help: description of the option to show in --help 24 // options: array of valid choices 25 // 26 // Prefixes of long option names are allowed. If a prefix is ambiguous, it 27 // will match the first parameter added to the ArgParser. 28 add_argument(name, spec) { 29 const names = Array.isArray(name) ? name : [name]; 30 31 spec = Object.assign({}, spec); 32 spec.aliases = names; 33 for (const name of names) { 34 if (!name.startsWith("-")) { 35 throw new Error(`unhandled argument syntax '${name}'`); 36 } 37 if (name.startsWith("--")) { 38 spec.dest = spec.dest || name.substr(2); 39 } 40 this._params.push({ name, spec }); 41 } 42 } 43 44 parse_args(args) { 45 const opts = {}; 46 const rest = []; 47 48 for (const { spec } of this._params) { 49 if (spec.default !== undefined) { 50 opts[spec.dest] = spec.default; 51 } 52 } 53 54 const seen = new Set(); 55 for (let i = 0; i < args.length; i++) { 56 const arg = args[i]; 57 if (!arg.startsWith("-")) { 58 rest.push(arg); 59 continue; 60 } else if (arg === "--") { 61 rest.push(args.slice(i+1)); 62 break; 63 } 64 65 if (arg == "--help" || arg == "-h") { 66 this.help(); 67 } 68 69 let parameter; 70 let [passed, value] = arg.split("="); 71 for (const { name, spec } of this._params) { 72 if (passed.startsWith("--")) { 73 if (name.startsWith(passed)) { 74 parameter = spec; 75 } 76 } else if (passed.startsWith("-") && passed === name) { 77 parameter = spec; 78 } 79 if (parameter) { 80 if (value === undefined) { 81 value = args[++i]; 82 } 83 opts[parameter.dest] = value; 84 break; 85 } 86 } 87 88 if (parameter) { 89 if (seen.has(parameter)) { 90 throw new Error(`${parameter.aliases[0]} given multiple times`); 91 } 92 seen.add(parameter); 93 } else { 94 throw new Error(`invalid command-line argument '${arg}'`); 95 } 96 } 97 98 for (const { name, spec } of this._params) { 99 if (spec.options && !spec.options.includes(opts[spec.dest])) { 100 throw new Error(`invalid ${name} value '${opts[spec.dest]}'`); 101 opts[spec.dest] = spec.default; 102 } 103 } 104 105 return { opts, rest }; 106 } 107 108 help() { 109 print(`Usage: ${this._doc}`); 110 const specs = new Set(this._params.map(p => p.spec)); 111 const optstrs = [...specs].map(p => p.aliases.join(", ")); 112 let maxlen = Math.max(...optstrs.map(s => s.length)); 113 for (const spec of specs) { 114 const name = spec.aliases[0]; 115 let helptext = spec.help ?? "undocumented"; 116 if ("options" in spec) { 117 helptext += ` (one of ${spec.options.map(x => `'${x}'`).join(", ")})`; 118 } 119 if ("default" in spec) { 120 helptext += ` (default '${spec.default}')`; 121 } 122 const optstr = spec.aliases.join(", "); 123 print(` ${optstr.padEnd(maxlen)} ${helptext}`); 124 } 125 quit(0); 126 } 127 };