1 /* 2 * getopt.js: node.js implementation of POSIX getopt() (and then some) 3 * 4 * Copyright 2011 David Pacheco. All rights reserved. 5 * 6 * Permission is hereby granted, free of charge, to any person obtaining a copy 7 * of this software and associated documentation files (the "Software"), to deal 8 * in the Software without restriction, including without limitation the rights 9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 * copies of the Software, and to permit persons to whom the Software is 11 * furnished to do so, subject to the following conditions: 12 * 13 * The above copyright notice and this permission notice shall be included in 14 * all copies or substantial portions of the Software. 15 * 16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 * SOFTWARE. 23 */ 24 25 var ASSERT = require('assert').ok; 26 27 function goError(msg) 28 { 29 return (new Error('getopt: ' + msg)); 30 }; 31 32 /* 33 * The BasicParser is our primary interface to the outside world. The 34 * documentation for this object and its public methods is contained in 35 * the included README.md. 36 */ 37 function goBasicParser(optstring, argv) 38 { 39 var ii; 40 41 ASSERT(optstring || optstring === '', "optstring is required"); 42 ASSERT(optstring.constructor === String, "optstring must be a string"); 43 ASSERT(argv, "argv is required"); 44 ASSERT(argv.constructor === Array, "argv must be an array"); 45 46 this.gop_argv = new Array(argv.length); 47 this.gop_options = {}; 48 this.gop_aliases = {}; 49 this.gop_optind = 2; 50 this.gop_subind = 0; 51 52 for (ii = 0; ii < argv.length; ii++) { 53 ASSERT(argv[ii].constructor === String, 54 "argv must be string array"); 55 this.gop_argv[ii] = argv[ii]; 56 } 57 58 this.parseOptstr(optstring); 59 } 60 61 exports.BasicParser = goBasicParser; 62 63 /* 64 * Parse the option string and update the following fields: 65 * 66 * gop_silent Whether to log errors to stderr. Silent mode is 67 * indicated by a leading ':' in the option string. 68 * 69 * gop_options Maps valid single-letter-options to booleans indicating 70 * whether each option is required. 71 * 72 * gop_aliases Maps valid long options to the corresponding 73 * single-letter short option. 74 */ 75 goBasicParser.prototype.parseOptstr = function (optstr) 76 { 77 var chr, cp, alias, arg, ii; 78 79 ii = 0; 80 if (optstr.length > 0 && optstr[0] == ':') { 81 this.gop_silent = true; 82 ii++; 83 } else { 84 this.gop_silent = false; 85 } 86 87 while (ii < optstr.length) { 88 chr = optstr[ii]; 89 arg = false; 90 91 if (!/^[\w\d]$/.test(chr)) 92 throw (goError('invalid optstring: only alphanumeric ' + 93 'characters may be used as options: ' + chr)); 94 95 if (ii + 1 < optstr.length && optstr[ii + 1] == ':') { 96 arg = true; 97 ii++; 98 } 99 100 this.gop_options[chr] = arg; 101 102 while (ii + 1 < optstr.length && optstr[ii + 1] == '(') { 103 ii++; 104 cp = optstr.indexOf(')', ii + 1); 105 if (cp == -1) 106 throw (goError('invalid optstring: missing ' + 107 '")" to match "(" at char ' + ii)); 108 109 alias = optstr.substring(ii + 1, cp); 110 this.gop_aliases[alias] = chr; 111 ii = cp; 112 } 113 114 ii++; 115 } 116 }; 117 118 goBasicParser.prototype.optind = function () 119 { 120 return (this.gop_optind); 121 }; 122 123 /* 124 * For documentation on what getopt() does, see README.md. The following 125 * implementation invariants are maintained by getopt() and its helper methods: 126 * 127 * this.gop_optind Refers to the element of gop_argv that contains 128 * the next argument to be processed. This may 129 * exceed gop_argv, in which case the end of input 130 * has been reached. 131 * 132 * this.gop_subind Refers to the character inside 133 * this.gop_options[this.gop_optind] which begins 134 * the next option to be processed. This may never 135 * exceed the length of gop_argv[gop_optind], so 136 * when incrementing this value we must always 137 * check if we should instead increment optind and 138 * reset subind to 0. 139 * 140 * That is, when any of these functions is entered, the above indices' values 141 * are as described above. getopt() itself and getoptArgument() may both be 142 * called at the end of the input, so they check whether optind exceeds 143 * argv.length. getoptShort() and getoptLong() are called only when the indices 144 * already point to a valid short or long option, respectively. 145 * 146 * getopt() processes the next option as follows: 147 * 148 * o If gop_optind > gop_argv.length, then we already parsed all arguments. 149 * 150 * o If gop_subind == 0, then we're looking at the start of an argument: 151 * 152 * o Check for special cases like '-', '--', and non-option arguments. 153 * If present, update the indices and return the appropriate value. 154 * 155 * o Check for a long-form option (beginning with '--'). If present, 156 * delegate to getoptLong() and return the result. 157 * 158 * o Otherwise, advance subind past the argument's leading '-' and 159 * continue as though gop_subind != 0 (since that's now the case). 160 * 161 * o Delegate to getoptShort() and return the result. 162 */ 163 goBasicParser.prototype.getopt = function () 164 { 165 if (this.gop_optind >= this.gop_argv.length) 166 /* end of input */ 167 return (undefined); 168 169 arg = this.gop_argv[this.gop_optind]; 170 171 if (this.gop_subind == 0) { 172 if (arg == '-' || arg === '' || arg[0] != '-') 173 return (undefined); 174 175 if (arg == '--') { 176 this.gop_optind++; 177 this.gop_subind = 0; 178 return (undefined); 179 } 180 181 if (arg[1] == '-') 182 return (this.getoptLong()); 183 184 this.gop_subind++; 185 ASSERT(this.gop_subind < arg.length); 186 } 187 188 return (this.getoptShort()); 189 }; 190 191 /* 192 * Implements getopt() for the case where optind/subind point to a short option. 193 */ 194 goBasicParser.prototype.getoptShort = function () 195 { 196 var arg, chr; 197 198 ASSERT(this.gop_optind < this.gop_argv.length); 199 arg = this.gop_argv[this.gop_optind]; 200 ASSERT(this.gop_subind < arg.length); 201 chr = arg[this.gop_subind]; 202 203 if (!(chr in this.gop_options)) 204 return (this.errInvalidOption(chr)); 205 206 if (++this.gop_subind >= arg.length) { 207 this.gop_optind++; 208 this.gop_subind = 0; 209 } 210 211 if (!this.gop_options[chr]) 212 return ({ option: chr }); 213 214 return (this.getoptArgument(chr)); 215 } 216 217 /* 218 * Implements getopt() for the case where optind/subind point to a long option. 219 */ 220 goBasicParser.prototype.getoptLong = function () 221 { 222 var arg, alias, chr, eq; 223 224 ASSERT(this.gop_subind === 0); 225 ASSERT(this.gop_optind < this.gop_argv.length); 226 arg = this.gop_argv[this.gop_optind]; 227 ASSERT(arg.length > 2 && arg[0] == '-' && arg[1] == '-'); 228 229 eq = arg.indexOf('='); 230 alias = arg.substring(2, eq == -1 ? arg.length : eq); 231 if (!(alias in this.gop_aliases)) 232 return (this.errInvalidOption(alias)); 233 234 chr = this.gop_aliases[alias]; 235 ASSERT(chr in this.gop_options); 236 237 if (!this.gop_options[chr]) { 238 if (eq != -1) 239 return (this.errExtraArg(alias)); 240 241 this.gop_optind++; /* eat this argument */ 242 return ({ option: chr }); 243 } 244 245 /* 246 * Advance optind/subind for the argument value and retrieve it. 247 */ 248 if (eq == -1) 249 this.gop_optind++; 250 else 251 this.gop_subind = eq + 1; 252 253 return (this.getoptArgument(chr)); 254 }; 255 256 /* 257 * For the given option letter 'chr' that takes an argument, assumes that 258 * optind/subind point to the argument (or denote the end of input) and return 259 * the appropriate getopt() return value for this option and argument (or return 260 * the appropriate error). 261 */ 262 goBasicParser.prototype.getoptArgument = function (chr) 263 { 264 var arg; 265 266 if (this.gop_optind >= this.gop_argv.length) 267 return (this.errMissingArg(chr)); 268 269 arg = this.gop_argv[this.gop_optind].substring(this.gop_subind); 270 this.gop_optind++; 271 this.gop_subind = 0; 272 return ({ option: chr, optarg: arg }); 273 }; 274 275 goBasicParser.prototype.errMissingArg = function (chr) 276 { 277 if (this.gop_silent) 278 return ({ option: '?', optopt: chr }); 279 280 process.stderr.write('option requires an argument -- ' + chr + '\n'); 281 return ({ option: ':', optopt: chr, error: true }); 282 }; 283 284 goBasicParser.prototype.errInvalidOption = function (chr) 285 { 286 if (!this.gop_silent) 287 process.stderr.write('illegal option -- ' + chr + '\n'); 288 289 return ({ option: '?', optopt: chr, error: true }); 290 }; 291 292 /* 293 * This error is not specified by POSIX, but neither is the notion of specifying 294 * long option arguments using "=" in the same argv-argument, but it's common 295 * practice and pretty convenient. 296 */ 297 goBasicParser.prototype.errExtraArg = function (chr) 298 { 299 if (!this.gop_silent) 300 process.stderr.write('option expects no argument -- ' + 301 chr + '\n'); 302 303 return ({ option: '?', optopt: chr, error: true }); 304 };