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 };