| // Licensed to the Software Freedom Conservancy (SFC) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The SFC licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| /** |
| * @fileoverview A simple command line option parser. |
| */ |
| |
| 'use strict'; |
| |
| var path = require('path'); |
| |
| |
| /** |
| * Repeats a string n times. |
| * @param {string} str The string to repeat. |
| * @param {number} n The number of times to repeat the string. |
| * @return {string} The repeated string, concatenated together. |
| */ |
| function repeatStr(str, n) { |
| return new Array(n + 1).join(str); |
| } |
| |
| |
| /** |
| * Trims a string of all trailing and leading whitespace. |
| * @param {string} str The string to trim. |
| * @return {string} The trimmed string. |
| */ |
| function trimStr(str) { |
| return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, ''); |
| } |
| |
| |
| /** |
| * Wraps the provided {@code text} so every line is at most {@code width} |
| * characters long. |
| * @param {string} text The text to wrap. |
| * @param {number} width The maximum line length. |
| * @param {string=} opt_indent String that will be prepended to each line. |
| * Defaults to the empty string. |
| * @return {!Array.<string>} A list of lines, without newline characters. |
| */ |
| function wrapStr(text, width, opt_indent) { |
| var out = [], |
| indent = opt_indent || ''; |
| |
| if (indent.length >= width) { |
| throw Error('Wrapped line indentation is longer than permitted width: ' + |
| indent.length + ' >= ' + width); |
| } |
| |
| text.split('\n').forEach(function(line) { |
| if (/^\s*$/.test(line)) { |
| out.push(''); // Push a blank line. |
| return; |
| } |
| |
| do { |
| line = indent + trimStr(line); |
| out.push(line.substring(0, width)); |
| line = line.substring(width); |
| } while (line); |
| }); |
| |
| return out; |
| } |
| |
| |
| /** |
| * The total number of columns for output in a printed help message. |
| * @type {number} |
| * @const |
| */ |
| var TOTAL_WIDTH = 80; |
| |
| |
| /** |
| * Indentation to use between the left column and options string, and between |
| * the options string and help text. |
| * @type {string} |
| * @const |
| */ |
| var IDENTATION = ' '; |
| |
| |
| /** |
| * The maximum column for option text. |
| * @type {number} |
| * @const |
| */ |
| var MAX_HELP_POSITION = 24; |
| |
| |
| /** |
| * Column where the help text should begin. |
| * @type {number} |
| * @const |
| */ |
| var HELP_TEXT_POSITION = MAX_HELP_POSITION + IDENTATION.length; |
| |
| |
| /** |
| * Formats a help message for the given parser. |
| * @param {string} usage The usage string. All occurrences of "$0" will be |
| * replaced with the name of the current program. |
| * @param {!Object.<!Option>} options The options to format. |
| * @return {string} The formatted help message. |
| */ |
| function formatHelpMsg(usage, options) { |
| var prog = path.basename( |
| process.argv[0]) + ' ' + path.basename(process.argv[1]); |
| var help = [ |
| usage.replace(/\$0\b/g, prog), |
| '', |
| 'Options:', |
| formatOption('help', 'Show this message and exit') |
| ]; |
| |
| Object.keys(options).sort().forEach(function(key) { |
| help.push(formatOption(key, options[key].help)); |
| }); |
| |
| help.push(''); |
| |
| return help.join('\n'); |
| } |
| |
| |
| /** |
| * Formats the help message for a single option. Will place the option string |
| * and help text on the same line whenever possible. |
| * @param {string} name The name of the option. |
| * @param {string} helpMsg The option's help message. |
| * @return {string} The formatted option. |
| */ |
| function formatOption(name, helpMsg) { |
| var result = []; |
| var options = IDENTATION + '--' + name; |
| |
| if (options.length > MAX_HELP_POSITION) { |
| result.push(options); |
| result.push('\n'); |
| result.push(wrapStr(helpMsg, TOTAL_WIDTH, |
| repeatStr(' ', HELP_TEXT_POSITION)).join('\n')); |
| } else { |
| var spaceCount = HELP_TEXT_POSITION - options.length; |
| options += repeatStr(' ', spaceCount) + helpMsg; |
| result.push(options.substring(0, TOTAL_WIDTH)); |
| options = options.substring(TOTAL_WIDTH); |
| if (options) { |
| result.push('\n'); |
| result.push(wrapStr(options, TOTAL_WIDTH, |
| repeatStr(' ', HELP_TEXT_POSITION)).join('\n')); |
| } |
| } |
| |
| return result.join(''); |
| } |
| |
| |
| var OPTION_NAME_PATTERN = '[a-zA-Z][a-zA-Z0-9_]*'; |
| var OPTION_NAME_REGEX = new RegExp('^' + OPTION_NAME_PATTERN + '$'); |
| var OPTION_FLAG_REGEX = new RegExp( |
| '^--(' + OPTION_NAME_PATTERN + ')(?:=(.*))?$'); |
| |
| |
| function checkOptionName(name, options) { |
| if ('help' === name) { |
| throw Error('"help" is a reserved option name'); |
| } |
| |
| if (!OPTION_NAME_REGEX.test(name)) { |
| throw Error('option ' + JSON.stringify(name) + ' must match ' + |
| OPTION_NAME_REGEX); |
| } |
| |
| if (name in options) { |
| throw Error('option ' + JSON.stringify(name) + ' has already been defined'); |
| } |
| } |
| |
| |
| function parseOptionValue(name, spec, value) { |
| try { |
| return spec.parse(value); |
| } catch (ex) { |
| ex.message = 'Invalid value for ' + JSON.stringify('--' + name) + |
| ': ' + ex.message; |
| throw ex; |
| } |
| } |
| |
| |
| function parseBoolean(value) { |
| // Empty string if the option was specified without a value; implies true. |
| if (!value) { |
| return true; |
| } |
| |
| var tmp = value.toLowerCase(); |
| if (tmp === 'true' || tmp === '1') { |
| return true; |
| } else if (tmp === 'false' || tmp === '0') { |
| return false; |
| } |
| throw Error(JSON.stringify(value) + ' is not a valid boolean value'); |
| } |
| parseBoolean['default'] = false; |
| |
| |
| function parseNumber(value) { |
| if (/^0x\d+$/.test(value)) { |
| return parseInt(value); |
| } |
| |
| var num = parseFloat(value); |
| if (isNaN(num) || num != value) { |
| throw Error(JSON.stringify(value) + ' is not a valid number'); |
| } |
| return num; |
| } |
| parseNumber['default'] = 0; |
| |
| |
| function parseString(value) { return value; } |
| parseString['default'] = ''; |
| |
| |
| function parseRegex(value) { return new RegExp(value); } |
| |
| |
| |
| /** |
| * A command line option parser. Will automatically parse the command line |
| * arguments to this program upon accessing the {@code options} or {@code argv} |
| * properies. The command line will only be re-parsed if this parser's |
| * configuration has changed since the last access. |
| * @constructor |
| */ |
| function OptionParser() { |
| |
| /** @type {string} */ |
| var usage = OptionParser.DEFAULT_USAGE; |
| |
| /** @type {boolean} */ |
| var mustParse = true; |
| |
| /** @type {!Object.<*>} */ |
| var parsedOptions = {}; |
| |
| /** @type {!Array.<string>} */ |
| var extraArgs = []; |
| |
| /** |
| * Sets the usage string. All occurrences of "$0" will be replaced with the |
| * current executable's name. |
| * @param {string} usageStr The new usage string. |
| * @return {!OptionParser} A self reference. |
| * @this {OptionParser} |
| */ |
| this.usage = function(usageStr) { |
| mustParse = true; |
| usage = usageStr; |
| return this; |
| }; |
| |
| /** |
| * @type {!Object.<{ |
| * help: string, |
| * parse: function(string): *, |
| * required: boolean, |
| * list: boolean, |
| * callback: function(*), |
| * default: * |
| * }>} |
| */ |
| var options = {}; |
| |
| /** |
| * Adds a new option to this parser. |
| * @param {string} name The name of the option. |
| * @param {function(string): *} parseFn The function to use for parsing the |
| * option. If this function has a "default" property, it will be used as |
| * the default value for the option if it was not provided on the command |
| * line. If not specified, the default value will be undefined. The |
| * default value may be overridden using the "default" property in the |
| * option spec. |
| * @param {Object=} opt_spec The option spec. |
| * @return {!OptionParser} A self reference. |
| * @this {OptionParser} |
| */ |
| this.addOption = function(name, parseFn, opt_spec) { |
| checkOptionName(name, options); |
| var spec = opt_spec || {}; |
| |
| // Quoted notation for "default" to bypass annoying IDE syntax bug. |
| var defaultValue = spec['default'] || |
| (!!spec.list ? [] : parseFn['default']); |
| |
| options[name] = { |
| help: spec.help || '', |
| parse: parseFn, |
| required: !!spec.required, |
| list: !!spec.list, |
| callback: spec.callback || function() {}, |
| 'default': defaultValue |
| }; |
| mustParse = true; |
| return this; |
| }; |
| |
| /** |
| * Defines a boolean option. Option values may be one of {'', 'true', 'false', |
| * '0', '1'}. In the case of the empty string (''), the option value will be |
| * set to true. |
| * @param {string} name The name of the option. |
| * @param {Object=} opt_spec The option spec. |
| * @return {!OptionParser} A self reference. |
| * @this {OptionParser} |
| */ |
| this.boolean = function(name, opt_spec) { |
| return this.addOption(name, parseBoolean, opt_spec); |
| }; |
| |
| /** |
| * Defines a numeric option. |
| * @param {string} name The name of the option. |
| * @param {Object=} opt_spec The option spec. |
| * @return {!OptionParser} A self reference. |
| * @this {OptionParser} |
| */ |
| this.number = function(name, opt_spec) { |
| return this.addOption(name, parseNumber, opt_spec); |
| }; |
| |
| /** |
| * Defines a path option. Each path will be resolved relative to the |
| * current working directory, if not absolute. |
| * @param {string} name The name of the option. |
| * @param {Object=} opt_spec The option spec. |
| * @return {!OptionParser} A self reference. |
| * @this {OptionParser} |
| */ |
| this.path = function(name, opt_spec) { |
| return this.addOption(name, path.resolve, opt_spec); |
| }; |
| |
| /** |
| * Defines a basic string option. |
| * @param {string} name The name of the option. |
| * @param {Object=} opt_spec The option spec. |
| * @return {!OptionParser} A self reference. |
| * @this {OptionParser} |
| */ |
| this.string = function(name, opt_spec) { |
| return this.addOption(name, parseString, opt_spec); |
| }; |
| |
| /** |
| * Defines a regular expression based option. |
| * @param {string} name The name of the option. |
| * @param {Object=} opt_spec The option spec. |
| * @return {!OptionParser} A self reference. |
| * @this {OptionParser} |
| */ |
| this.regex = function(name, opt_spec) { |
| return this.addOption(name, parseRegex, opt_spec); |
| }; |
| |
| /** |
| * Returns the parsed command line options. |
| * |
| * <p>The command line arguments will be re-parsed if this parser's |
| * configuration has changed since the last access. |
| * |
| * @return {!Object.<*>} The parsed options. |
| */ |
| this.__defineGetter__('options', function() { |
| parse(); |
| return parsedOptions; |
| }); |
| |
| /** |
| * Returns the remaining command line arguments after all options have been |
| * parsed. |
| * |
| * <p>The command line arguments will be re-parsed if this parser's |
| * configuration has changed since the last access. |
| * |
| * @return {!Array.<string>} The remaining command line arguments. |
| */ |
| this.__defineGetter__('argv', function() { |
| parse(); |
| return extraArgs; |
| }); |
| |
| /** |
| * Parses a list of arguments. After parsing, the options property of this |
| * object will contain the value for each parsed flags, and the argv |
| * property will contain all remaining command line arguments. |
| * @throws {Error} If the arguments could not be parsed. |
| */ |
| this.parse = parse; |
| |
| /** |
| * Returns a formatted help message for this parser. |
| * @return {string} The help message for this parser. |
| */ |
| this.getHelpMsg = getHelpMsg; |
| |
| function getHelpMsg() { |
| return formatHelpMsg(usage, options); |
| } |
| |
| function parse() { |
| if (!mustParse) { |
| return; |
| } |
| |
| parsedOptions = {}; |
| extraArgs = []; |
| |
| var args = process.argv.slice(2); |
| var n = args.length; |
| |
| try { |
| for (var i = 0; i < n; ++i) { |
| var arg = args[i]; |
| if (arg === '--') { |
| extraArgs = args.slice(i + 1); |
| break; |
| } |
| |
| var match = arg.match(OPTION_FLAG_REGEX); |
| if (match) { |
| // Special case --help. |
| if (match[1] === 'help') { |
| printHelpAndDie('', 0); |
| } |
| parseOption(match); |
| } else { |
| extraArgs = args.slice(i + 1); |
| break; |
| } |
| } |
| } catch (ex) { |
| printHelpAndDie(ex.message, 1); |
| } |
| |
| for (var name in options) { |
| var option = options[name]; |
| if (!(name in parsedOptions)) { |
| if (option.required) { |
| printHelpAndDie('Missing required option: --' + name, 1); |
| } |
| parsedOptions[name] = option.list ? [] : option['default']; |
| } |
| } |
| |
| mustParse = false; |
| } |
| |
| function printHelpAndDie(errorMessage, exitCode) { |
| process.stdout.write(errorMessage + '\n'); |
| process.stdout.write(getHelpMsg()); |
| process.exit(exitCode); |
| } |
| |
| function parseOption(match) { |
| var option = options[match[1]]; |
| if (!option) { |
| throw Error(JSON.stringify('--' + match[1]) + ' is not a valid option'); |
| } |
| |
| var value = match[2]; |
| if (typeof value !== 'undefined') { |
| value = parseOptionValue(match[1], option, value); |
| } else if (option.parse === parseBoolean) { |
| value = true; |
| } else { |
| throw Error('Option ' + JSON.stringify('--' + option.name) + |
| 'requires an operand'); |
| } |
| |
| option.callback(value); |
| |
| if (option.list) { |
| var array = parsedOptions[match[1]] || []; |
| parsedOptions[match[1]] = array; |
| array.push(value); |
| } else { |
| parsedOptions[match[1]] = value; |
| } |
| } |
| } |
| |
| |
| /** |
| * The default usage message for an option parser. |
| */ |
| Object.defineProperty(OptionParser, 'DEFAULT_USAGE', { |
| value: 'Usage: $0 [options] [arguments]', |
| writable: false, |
| enumerable: true, |
| configurable: false |
| }); |
| |
| |
| /** @type {!OptionParser} */ |
| exports.OptionParser = OptionParser; |
| |
| if (module === require.main) { |
| var parser = new OptionParser(). |
| boolean('bool_flag', { |
| help: 'This is a boolean flag' |
| }). |
| number('number_flag', { |
| help: 'This is a number flag' |
| }). |
| path('path_flag', { |
| help: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + |
| 'Nam pharetra pellentesque augue ut auctor. Mauris eget est ' + |
| 'vitae quam imperdiet mollis. Ut lorem lorem, commodo et ' + |
| 'interdum cursus, convallis quis mi. Aenean facilisis adipiscing ' + |
| 'imperdiet. Etiam tristique facilisis ullamcorper. Vestibulum ' + |
| 'at mauris quis eros lobortis viverra vel non massa. ' + |
| 'Aenean eu sodales quam.' |
| }). |
| string('the_quick_brown_fox_jumped_over_the_very_lazy_dog', { |
| help: 'The quick brown fox jumped over the very lazy dog' |
| }); |
| |
| console.log('Options are: '); |
| for (var key in parser.options) { |
| console.log(' --' + key + '=' + parser.options[key]); |
| } |
| console.log('Args are: '); |
| console.log(' ', parser.argv.join(' ')); |
| } |