Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(235)

Side by Side Diff: lib/args/args.dart

Issue 10335007: First pass at a generic reusable command line option parser. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Respond to review. Created 8 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | lib/args/example.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 /**
6 * This library lets you define parsers for parsing raw command-line arguments
7 * into a set of options and values using [GNU][] and [POSIX][] style options.
8 *
9 * ## Defining options ##
10 *
11 * To use this library, you create an [ArgParser] object which will contain
12 * the set of options you support:
13 *
14 * var parser = new ArgParser();
15 *
16 * Then you define a set of options on that parser using [addOption()] and
17 * [addFlag()]. The minimal way to create an option is:
18 *
19 * parser.addOption('name');
20 *
21 * This creates an option named "name". Options must be given a value on the
22 * command line. If you have a simple on/off flag, you can instead use:
23 *
24 * parser.addFlag('name');
25 *
26 * (From here on out "option" will refer to both "regular" options and flags.
27 * In cases where the distinction matters, we'll use "non-flag option".)
28 *
29 * Options may have an optional single-character abbreviation:
30 *
31 * parser.addOption('mode', abbr: 'm');
32 * parser.addFlag('verbose', abbr: 'v');
33 *
34 * They may also specify a default value. The default value will be used if the
35 * option isn't provided:
36 *
37 * parser.addOption('mode', defaultsTo: 'debug');
38 * parser.addFlag('verbose', defaultsTo: false);
39 *
40 * The default value for non-flag options can be any [String]. For flags, it
41 * must be a [bool].
42 *
43 * To validate non-flag options, you may provide an allowed set of values. When
44 * you do, it will throw an [ArgFormatException] when you parse the arguments
45 * if the value for an option is not in the allowed set:
46 *
47 * parser.addOption('mode', allowed: ['debug', 'release']);
48 *
49 * You can provide a callback when you define an option. When you later parse
50 * a set of arguments, the callback for that option will be invoked with the
51 * value provided for it:
52 *
53 * parser.addOption('mode', callback: (mode) => print('Got mode $mode));
54 * parser.addFlag('verbose', callback: (verbose) {
55 * if (verbose) print('Verbose');
56 * });
57 *
58 * The callback for each option will *always* be called when you parse a set of
59 * arguments. If the option isn't provided in the args, the callback will be
60 * passed the default value, or `null` if there is none set.
61 *
62 * ## Parsing arguments ##
63 *
64 * Once you have an [ArgParser] set up with some options and flags, you use it
65 * by calling [ArgParser.parse()] with a set of arguments:
66 *
67 * var results = parser.parse(['some', 'command', 'line', 'args']);
68 *
69 * These will usually come from `new Options().arguments`, but you can pass in
70 * any list of strings. It returns an instance of [ArgResults]. This is a
71 * map-like object that will return the value of any parsed option.
72 *
73 * var parser = new ArgParser();
74 * parser.addOption('mode');
75 * parser.addFlag('verbose', defaultsTo: true);
76 * var results = parser.parse('['--mode', 'debug', 'something', 'else']);
77 *
78 * print(results['mode']); // debug
79 * print(results['verbose']); // true
80 *
81 * The [parse()] method will stop as soon as it reaches `--` or anything that
82 * it doesn't recognize as an option, flag, or option value. If there are still
83 * arguments left, they will be provided to you in
84 * [ArgResults.rest].
85 *
86 * print(results.rest); // ['something', 'else']
87 *
88 * ## Specifying options ##
89 *
90 * To actually pass in options and flags on the command line, use GNU or POSIX
91 * style. If you define an option like:
92 *
93 * parser.addOption('name', abbr: 'n');
94 *
95 * Then a value for it can be specified on the command line using any of:
96 *
97 * --name=somevalue
98 * --name somevalue
99 * -nsomevalue
100 * -n somevalue
101 *
102 * Given this flag:
103 *
104 * parser.addFlag('name', abbr: 'n');
105 *
106 * You can set it on using one of:
107 *
108 * --name
109 * -n
110 *
111 * Or set it off using:
112 *
113 * --no-name
114 *
115 * Multiple flag abbreviation can also be collapsed into a single argument. If
116 * you define:
117 *
118 * parser.addFlag('verbose', abbr: 'v');
119 * parser.addFlag('french', abbr: 'f');
120 * parser.addFlag('iambic-pentameter', abbr: 'i');
121 *
122 * Then all three flags could be set using:
123 *
124 * -vfi
125 *
126 * ## Usage ##
127 *
128 * This library can also be used to automatically generate nice usage help
129 * text like you get when you run a program with `--help`. To use this, you
130 * will also want to provide some help text when you create your options. To
131 * define help text for the entire option, do:
132 *
133 * parser.addOption('mode', help: 'The compiler configuration',
134 * allowed: ['debug', 'release']);
135 * parser.addFlag('verbose', help: 'Show additional diagnostic info');
136 *
137 * For non-flag options, you can also provide detailed help for each expected
138 * value using a map:
139 *
140 * parser.addOption('arch', help: 'The architecture to compile for',
141 * allowedHelp: {
142 * 'ia32': 'Intel x86',
143 * 'arm': 'ARM Holding 32-bit chip'
144 * });
145 *
146 * If you define a set of options like the above, then calling this:
147 *
148 * print(parser.getUsage());
149 *
150 * Will display something like:
151 *
152 * --mode The compiler configuration
153 * [debug, release]
154 *
155 * --[no-]verbose Show additional diagnostic info
156 * --arch The architecture to compile for
157 *
158 * [arm] ARM Holding 32-bit chip
159 * [ia32] Intel x86
160 *
161 * [posix]: http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.h tml#tag_12_02
162 * [gnu]: http://www.gnu.org/prep/standards/standards.html#Command_002dLine-Inte rfaces
163 */
164 #library('args');
165
166 #import('utils.dart');
167
168 /**
169 * A class for taking a list of raw command line arguments and parsing out
170 * options and flags from them.
171 */
172 class ArgParser {
173 static final _SOLO_OPT = const RegExp(@'^-([a-z0-9])$');
174 static final _ABBR_OPT = const RegExp(@'^-([a-z0-9]+)(.*)$');
175 static final _LONG_OPT = const RegExp(@'^--([a-z\-_0-9]+)(=(.*))?$');
176
177 final Map<String, _Option> _options;
178
179 /**
180 * The names of the options, in the order that they were added. This way we
181 * can generate usage information in the same order.
182 */
183 // TODO(rnystrom): Use an ordered map type, if one appears.
184 final List<String> _optionNames;
185
186 /** The current argument list being parsed. Set by [parse()]. */
187 List<String> _args;
188
189 /** Index of the current argument being parsed in [_args]. */
190 int _current;
191
192 /** Creates a new ArgParser. */
193 ArgParser()
194 : _options = <_Option>{},
195 _optionNames = <String>[];
196
197 /**
198 * Defines a flag. Throws an [IllegalArgumentException] if:
199 *
200 * * There is already an option named [name].
201 * * There is already an option using abbreviation [abbr].
202 */
203 void addFlag(String name, [String abbr, String help, bool defaultsTo = false,
204 void callback(bool value)]) {
205 _addOption(name, abbr, help, null, null, defaultsTo, callback,
206 isFlag: true);
207 }
208
209 /**
210 * Defines a value-taking option. Throws an [IllegalArgumentException] if:
211 *
212 * * There is already an option with name [name].
213 * * There is already an option using abbreviation [abbr].
214 */
215 void addOption(String name, [String abbr, String help, List<String> allowed,
216 Map<String, String> allowedHelp, String defaultsTo,
217 void callback(bool value)]) {
218 _addOption(name, abbr, help, allowed, allowedHelp, defaultsTo, callback,
219 isFlag: false);
220 }
221
222 void _addOption(String name, String abbr, String help, List<String> allowed,
223 Map<String, String> allowedHelp, defaultsTo,
224 void callback(bool value), [bool isFlag]) {
225 // Make sure the name isn't in use.
226 if (_options.containsKey(name)) {
227 throw new IllegalArgumentException('Duplicate option "$name".');
228 }
229
230 // Make sure the abbreviation isn't too long or in use.
231 if (abbr != null) {
232 if (abbr.length > 1) {
233 throw new IllegalArgumentException(
234 'Abbreviation "$abbr" is longer than one character.');
235 }
236
237 var existing = _findByAbbr(abbr);
238 if (existing != null) {
239 throw new IllegalArgumentException(
240 'Abbreviation "$abbr" is already used by "${existing.name}".');
241 }
242 }
243
244 _options[name] = new _Option(name, abbr, help, allowed, allowedHelp,
245 defaultsTo, callback, isFlag: isFlag);
246 _optionNames.add(name);
247 }
248
249 /**
250 * Parses [args], a list of command-line arguments, matches them against the
251 * flags and options defined by this parser, and returns the result.
252 */
253 ArgResults parse(List<String> args) {
254 _args = args;
255 _current = 0;
256 var results = {};
257
258 // Initialize flags to their defaults.
259 _options.forEach((name, option) {
260 results[name] = option.defaultValue;
261 });
262
263 // Parse the args.
264 for (_current = 0; _current < args.length; _current++) {
265 var arg = args[_current];
266
267 if (arg == '--') {
268 // Reached the argument terminator, so stop here.
269 _current++;
270 break;
271 }
272
273 // Try to parse the current argument as an option. Note that the order
274 // here matters.
275 if (_parseSoloOption(results)) continue;
276 if (_parseAbbreviation(results)) continue;
277 if (_parseLongOption(results)) continue;
278
279 // If we got here, the argument doesn't look like an option, so stop.
280 break;
281 }
282
283 // Invoke the callbacks.
284 for (var name in _optionNames) {
285 var option = _options[name];
286 if (option.callback != null) option.callback(results[name]);
287 }
288
289 // Add in the leftover arguments we didn't parse.
290 return new ArgResults(results,
291 _args.getRange(_current, _args.length - _current));
292 }
293
294 /**
295 * Generates a string displaying usage information for the defined options.
296 * This is basically the help text shown on the command line.
297 */
298 String getUsage() {
299 return new _Usage(this).generate();
300 }
301
302 /**
303 * Called during parsing to validate the arguments. Throws an
304 * [ArgFormatException] if [condition] is `false`.
305 */
306 _validate(bool condition, String message) {
307 if (!condition) throw new ArgFormatException(message);
308 }
309
310 /** Validates and stores [value] as the value for [option]. */
311 _setOption(Map results, _Option option, value) {
312 // See if it's one of the allowed values.
313 if (option.allowed != null) {
314 _validate(option.allowed.some((allow) => allow == value),
315 '"$value" is not an allowed value for option "${option.name}".');
316 }
317
318 results[option.name] = value;
319 }
320
321 /**
322 * Pulls the value for [option] from the next argument in [_args] (where the
323 * current option is at index [_current]. Validates that there is a valid
324 * value there.
325 */
326 void _readNextArgAsValue(Map results, _Option option) {
327 _current++;
328 // Take the option argument from the next command line arg.
329 _validate(_current < _args.length,
330 'Missing argument for "${option.name}".');
331
332 // Make sure it isn't an option itself.
333 _validate(!_ABBR_OPT.hasMatch(_args[_current]) &&
334 !_LONG_OPT.hasMatch(_args[_current]),
335 'Missing argument for "${option.name}".');
336
337 _setOption(results, option, _args[_current]);
338 }
339
340 /**
341 * Tries to parse the current argument as a "solo" option, which is a single
342 * hyphen followed by a single letter. We treat this differently than
343 * collapsed abbreviations (like "-abc") to handle the possible value that
344 * may follow it.
345 */
346 bool _parseSoloOption(Map results) {
347 var soloOpt = _SOLO_OPT.firstMatch(_args[_current]);
348 if (soloOpt == null) return false;
349
350 var option = _findByAbbr(soloOpt[1]);
351 _validate(option != null,
352 'Could not find an option or flag "-${soloOpt[1]}".');
353
354 if (option.isFlag) {
355 _setOption(results, option, true);
356 } else {
357 _readNextArgAsValue(results, option);
358 }
359
360 return true;
361 }
362
363 /**
364 * Tries to parse the current argument as a series of collapsed abbreviations
365 * (like "-abc") or a single abbreviation with the value directly attached
366 * to it (like "-mrelease").
367 */
368 bool _parseAbbreviation(Map results) {
369 var abbrOpt = _ABBR_OPT.firstMatch(_args[_current]);
370 if (abbrOpt == null) return false;
371
372 // If the first character is the abbreviation for a non-flag option, then
373 // the rest is the value.
374 var c = abbrOpt[1].substring(0, 1);
375 var first = _findByAbbr(c);
376 if (first == null) {
377 _validate(false, 'Could not find an option with short name "-$c".');
378 } else if (!first.isFlag) {
379 // The first character is a non-flag option, so the rest must be the
380 // value.
381 var value = '${abbrOpt[1].substring(1)}${abbrOpt[2]}';
382 _setOption(results, first, value);
383 } else {
384 // If we got some non-flag characters, then it must be a value, but
385 // if we got here, it's a flag, which is wrong.
386 _validate(abbrOpt[2] == '',
387 'Option "-$c" is a flag and cannot handle value '
388 '"${abbrOpt[1].substring(1)}${abbrOpt[2]}".');
389
390 // Not an option, so all characters should be flags.
391 for (var i = 0; i < abbrOpt[1].length; i++) {
392 var c = abbrOpt[1].substring(i, i + 1);
393 var option = _findByAbbr(c);
394 _validate(option != null,
395 'Could not find an option with short name "-$c".');
396
397 // In a list of short options, only the first can be a non-flag. If
398 // we get here we've checked that already.
399 _validate(option.isFlag,
400 'Option "-$c" must be a flag to be in a collapsed "-".');
401
402 _setOption(results, option, true);
403 }
404 }
405
406 return true;
407 }
408
409 /**
410 * Tries to parse the current argument as a long-form named option, which
411 * may include a value like "--mode=release" or "--mode release".
412 */
413 bool _parseLongOption(Map results) {
414 var longOpt = _LONG_OPT.firstMatch(_args[_current]);
415 if (longOpt == null) return false;
416
417 var name = longOpt[1];
418 var option = _options[name];
419 if (option != null) {
420 if (option.isFlag) {
421 _validate(longOpt[3] == null,
422 'Flag option "$name" should not be given a value.');
423
424 _setOption(results, option, true);
425 } else if (longOpt[3] != null) {
426 // We have a value like --foo=bar.
427 _setOption(results, option, longOpt[3]);
428 } else {
429 // Option like --foo, so look for the value as the next arg.
430 _readNextArgAsValue(results, option);
431 }
432 } else if (name.startsWith('no-')) {
433 // See if it's a negated flag.
434 name = name.substring('no-'.length);
435 option = _options[name];
436 _validate(option != null, 'Could not find an option named "$name".');
437 _validate(option.isFlag, 'Cannot negate non-flag option "$name.');
438
439 _setOption(results, option, false);
440 } else {
441 _validate(option != null, 'Could not find an option named "$name".');
442 }
443
444 return true;
445 }
446
447 /**
448 * Finds the option whose abbreviation is [abbr], or `null` if no option has
449 * that abbreviation.
450 */
451 _Option _findByAbbr(String abbr) {
452 for (var option in _options.getValues()) {
453 if (option.abbreviation == abbr) return option;
454 }
455
456 return null;
457 }
458 }
459
460 /**
461 * The results of parsing a series of command line arguments using
462 * [ArgParser.parse()]. Includes the parsed options and any remaining unparsed
463 * command line arguments.
464 */
465 class ArgResults {
466 final Map _options;
467
468 /**
469 * The remaining command-line arguments that were not parsed as options or
470 * flags. If `--` was used to separate the options from the remaining
471 * arguments, it will not be included in this list.
472 */
473 final List<String> rest;
474
475 /** Creates a new [ArgResults]. */
476 ArgResults(this._options, this.rest);
477
478 /** Gets the parsed command-line option named [name]. */
479 operator [](String name) {
480 if (!_options.containsKey(name)) {
481 throw new IllegalArgumentException(
482 'Could not find an option named "$name".');
483 }
484
485 return _options[name];
486 }
487 }
488
489 /**
490 * Exception thrown by [ArgParser.parse()] when the argument list isn't valid.
491 */
492 class ArgFormatException implements Exception {
493 final String message;
494 const ArgFormatException(this.message);
495 }
496
497 class _Option {
498 final String name;
499 final String abbreviation;
500 final List allowed;
501 final defaultValue;
502 final Function callback;
503 final String help;
504 final Map<String, String> allowedHelp;
505 final bool isFlag;
506
507 _Option(this.name, this.abbreviation, this.help, this.allowed,
508 this.allowedHelp, this.defaultValue, this.callback, [this.isFlag]);
509 }
510
511 /**
512 * Takes an [ArgParser] and generates a string of usage (i.e. help) text for its
513 * defined options. Internally, it works like a tabular printer. The output is
514 * divided into three horizontal columns, like so:
515 *
516 * -h, --help Prints the usage information
517 * | | | |
518 *
519 * It builds the usage text up one column at a time and handles padding with
520 * spaces and wrapping to the next line to keep the cells correctly lined up.
521 */
522 class _Usage {
523 static final NUM_COLUMNS = 3; // Abbreviation, long name, help.
524
525 /** The parser this is generating usage for. */
526 final ArgParser args;
527
528 /** The working buffer for the generated usage text. */
529 StringBuffer buffer;
530
531 /**
532 * The column that the "cursor" is currently on. If the next call to
533 * [write()] is not for this column, it will correctly handle advancing to
534 * the next column (and possibly the next row).
535 */
536 int currentColumn = 0;
537
538 /** The width in characters of each column. */
539 List<int> columnWidths;
540
541 /**
542 * The number of sequential lines of text that have been written to the last
543 * column (which shows help info). We track this so that help text that spans
544 * multiple lines can be padded with a blank line after it for separation.
545 * Meanwhile, sequential options with single-line help will be compacted next
546 * to each other.
547 */
548 int numHelpLines = 0;
549
550 /**
551 * How many newlines need to be rendered before the next bit of text can be
552 * written. We do this lazily so that the last bit of usage doesn't have
553 * dangling newlines. We only write newlines right *before* we write some
554 * real content.
555 */
556 int newlinesNeeded = 0;
557
558 _Usage(this.args);
559
560 /**
561 * Generates a string displaying usage information for the defined options.
562 * This is basically the help text shown on the command line.
563 */
564 String generate() {
565 buffer = new StringBuffer();
566
567 calculateColumnWidths();
568
569 for (var name in args._optionNames) {
570 var option = args._options[name];
571 write(0, getAbbreviation(option));
572 write(1, getLongOption(option));
573
574 if (option.help != null) write(2, option.help);
575
576 if (option.allowedHelp != null) {
577 var allowedNames = option.allowedHelp.getKeys();
578 allowedNames.sort((a, b) => a.compareTo(b));
579 newline();
580 for (var name in allowedNames) {
581 write(1, getAllowedTitle(name));
582 write(2, option.allowedHelp[name]);
583 }
584 newline();
585 } else if (option.allowed != null) {
586 write(2, buildAllowedList(option));
587 } else if (option.defaultValue != null) {
588 if (option.isFlag && option.defaultValue == true) {
589 write(2, '(defaults to on)');
590 } else if (!option.isFlag) {
591 write(2, '(defaults to "${option.defaultValue}")');
592 }
593 }
594
595 // If any given option displays more than one line of text on the right
596 // column (i.e. help, default value, allowed options, etc.) then put a
597 // blank line after it. This gives space where it's useful while still
598 // keeping simple one-line options clumped together.
599 if (numHelpLines > 1) newline();
600 }
601
602 return buffer.toString();
603 }
604
605 String getAbbreviation(_Option option) {
606 if (option.abbreviation != null) {
607 return '-${option.abbreviation}, ';
608 } else {
609 return '';
610 }
611 }
612
613 String getLongOption(_Option option) {
614 if (option.isFlag) {
615 return '--[no-]${option.name}';
616 } else {
617 return '--${option.name}';
618 }
619 }
620
621 String getAllowedTitle(String allowed) {
622 return ' [$allowed]';
623 }
624
625 void calculateColumnWidths() {
626 int abbr = 0;
627 int title = 0;
628 for (var name in args._optionNames) {
629 var option = args._options[name];
630
631 // Make room in the first column if there are abbreviations.
632 abbr = Math.max(abbr, getAbbreviation(option).length);
633
634 // Make room for the option.
635 title = Math.max(title, getLongOption(option).length);
636
637 // Make room for the allowed help.
638 if (option.allowedHelp != null) {
639 for (var allowed in option.allowedHelp.getKeys()) {
640 title = Math.max(title, getAllowedTitle(allowed).length);
641 }
642 }
643 }
644
645 // Leave a gutter between the columns.
646 title += 4;
647 columnWidths = [abbr, title];
648 }
649
650 newline() {
651 newlinesNeeded++;
652 currentColumn = 0;
653 numHelpLines = 0;
654 }
655
656 write(int column, String text) {
657 var lines = text.split('\n');
658
659 // Strip leading and trailing empty lines.
660 while (lines.length > 0 && lines[0].trim() == '') {
661 lines.removeRange(0, 1);
662 }
663
664 while (lines.length > 0 && lines[lines.length - 1].trim() == '') {
665 lines.removeLast();
666 }
667
668 for (var line in lines) {
669 writeLine(column, line);
670 }
671 }
672
673 writeLine(int column, String text) {
674 // Write any pending newlines.
675 while (newlinesNeeded > 0) {
676 buffer.add('\n');
677 newlinesNeeded--;
678 }
679
680 // Advance until we are at the right column (which may mean wrapping around
681 // to the next line.
682 while (currentColumn != column) {
683 if (currentColumn < NUM_COLUMNS - 1) {
684 buffer.add(padRight('', columnWidths[currentColumn]));
685 } else {
686 buffer.add('\n');
687 }
688 currentColumn = (currentColumn + 1) % NUM_COLUMNS;
689 }
690
691 if (column < columnWidths.length) {
692 // Fixed-size column, so pad it.
693 buffer.add(padRight(text, columnWidths[column]));
694 } else {
695 // The last column, so just write it.
696 buffer.add(text);
697 }
698
699 // Advance to the next column.
700 currentColumn = (currentColumn + 1) % NUM_COLUMNS;
701
702 // If we reached the last column, we need to wrap to the next line.
703 if (column == NUM_COLUMNS - 1) newlinesNeeded++;
704
705 // Keep track of how many consecutive lines we've written in the last
706 // column.
707 if (column == NUM_COLUMNS - 1) {
708 numHelpLines++;
709 } else {
710 numHelpLines = 0;
711 }
712 }
713
714 buildAllowedList(_Option option) {
715 var allowedBuffer = new StringBuffer();
716 allowedBuffer.add('[');
717 bool first = true;
718 for (var allowed in option.allowed) {
719 if (!first) allowedBuffer.add(', ');
720 allowedBuffer.add(allowed);
721 if (allowed == option.defaultValue) {
722 allowedBuffer.add(' (default)');
723 }
724 first = false;
725 }
726 allowedBuffer.add(']');
727 return allowedBuffer.toString();
728 }
729 }
OLDNEW
« no previous file with comments | « no previous file | lib/args/example.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698