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