Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Adding the last two days worth of work

  • Loading branch information...
commit 087fdaba6ac9e4b45550c0f089ad7101785c50de 1 parent ea49161
@fidian fidian authored
View
30 LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2102 individual committers of the code
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+Except as contained in this notice, the name(s) of the above copyright holders
+shall not be used in advertising or otherwise to promote the sale, use or other
+dealings in this Software without prior written authorization.
+
+The end-user documentation included with the redistribution, if any, must
+include the following acknowledgment: "This product includes software developed
+by contributors", in the same place and form as other third-party
+acknowledgments. Alternately, this acknowledgment may appear in the software
+itself, in the same form and location as other such third-party
+acknowledgments.
View
157 OptionParameter.md
@@ -0,0 +1,157 @@
+An instance of the OptionParameter class represents a single option. It knows
+about all of the ways that option can get specified, if it takes a value, and
+what values were passed in from the command-line.
+
+Constructor($short, $long, $message = null)
+-------------------------------------------
+
+Create a new instance of the class. $short and $long indicate what options are
+allowed. Multiple short and long options can be specified by passing an array
+of allowed options. A wildcard short option of '*' or '-' will mean that this
+option should match any short or long option that didn't match anything else.
+
+$short can be an array of strings or just a single string. Each string should
+be exactly 1 character long.
+
+$long can be an array of strings or just a single string.
+
+$message is the help message to be displayed for this option.
+
+action($callback)
+-----------------
+
+Set an action to be performed when you specify this parameter. In PHP,
+$callback can be a closure, function name, array containing a class name and a
+method, or anything else that's acceptable to call_user_func(). In JavaScript,
+this can only be a closure.
+
+If an invalid $callback is used, an Exception is thrown.
+
+argument($name, $required = true)
+---------------------------------
+
+Specify that this parameter takes a value from the command line.
+
+$name is a string for the help message. It isn't used anywhere else.
+
+$required is a boolean and will default to true if not passed.
+
+
+count()
+-------
+
+Returns the number of times this parameter was used on the command line.
+
+
+handleArgument($option, $value = null)
+--------------------------------------
+Performs validation with the optionally specified validation callback. If that
+fails, will throw an Exception. Once it passes the validation step, an action
+callback may be executed if one was assigned via action(). Lastly, this adds
+the value to the getopt version of the arguments.
+
+This is intended to be only used by OptionParser.
+
+getopt()
+--------
+
+Returns an array to be used by OptionParser->getopt().
+
+getWidth()
+----------
+
+Returns the terminal width, employing whatever tricks that might work for you.
+Defaults to 80 if it can't find an actual terminal's width.
+
+help($pad = 16, $gutter = 2, $width = null)
+-------------------------------------------
+
+Returns a string describing this option and how to invoke it.
+
+$pad is the amount of space on the left for indenting the help option. It
+defaults to 16 spaces.
+
+$gutter is the minimum amount of space required at the end of the short and
+long options and before the beginning of the help text. If there isn't this
+much space, the options will be put on a separate line.
+
+$width is the size of the terminal. The default is null, which means the
+option should call getWidth() and try to figure out the terminal width itself.
+
+matchAutocomplete($arg)
+-----------------------
+
+Returns an array of options that might match if the user just didn't finish the
+entire long option. for instance, the $arg of "lo" would match "--long".
+
+This is only intended to be called from OptionParser.
+
+matchLongArgument($arg)
+-----------------------
+
+Returns true if the argument exactly matches a long argument for this
+parameter.
+
+matchShortArgument($arg)
+------------------------
+
+Returns true if the argument exactly matches a short argument for this
+parameter.
+
+
+matchWildcard()
+---------------
+
+Returns true if this parameter is considered a "wildcard" parameter. This
+means that a short option is either '*' or '-'.
+
+usesArgument()
+--------------
+
+Returns whether or not the parameter takes an argument. Return value is
+language-specific.
+
+This is intended to only be called from OptionParser.
+
+validateCallback($callback)
+---------------------------
+
+Returns true if the callback is valid. In PHP, this checks to see if it
+is_callable(). For JavaScript, we check to make typeof $callback is
+"function".
+
+Throws an Exception if an invalid callback is specified.
+
+validation($callback)
+---------------------
+
+Set a validation function to be performed when you specify this parameter. In
+PHP, $callback can be a closure, function name, array containing a class name
+and a method, or anything else that's acceptable to call_user_func(). In
+JavaScript, this can only be a closure.
+
+The validation function will take a single parameter, which is the value
+specified for the parameter. If the value is valid, null should be returned.
+Otherwise, return an informative message as a string, which is set as the
+Exception's message.
+
+If an invalid $callback is used, an Exception is thrown.
+
+value()
+-------
+
+For a parameter that takes values, returns the last value specified. Otherwise
+return the number of times this parameter was specified. Similar to values().
+
+values()
+--------
+
+For a parameter that takes values, returns an array that contains all values
+ever set on the command line. Otherwise returns the number of times the
+parameter was specified.
+
+wrap($str, $pad, $width)
+------------------------
+
+Rewraps $str to fit within $width. When newlines are added, indents the next
+line with $pad spaces.
View
86 OptionParser.md
@@ -0,0 +1,86 @@
+The OptionParser object handles keeping track of OptionParameters and parsing
+of options.
+
+The class also will set up named options so you can access the OptionParameter
+objects as just properties of the object directly. In PHP, the following code
+would be equivalent for a parameter named "testThing".
+
+ $parameter = $parser->get("testThing");
+ $parameter = $parser->testThing;
+
+addOption($short, $long, $helpMessage = null, $name = null)
+-----------------------------------------------------------
+
+Creates a new OptionParameter. $short, $long and $helpMessage are passed
+directly to OptionParameter's constructor. $name, if specified, will assign a
+name to the object. Named objects can be retrieved directly as properties and
+via the get() method.
+
+autocomplete($boolean)
+----------------------
+
+Turn on or off the autocomplete functionality, which lets long options match
+shorter phrases, like "--long" would match "--longOption". By default,
+autocomplete is disabled because it could cause confusion or problems.
+
+get($name)
+----------
+
+Return a named parameter. If no parameter exists with the given name, an
+Exception is thrown.
+
+getopt()
+--------
+
+Return an array or object similar to how PHP's getopt() function works. It
+does this by calling all of the OptionParameter objects that were added and
+combining their results.
+
+getValue($name)
+---------------
+
+Gets the value from the named parameter. The following three versions of PHP
+code are equivalent.
+
+ $value = $parser->get('test')->value();
+ $value = $parser->test->value();
+ $value = $parser->getValue('test');
+
+help()
+------
+
+Builds a help message that describes all of the options that are available.
+Returns it as a large string with a trailing newline. Builds the message by
+calling help() on all of the OptionParameters available.
+
+helpAction($commandLine = '[options]')
+--------------------------------------
+
+Builds a very generic help callback that can be used with
+OptionParameter->action(). The description of parameters that can be passed on
+the command line can be overridden by providing a different $commandLine
+variable.
+
+
+parse($options = null)
+----------------------
+
+Parses the options passed to the program. If $options is null (as I expect it
+would be), the options are grabbed from $GLOBALS['argv'] in PHP or process.argv
+in JavaScript. The program name is also parsed out before options.
+
+If $options is an array, the program name is not parsed and the options are
+used as-is.
+
+programName()
+-------------
+
+Returns the program name, if one was parsed out by not passing anything to the
+parse() method.
+
+scanAll($boolean)
+-----------------
+
+If true, scan for options across the entire command line. If false, stop
+parsing at the first unknown option. The default is true. To better emulate
+PHP's getopt() implementation, this needs to be set to false.
View
163 README.md
@@ -0,0 +1,163 @@
+OptionParser is a library to help you parse command-line options, similar to
+how getopt works. An effort is made to make it POSIX compliant and easy for
+people to use. Features of many other implementations have been integrated in
+order to provide significant flexibility and to make it easier to use.
+
+Examples in this guide are written in PHP, but should be trivial for one to
+port to JavaScript.
+
+Features
+========
+* -x (short options)
+* -xxxyxxz (combined and repeating short options)
+* --long (long option)
+* -x=Value, --long=Value (optional and required values)
+* -xValue --long Value (special format for required values)
+* -- (signify the end of options)
+* Can stop on the first unparsed option
+* Can autocomplete long options
+* Returns unparsed options
+* Flexible option handling
+
+Languages Supported
+===================
+
+* PHP http://php.net
+* JavaScript via node.js http://nodejs.org/
+
+How to use
+==========
+
+First, create the parser object:
+
+ $parser = new OptionParser();
+
+Next, you add some options. I'll jump right in and add the standard "help"
+option, triggered with either -h or --help.
+
+ $parser->addOption('h', 'help', 'Display this help message')
+ ->action($parser->helpAction());
+
+Finally, parse the command line options.
+
+ $parser->parse();
+
+How To Use Parameters
+=====================
+
+The whole point of the library is to make it really easy to handle the parsing
+of options to your program. Let's run through a few examples here:
+
+Toggle a Flag With A Callback
+-----------------------------
+
+In PHP you can pass more than just callbacks; names of functions, an array with
+the class name and method, and anything else that works with call_user_func().
+For JavaScript, this only works for closures. Let's have this option only work
+with the short option "-f".
+
+ $flagWasSet = false;
+
+ $parser->addOption('f', null, 'Toggle a flag')
+ ->action(function () use (&$flagWasSet) {
+ $flagWasSet = true;
+ });
+
+Pass a Required Value
+---------------------
+
+Many options need a specific value, such as an input file to process. Here is
+another option that is specified by "-i" or "--input" that requires a filename
+of the input file. It uses a callback, just like the above example.
+
+ $inputFile = 'php://stdin';
+
+ $parser->addOption('i', 'input', 'Specify an input file')
+ ->argument('FILE') // You can name the argument anything you like
+ ->action(function ($value) use (&$inputFile) {
+ $inputFile = $value;
+ });
+
+Optional Value and Parameter Object
+-----------------------------------
+
+Closures in PHP are not quite the same as JavaScript since you need to
+explicitly list the variables that are in scope for the execution of the
+function. An alternative would be to use the returned object from setting up
+the option on the parser. Here, we add a debug option that lets you set the
+debug level, but can default to 5 if you don't set one explicitly.
+
+ $debugLevel = 0;
+
+ $debugOption = $parser->addOption(null, 'debug',
+ 'Sets the debug level; if set, default is 5')
+ ->argument('Level', false); // "false" makes this optional
+
+ // Don't forget to set up the other options here
+
+ $parser->parse();
+
+ // Now use the $debugOption object to set the debug value
+ if ($debugOption->count()) {
+ $debugLevel = $debugOption->value();
+ }
+
+Named Parameters
+----------------
+
+Keeping references to the objects can be tedious. Here is the above example
+altered to name the parameter and then use the named parameter. I'm naming the
+parameter "dddd" to help contrast against the previous code.
+
+ $debugLevel = 0;
+
+ $parser->addOption(null, 'debug',
+ 'Sets the debug level, default is 5', 'dddd')
+ ->argument('Level', false); // "false" makes this optional
+
+ // Don't forget to set up the other options here
+
+ $parser->parse();
+
+ // Now use the $debugOption object to set the debug value
+ if ($parser->dddd->count()) {
+ $debugLevel = $debugOption->value();
+ }
+
+GetOpt
+------
+
+Lastly, PHP has a unique format for handling command-line arguments using
+the built-in function getopt(). After setting up options and calling
+$parser->parse(), you can get back an array that mimics getopt()'s return
+value. This should make it easier to plug this parser into your code and
+benefit from the better option handling without retooling anything after the
+call to getopt() that you'd normally make.
+
+ // Set up options and then call parse()
+ $parser->parse();
+
+ // Get back an array of options like PHP's getopt()
+ $options = $parser->getopt();
+
+Unparsed Options
+================
+
+If you plan on making a program that takes a list of files or needs to work on
+the options that were passed to the program but were not parsed by
+OptionParser, that's really simple:
+
+ $unparsed = $parser->parse();
+
+This will contain an array of options, split out into individual options. If
+you passed "-abcd" to your program and it handled "-a", "-b", and "-c", then
+$unparsed would be an array that only contains "-d".
+
+More Reading
+============
+
+You can check out OptionParser.md and OptionParameter.md for more documentation
+regarding how those two classes work.
+
+Reference implementations are available in the examples/ directory in the
+repository.
View
54 examples/nodejs.js
@@ -0,0 +1,54 @@
+#!/usr/bin/node
+
+'use strict';
+
+var OP = require('../nodejs/OptionParser');
+var parser = new OP.OptionParser();
+var requiredValue = null;
+
+/* Method 1 for handling options - use a callback */
+parser.addOption('h', 'help', 'Show a help message')
+ .action(parser.helpAction()); // We provide a quick help action
+
+parser.addOption('r', 'required', 'Specify a required option')
+ .argument('OPTION')
+ .action(function (v) {
+ // Can handle callbacks
+ requiredValue = v;
+ console.log("Required value: " + v);
+ });
+
+/* Method 2 for handling options - save a reference to the object */
+var optional = parser.addOption('o', 'optional', 'Add an optional value')
+ .argument('VALUE', false); // Parse these by hand later
+
+/* Method 3 for handling options - name the option */
+parser.addOption('f', 'flag', 'Turn on some flag', 'flag');
+
+parser.addOption('d', 'debug', null, 'debug'); // Hidden option
+
+try {
+ var unparsed = parser.parse();
+
+ /* Second part of "Method 2" */
+ optional.values().forEach(function (value) {
+ console.log("Optional value: " + JSON.stringify(value));
+ });
+
+ /* Second part of "Method 3" */
+ console.log("Times the --flag was specified: " + parser.flag.count().toString());
+ console.log("Times the hidden --debug option was used: " + parser.debug.count().toString());
+ console.log("");
+
+ /* Add some diagnostic information */
+ console.log("getopt:");
+ console.log(parser.getopt());
+ console.log("");
+ console.log("Unparsed:");
+ console.log(unparsed);
+} catch (e) {
+ console.log('Exception caught:');
+ console.log(e);
+ throw e;
+}
+
View
55 examples/php.php
@@ -0,0 +1,55 @@
+#!/usr/bin/php
+<?php
+
+require_once(__DIR__ . '/../php/OptionParser.class.php');
+
+$parser = new OptionParser();
+
+$requiredValue = null;
+
+/* Method 1 for handling options - use a callback */
+$parser->addOption('h', 'help', 'Show a help message')
+ ->action($parser->helpAction()); // We provide a quick help action
+
+$parser->addOption('r', 'required', 'Specify a required option')
+ ->argument('OPTION')
+ ->action(function ($v) use (&$requiredValue) {
+ // Can handle values in closures and anything else that's callable
+ // by call_user_func()
+ $requiredValue = $v;
+ echo "Required value: $v\n";
+ });
+
+/* Method 2 for handling options - save a reference to the object */
+$optional = $parser->addOption('o', 'optional', 'Add an optional value')
+ ->argument('VALUE', false); // Parse these by hand later
+
+/* Method 3 for handling options - name the option */
+$parser->addOption('f', 'flag', 'Turn on some flag', 'flag');
+
+$parser->addOption('d', 'debug', null, 'debug'); // Hidden option
+
+try {
+ $unparsed = $parser->parse();
+
+ /* Second part of "Method 2" */
+ foreach ($optional->values() as $value) {
+ echo "Optional value: " . json_encode($value) . "\n";
+ }
+
+ /* Second part of "Method 3" */
+ echo "Times the --flag was specified: " . $parser->flag->count() . "\n";
+ echo "Times the hidden --debug option was used: " . $parser->debug->count() . "\n";
+ echo "\n";
+
+ /* Add some diagnostic information */
+ echo "getopt:\n";
+ echo json_encode($parser->getopt()) . "\n";
+ echo "\n";
+ echo "Unparsed:\n";
+ echo json_encode($unparsed) . "\n";
+} catch (Exception $e) {
+ echo "Exception caught:\n";
+ echo $e->getMessage() . "\n\n";
+}
+
View
249 nodejs/OptionParameter.js
@@ -0,0 +1,249 @@
+'use strict';
+
+var undefined = (function (u) { return u; })();
+
+var OptionParameter = function (shortOptions, longOptions, message) {
+ this.actionClosure = null;
+ this.argumentName = null;
+ this.argumentRequired = 'none';
+ this.getoptFormat = {};
+ this.optionHelp = null;
+ this.optionLong = [];
+ this.optionShort = [];
+ this.validationClosure = null;
+ this.valuesParsed = [];
+
+ if (shortOptions) {
+ if (Array.isArray(shortOptions)) {
+ this.optionShort = shortOptions;
+ } else {
+ this.optionShort = [ shortOptions ];
+ }
+ }
+
+ if (longOptions) {
+ if (Array.isArray(longOptions)) {
+ this.optionLong = longOptions;
+ } else {
+ this.optionLong = [ longOptions ];
+ }
+ }
+
+ if (message) {
+ this.optionHelp = message;
+ }
+};
+
+OptionParameter.prototype.action = function (runThis) {
+ this.validateCallback(runThis);
+ this.actionClosure = runThis;
+ return this;
+};
+
+OptionParameter.prototype.argument = function (name, required) {
+ this.argumentName = name;
+
+ if (required === undefined || required) {
+ this.argumentRequired = 'required';
+ } else {
+ this.argumentRequired = 'optional';
+ }
+
+ return this;
+};
+
+OptionParameter.prototype.count = function () {
+ return this.valuesParsed.length;
+};
+
+OptionParameter.prototype.handleArgument = function (option, value) {
+ if (value !== null && this.validationClosure) {
+ var result = this.validationClosure($value);
+
+ if (result !== null) {
+ var exception = new Error(result);
+ exception.argument = this;
+ throw exception;
+ }
+ }
+
+ if (this.actionClosure) {
+ this.actionClosure(value);
+ }
+
+ this.valuesParsed.push(value);
+ var getoptValue = false;
+
+ if (this.argumentRequired != 'none' && value !== null) {
+ getoptValue = value;
+ }
+
+ if (this.getoptFormat[option] === undefined) {
+ this.getoptFormat[option] = getoptValue;
+ } else {
+ if (! Array.isArray(this.getoptFormat[option])) {
+ this.getoptFormat[option] = [ this.getoptFormat[option] ];
+ }
+
+ this.getoptFormat[option].push(getoptValue);
+ }
+
+ return this;
+};
+
+OptionParameter.prototype.getopt = function () {
+ return this.getoptFormat;
+};
+
+OptionParameter.prototype.getWidth = function () {
+ if (process.stdout.getWindowSize) {
+ return + (process.stdout.getWindowSize()[0]);
+ }
+
+ return 80;
+};
+
+OptionParameter.prototype.help = function (pad, gutter, width) {
+ var doDefault = function (input, defaultValue) {
+ if (input === undefined || +input < 0) {
+ return defaultValue;
+ }
+
+ return input;
+ };
+
+ pad = doDefault(pad, 16);
+ gutter = doDefault(gutter, 2);
+ width = doDefault(width, this.getWidth() - gutter);
+ var options = [];
+ var buffer = '';
+
+ if (this.optionHelp === null) {
+ return '';
+ }
+
+ this.optionShort.forEach(function (item) {
+ options.push('-' + item);
+ });
+ this.optionLong.forEach(function (item) {
+ options.push('--' + item);
+ });
+
+ buffer = options.join(', ');
+
+ if (this.argumentRequired == 'required') {
+ buffer += ' ' + this.argumentName;
+ } else if (this.argumentRequired == 'optional') {
+ buffer += '[=' + this.argumentName + ']';
+ }
+
+ var help = this.wrap(Array(pad + 1).join(' ') + this.optionHelp, pad, width);
+
+ if (buffer.length > pad - gutter) {
+ help = this.wrap(buffer, 0, width) + help;
+ } else {
+ help = buffer + help.substr(buffer.length);
+ }
+
+ return help;
+};
+
+OptionParameter.prototype.matchAutocomplete = function (arg) {
+ var hits = {};
+
+ this.optionLong.forEach(function (o) {
+ if (o.substr(0, arg.length) == arg) {
+ hits[o] = this;
+ }
+ });
+
+ return hits;
+};
+
+OptionParameter.prototype.matchLongArgument = function (arg) {
+ return this.optionLong.some(function (item) {
+ return item == arg;
+ });
+};
+
+OptionParameter.prototype.matchShortArgument = function (arg) {
+ return this.optionShort.some(function (item) {
+ return item == arg;
+ });
+};
+
+OptionParameter.prototype.matchWildcard = function () {
+ return this.optionShort.some(function (item) {
+ return item == '*' || item == '-';
+ });
+};
+
+OptionParameter.prototype.usesArgument = function () {
+ return this.argumentRequired;
+};
+
+OptionParameter.prototype.validateCallback = function (runThis) {
+ if (typeof runThis == 'function') {
+ return;
+ }
+
+ throw new Error('Invalid closure specified');
+};
+
+OptionParameter.prototype.validation = function (runThis) {
+ this.validateCallback(runThis);
+ this.validationClosure = runthis;
+ return this;
+};
+
+OptionParameter.prototype.value = function () {
+ var ret = this.values();
+
+ if (Array.isArray(ret)) {
+ ret = ret.pop();
+ }
+
+ return ret;
+};
+
+OptionParameter.prototype.values = function () {
+ if (this.argumentName === null) {
+ return this.count();
+ }
+
+ return this.valuesParsed;
+};
+
+OptionParameter.prototype.wrap = function (str, pad, width) {
+ var lines = [];
+ str = str.replace(/[ \t\r\n]*$/, '');
+ var spaces = Array(pad + 1).join(' ');
+ var newlineMatch;
+
+ while (str.length) {
+ newlineMatch = str.match(/\r?\n|\r/);
+
+ if (newlineMatch && newlineMatch.index <= width) {
+ lines.push(str.substr(0, newlineMatch.index - 1));
+ str = str.substr(newlineMatch.index + newlineMatch[0].length);
+ } else {
+ var line = str.substr(0, width);
+ var spacePos = line.lastIndexOf(' ');
+
+ if (spacePos > width * 0.8) {
+ line = line.substr(0, spacePos + 1);
+ }
+
+ str = str.substr(line.length);
+ lines.push(line.replace(/[ \t\r\n]*$/, ''));
+ }
+
+ if (str.length) {
+ str = spaces + str;
+ }
+ }
+
+ return lines.join("\n") + "\n";
+};
+
+exports.OptionParameter = OptionParameter;
View
297 nodejs/OptionParser.js
@@ -0,0 +1,297 @@
+'use strict';
+
+var OptionParameter = require('./OptionParameter');
+
+var undefined = (function (u) { return u; })();
+
+var OptionParser = function () {
+ this.doAutocomplete = false;
+ this.doScanAll = true;
+ this.parameters = [];
+ this.programNameParsed = null;
+};
+
+OptionParser.prototype.addOption = function (shortName, longName, helpMessage, referenceName) {
+ var op = new OptionParameter.OptionParameter(shortName, longName, helpMessage);
+ this.parameters.push(op);
+
+ if (referenceName) {
+ this[referenceName] = op;
+ }
+
+ return op;
+};
+
+OptionParser.prototype.autocomplete = function (bool) {
+ this.doAutocomplete = !! bool; // Force bool to be a boolean
+ return this;
+};
+
+OptionParser.prototype.get = function (name) {
+ if (! name || ! this[name]) {
+ throw new Error('No parameter named ' . name);
+ }
+
+ return this[name];
+};
+
+OptionParser.prototype.getopt = function () {
+ var out = {};
+
+ this.parameters.forEach(function (p) {
+ var getopt = p.getopt();
+
+ for (var i in getopt) {
+ out[i] = getopt[i];
+ }
+ });
+
+ return out;
+};
+
+OptionParser.prototype.getValue = function (name) {
+ return this.get(name).value();
+};
+
+OptionParser.prototype.help = function () {
+ var out = '';
+
+ this.parameters.forEach(function (p) {
+ out += p.help();
+ });
+
+ return out;
+};
+
+OptionParser.prototype.helpAction = function (cmdline) {
+ if (! cmdline) {
+ cmdline = "[options]";
+ }
+
+ var myself = this;
+ return function () {
+ console.log("Usage:");
+ console.log("\t" + myself.programName() + " " + cmdline);
+ console.log("");
+ console.log("Available Options:");
+ console.log(myself.help());
+ process.exit(0);
+ };
+};
+
+OptionParser.prototype.parse = function (options) {
+ if (arguments.length < 1) {
+ options = process.argv.slice(0);
+
+ // First option is "node", second is program name
+ this.programNameParsed = options.shift() + ' ' + options.shift();
+ }
+
+ if (! Array.isArray(options)) {
+ throw new Error('Unable to parse options - they are not an array');
+ }
+
+ var unparsed = [];
+
+ while (options.length) {
+ var current = options[0];
+ var arg = null;
+ var found = null;
+
+ if (current == '--') {
+ // Designator for the end of arguments
+ options.shift(); // Shift off "--"
+ return unparsed.concat(options);
+ }
+
+ if (current.substr(0, 2) == '--') {
+ // Long option
+ arg = current.substr(2);
+ var value = null;
+ var matches = arg.match(/^([^=]+)=(.*)$/);
+
+ if (matches) {
+ arg = matches[1];
+ value = matches[2];
+ }
+
+ // Match a regular argument
+ this.parameters.forEach(function (p) {
+ if (p.matchLongArgument(arg)) {
+ found = p;
+ }
+ });
+
+ // Autocomplete if possible
+ if (! found && this.doAutocomplete) {
+ var suggestionCount = 0;
+ var suggestionName = null;
+ var suggestionObject = null;
+
+ this.parameters.forEach(function (p) {
+ var hits = p.matchAutocomplete(arg);
+
+ for (var i in hits) {
+ suggestionCount ++;
+ suggestionName = i;
+ suggestionObject = hits[i];
+ }
+ });
+
+ if (suggestionCount == 1) {
+ arg = suggestionName;
+ found = suggestionObject;
+ }
+ }
+
+ // Wildcard if possible
+ if (! found) {
+ this.parameters.forEach(function (p) {
+ if (p.matchWildcard()) {
+ found = p;
+ }
+ });
+ }
+
+ options.shift();
+
+ if (! found) {
+ var reconstructed = '--' + arg;
+
+ if (value !== null) {
+ reconstructed += '=' + value;
+ }
+
+ unparsed.push(reconstructed);
+ } else {
+ switch (found.usesArgument()) {
+ case 'none':
+ // Must not use '=' syntax
+ // Treat it as another parameter if found
+ found.handleArgument(arg, null);
+
+ if (value !== null) {
+ options.unshift('=' + value);
+ }
+
+ break;
+
+ case 'required':
+ // Can use '=' syntax or next argument
+ if (value === null) {
+ if (! options.length) {
+ throw new Error('Value needed for --' + arg);
+ }
+
+ value = options.shift();
+ }
+
+ found.handleArgument(arg, value);
+ break;
+
+ default: // optional
+ // Must use '=' syntax if a value is to be specified
+ found.handleArgument(arg, value);
+ break;
+ }
+ }
+ } else if (current.charAt(0) == '-') {
+ // Short option
+ arg = current.charAt(1);
+ var rest = current.substr(2);
+
+ // Match a regular argument
+ this.parameters.forEach(function (p) {
+ if (p.matchShortArgument(arg)) {
+ found = p;
+ }
+ });
+
+ // Wildcard if possible
+ if (! found) {
+ this.parameters.forEach(function (p) {
+ if (p.matchWildcard()) {
+ found = p;
+ }
+ });
+ }
+
+ if (! found) {
+ unparsed.push('-' + arg);
+ options.shift();
+
+ if (rest.length) {
+ options.unshift('-' + rest);
+ }
+ } else {
+ switch (found.usesArgument()) {
+ case 'none':
+ // Must not use '=' syntax
+ found.handleArgument(arg, null);
+ options.shift();
+
+ if (rest.length) {
+ options.unshift('-' + rest);
+ }
+
+ break;
+
+ case 'required':
+ // Can use '=' syntax, the rest of the current arg
+ // or next argument
+ options.shift();
+
+ if (rest.length) {
+ if (rest.charAt(0) == '=') {
+ rest = rest.substr(1);
+ }
+ } else {
+ if (! options.length) {
+ throw new Error('Value needed for -' . arg);
+ }
+
+ rest = options.shift();
+ }
+
+ found.handleArgument(arg, rest);
+ break;
+
+ case 'optional':
+ // Must use '=' syntax
+ options.shift();
+
+ if (rest.charAt(0) == '=') {
+ found.handleArgument(arg, rest.substr(1));
+ } else {
+ found.handleArgument(arg, null);
+
+ if (rest.length) {
+ options.unshift('-' + rest);
+ }
+ }
+ break;
+
+ default:
+ throw new Error('Invalid OptionParameter argument type: ' + found.usesArgument());
+ }
+ }
+ } else if (! this.doScanAll) {
+ // Stopping at first unknown
+ return unparsed.concat(options);
+ } else {
+ unparsed.push(options.shift());
+ }
+ }
+
+ return unparsed;
+};
+
+OptionParser.prototype.programName = function () {
+ return this.programNameParsed;
+};
+
+OptionParser.prototype.scanAll = function (bool) {
+ this.doScanAll = !! bool; // Force bool to be a boolean
+};
+
+exports.OptionParser = OptionParser;
View
61 php/OptionParameter.class.php
@@ -4,15 +4,15 @@ class OptionParameter {
const ARGUMENT_NONE = 0;
const ARGUMENT_REQUIRED = 1;
const ARGUMENT_OPTIONAL = 2;
- protected $action = null;
+ protected $actionCallback = null;
protected $argumentName = null;
protected $argumentRequired = self::ARGUMENT_NONE;
protected $getoptFormat = array();
protected $optionHelp = null;
protected $optionLong = array();
protected $optionShort = array();
- protected $validation = null;
- protected $values = array();
+ protected $validationCallback = null;
+ protected $valuesParsed = array();
public function __construct($short, $long, $message = null) {
if ($short) {
@@ -30,7 +30,7 @@ public function __construct($short, $long, $message = null) {
public function action($runthis) {
$this->validateCallback($runthis);
- $this->action = $runthis;
+ $this->actionCallback = $runthis;
return $this;
}
@@ -47,12 +47,12 @@ public function argument($name, $required = true) {
}
public function count() {
- return count($this->values);
+ return count($this->valuesParsed);
}
public function handleArgument($option, $value = null) {
- if (! is_null($value) && $this->validation) {
- $result = call_user_func($this->validation, $value);
+ if (! is_null($value) && $this->validationCallback) {
+ $result = call_user_func($this->validationCallback, $value);
if (! is_null($result)) {
$exception = new Exception($result);
@@ -61,7 +61,11 @@ public function handleArgument($option, $value = null) {
}
}
- $this->values[] = $value;
+ if ($this->actionCallback) {
+ call_user_func($this->actionCallback, $value);
+ }
+
+ $this->valuesParsed[] = $value;
$getoptValue = false;
if ($this->argumentRequired != self::ARGUMENT_NONE && ! is_null($value)) {
@@ -85,10 +89,41 @@ public function getopt() {
return $this->getoptFormat;
}
- public function help($pad = 16, $gutter = 2, $width = 76) {
+ protected function getWidth() {
+ static $cached = null;
+
+ if (! is_null($cached)) {
+ return $cached;
+ }
+
+ $output = `tput cols`;
+
+ if ($output > 0) {
+ $cached = $output * 1;
+ return $cached;
+ }
+
+ $output = `stty -a`;
+
+ if (preg_match('/ columns ([0-9]+)/', $output, $matches)) {
+ if ($matches[1] > 0) {
+ $cached = $matches[1] * 1;
+ return $cached;
+ }
+ }
+
+ $cached = 80;
+ return $cached;
+ }
+
+ public function help($pad = 16, $gutter = 2, $width = null) {
$options = array();
$buffer = '';
+ if (is_null($width)) {
+ $width = $this->getWidth() - $gutter;
+ }
+
if (is_null($this->optionHelp)) {
return '';
}
@@ -125,7 +160,7 @@ public function matchAutocomplete($arg) {
foreach ($this->optionLong as $o) {
if (substr($o, 0, strlen($arg)) == $arg) {
- $hits[] = $o;
+ $hits[$o] = $this;
}
}
@@ -166,7 +201,7 @@ protected function validateCallback($runthis) {
public function validation($runthis) {
$this->validateCallback($runthis);
- $this->action = $runthis;
+ $this->validationCallback = $runthis;
return $this;
}
@@ -185,7 +220,7 @@ public function values() {
return $this->count();
}
- return $this->values;
+ return $this->valuesParsed;
}
public function wrap($str, $pad, $width) {
@@ -215,8 +250,8 @@ public function wrap($str, $pad, $width) {
$line = substr($line, 0, $spacePos + 1);
}
- $lines[] = rtrim($line);
$str = substr($str, strlen($line));
+ $lines[] = rtrim($line);
}
if (strlen($str)) {
View
64 php/OptionParser.class.php
@@ -10,7 +10,7 @@ class OptionParser {
protected $programNameParsed = null;
public function __get($name) {
- return $this->get($name)->value;
+ return $this->get($name);
}
public function addOption($short, $long, $helpMessage = null, $name = null) {
@@ -49,6 +49,14 @@ public function getopt() {
return $out;
}
+ public function getValue($name) {
+ if (! $this->parametersByName[$name]) {
+ throw new Exception('No parameter named ' . $name);
+ }
+
+ return $this->parametersByName[$name]->value();
+ }
+
public function help() {
$out = '';
@@ -75,9 +83,14 @@ public function parse($options = null) {
if (is_null($options)) {
$options = $GLOBALS['argv'];
- // First entry will be 'php'
- array_shift($options);
- $this->programNameParsed = array_shift($options);
+ // First entry may be 'php'
+ $cmd = array_shift($options);
+
+ if (preg_match('/^(.*\/)?php5?(\.exe)?$/', $cmd)) {
+ $cmd .= ' ' . array_shift($options);
+ }
+
+ $this->programNameParsed = $cmd;
}
if (! is_array($options)) {
@@ -103,7 +116,11 @@ public function parse($options = null) {
if (preg_match('/^([^=]+)=(.*)$/', $arg, $matches)) {
$arg = $matches[1];
- $value = $matches[2];
+ $value = '';
+
+ if (count($matches) > 2) {
+ $value = $matches[2];
+ }
}
// Match a regular argument
@@ -115,27 +132,32 @@ public function parse($options = null) {
// Autocomplete if possible
if (! $found && $this->doAutocomplete) {
- $best = 0;
- $bestName = null;
+ $suggestions = array();
foreach ($this->parameters as $p) {
- foreach ($p->matchAutocomplete($arg) as $score => $name) {
- if ($score > $best) {
- $best = $score;
- $bestName = $name;
- $found = $name;
- } elseif ($score == $best) {
- // A tie
- $found = null;
- }
+ foreach ($p->matchAutocomplete($arg) as $name => $obj) {
+ $suggestions[$name] = $obj;
}
}
- if ($found) {
- $arg = $bestName;
+ if (count($suggestions) == 1) {
+ $arg = array_keys($suggestions);
+ $arg = reset($arg);
+ $found = $suggestions[$arg];
}
}
+ // Wildcard if possible
+ if (! $found) {
+ foreach ($this->parameters as $p) {
+ if ($p->matchWildcard()) {
+ $found = $p;
+ }
+ }
+ }
+
+ array_shift($options);
+
if (! $found) {
$reconstructed = '--' . $arg;
@@ -150,7 +172,6 @@ public function parse($options = null) {
// Must not use "=" syntax
// Treat it as another paramter if found
$found->handleArgument($arg);
- array_shift($options);
if (! is_null($value)) {
array_unshift($options, '=' . $value);
@@ -158,10 +179,8 @@ public function parse($options = null) {
break;
- case OptionParameter::ARGUMENT_REQURED:
+ case OptionParameter::ARGUMENT_REQUIRED:
// Can use "=" syntax or next argument
- array_shift($options);
-
if (is_null($value)) {
if (! count($options)) {
throw new Exception('Value needed for --' . $arg);
@@ -176,7 +195,6 @@ public function parse($options = null) {
default: // Optional
// Must use "=" syntax
$found->handleArgument($arg, $value);
- array_shift($options);
break;
}
}
View
111 tests/nodejs.js
@@ -0,0 +1,111 @@
+var OP = require('../nodejs/OptionParser');
+var fs = require('fs');
+var scenariosDir = 'scenarios/';
+
+var pending = 0;
+var failures = 0;
+
+fs.readdir(scenariosDir, function (err, files) {
+ if (err) {
+ throw err;
+ }
+
+ files.forEach(function (filename) {
+ if (filename.match(/\.json$/)) {
+ runScenario(scenariosDir + filename);
+ }
+ });
+});
+
+function runScenario(filename) {
+ pending ++;
+ fs.readFile(filename, function (err, data) {
+ if (err) {
+ throw err;
+ }
+
+ var json = JSON.parse(data);
+
+ if (typeof json != "object") {
+ throw new Error('Unable to load JSON: ' + filename);
+ }
+
+ json.tests.forEach(function (test) {
+ var parser = makeParser(json);
+ var parseAsString = JSON.stringify(test.parse);
+ var unparsed = parser.parse(test.parse);
+ var getopt = parser.getopt();
+
+ assertSame(test.getopt, getopt, "Getopt difference running scenario " + filename + " (" + parseAsString + ")");
+ assertSame(test.unparsed, unparsed, "Unparsed difference runnning scenario " + filename + " (" + parseAsString + ")");
+ });
+
+ var parser = makeParser(json);
+ var helpText = parser.help(16, 2, 78).replace(/[ \t\r\n]*$/, '').split("\n");
+ assertSame(json.help, helpText, "Help text difference for scenario " + filename);
+
+ summary();
+ });
+}
+
+function summary() {
+ pending --;
+
+ if (pending) {
+ return;
+ }
+
+ if (failures) {
+ console.log("Failures detected");
+ } else {
+ console.log("All tests in all scenarios passed!");
+ }
+}
+
+function makeParser(json) {
+ var parser = new OP.OptionParser();
+ parser.autocomplete(json.autocomplete);
+ parser.scanAll(json.scanAll);
+
+ json.options.forEach(function (opt) {
+ var param = parser.addOption(opt.shortOptions, opt.longOptions, opt.help, opt.name);
+
+ if (opt.argumentName) {
+ param.argument(opt.argumentName, opt.argumentRequired);
+ }
+ });
+
+ return parser;
+}
+
+function assertSame(expected, actual, message) {
+ expected = sortObject(expected);
+ actual = sortObject(actual);
+
+ if (JSON.stringify(actual) != JSON.stringify(expected)) {
+ console.log(message);
+ console.log("Expected:");
+ console.log(expected);
+ console.log("Actual:");
+ console.log(actual);
+ console.log("");
+ failures ++;
+ }
+}
+
+function sortObject(input) {
+ var output = {};
+ var keys = [];
+
+ for (var i in input) {
+ keys.push(i);
+ }
+
+ keys.sort();
+
+ keys.forEach(function (item) {
+ output[item] = input[item];
+ });
+
+ return output;
+}
View
4 tests/php.php
@@ -35,7 +35,7 @@ function runScenario($scenarioFile) {
}
$parser = makeParser($json);
- $helpText = $parser->help();
+ $helpText = $parser->help(16, 2, 78);
$helpText = rtrim($helpText);
$helpText = explode("\n", $helpText);
$failures += assertSame($json->help, $helpText, "Help text difference for scenario $scenarioFile");
@@ -49,7 +49,7 @@ function makeParser($json) {
$parser->scanAll($json->scanAll);
foreach ($json->options as $opt) {
- $param = $parser->addOption($opt->short, $opt->long, $opt->help, $opt->name);
+ $param = $parser->addOption($opt->shortOptions, $opt->longOptions, $opt->help, $opt->name);
if (! empty($opt->argumentName)) {
$param->argument($opt->argumentName, $opt->argumentRequired);
}
View
4 tests/run_all
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+node nodejs.js
+php php.php
View
16 tests/scenarios/general_use.json
@@ -3,30 +3,30 @@
"scanAll": true,
"options": [
{
- "short": "f",
- "long": "flag",
+ "shortOptions": "f",
+ "longOptions": "flag",
"help": "Test option",
"name": null
},
{
- "short": "r",
- "long": null,
+ "shortOptions": "r",
+ "longOptions": null,
"help": "Second test option",
"argumentName": "ARG",
"argumentRequired": true,
"name": null
},
{
- "short": null,
- "long": "opt",
+ "shortOptions": null,
+ "longOptions": "opt",
"help": "Third test option",
"argumentName": "Op",
"argumentRequired": false,
"name": null
},
{
- "short": "d",
- "long": "hidden-option",
+ "shortOptions": "d",
+ "longOptions": "hidden-option",
"help": null,
"name": "hidden"
}
View
16 tests/scenarios/long_help.json
@@ -3,22 +3,22 @@
"scanAll": true,
"options": [
{
- "short": [ "f", "F" ],
- "long": [ "flag", "flagged-option" ],
+ "shortOptions": [ "f", "F" ],
+ "longOptions": [ "flag", "flagged-option" ],
"help": "Test option that can be used many ways",
"name": null
},
{
- "short": "r",
- "long": "rubber-duckie-wrapping",
+ "shortOptions": "r",
+ "longOptions": "rubber-duckie-wrapping",
"help": "Second test option, but let us make the help message super duper long so that it wraps around and around. I think a couple times (at a minimum) would be sufficient.",
"argumentName": "ARG",
"argumentRequired": true,
"name": null
},
{
- "short": "t",
- "long": null,
+ "shortOptions": "t",
+ "longOptions": null,
"help": "This option should just barely wrap-around-to-the-next-line-but-it-should-chop-this-super-long-word up.",
"argumentName": "ARG",
"argumentRequired": false,
@@ -34,7 +34,7 @@
" Second test option, but let us make the help message super",
" duper long so that it wraps around and around. I think a",
" couple times (at a minimum) would be sufficient.",
- "-t[=ARG] This option should just barely wrap-around-to-the-next-line-",
- " but-it-should-chop-this-super-long-word up."
+ "-t[=ARG] This option should just barely wrap-around-to-the-next-line-bu",
+ " t-it-should-chop-this-super-long-word up."
]
}
View
12 tests/scenarios/obscure_posix.json
@@ -3,22 +3,22 @@
"scanAll": true,
"options": [
{
- "short": "f",
- "long": null,
+ "shortOptions": "f",
+ "longOptions": null,
"help": "No argument",
"name": null
},
{
- "short": "r",
- "long": null,
+ "shortOptions": "r",
+ "longOptions": null,
"help": "Required argument",
"argumentName": "ARG",
"argumentRequired": true,
"name": null
},
{
- "short": "o",
- "long": null,
+ "shortOptions": "o",
+ "longOptions": null,
"help": "Optional argument",
"argumentName": "ARG",
"argumentRequired": false,
Please sign in to comment.
Something went wrong with that request. Please try again.