Skip to content
This repository
Browse code

Initial commit.

  • Loading branch information...
commit c4d305e64cc16abb15aa363dc94fc1703d103941 0 parents
Ben Nolan authored
6 bin/capt
... ... @@ -0,0 +1,6 @@
  1 +#!/usr/local/bin/node
  2 +
  3 +root = __dirname + "/.."
  4 +
  5 +require("coffee-script")
  6 +require(root + "/src/main.coffee")
956 lib/parseopt.js
... ... @@ -0,0 +1,956 @@
  1 +/**
  2 + * JavaScript Option Parser (parseopt)
  3 + * Copyright (C) 2010 Mathias Panzenböck <grosser.meister.morti@gmx.net>
  4 + *
  5 + * This library is free software; you can redistribute it and/or
  6 + * modify it under the terms of the GNU Lesser General Public
  7 + * License as published by the Free Software Foundation; either
  8 + * version 2.1 of the License, or (at your option) any later version.
  9 + *
  10 + * This library is distributed in the hope that it will be useful,
  11 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13 + * Lesser General Public License for more details.
  14 + *
  15 + * You should have received a copy of the GNU Lesser General Public
  16 + * License along with this library; if not, write to the Free Software
  17 + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  18 + */
  19 +
  20 +/**
  21 + * Construct a new OptionParser.
  22 + * See the demo folder the end of this file for example usage.
  23 + *
  24 + * @param object params optional Parameter-Object
  25 + *
  26 + * ===== Parameter-Object =====
  27 + * {
  28 + * minargs: integer, optional
  29 + * maxargs: integer, optional
  30 + * program: string, per default inferred from process.argv
  31 + * strings: object, optional
  32 + * Table of strings used in the output. See below.
  33 + * options: array, optional
  34 + * Array of option definitions. See below.
  35 + * }
  36 + *
  37 + * ===== String-Table =====
  38 + * {
  39 + * help: string, default: 'No help available for this option.'
  40 + * usage: string, default: 'Usage'
  41 + * options: string, default: 'OPTIONS'
  42 + * arguments: string, default: 'ARGUMENTS'
  43 + * required: string, default: 'required'
  44 + * default: string, default: 'default'
  45 + * base: string, default: 'base'
  46 + * metavars: object, optional
  47 + * Table of default metavar names per type.
  48 + * Per default the type name in capital letters or derived
  49 + * from the possible values.
  50 + * }
  51 + *
  52 + * ===== Option Definition =====
  53 + * {
  54 + * // Only when passed to the OptionParser constructor:
  55 + * name: string or array
  56 + * names: string or array, alias of name
  57 + * Only one of both may be used at the same time.
  58 + *
  59 + * Names can be long options (e.g. '--foo') and short options
  60 + * (e.g. '-f'). The first name is used to indentify the option.
  61 + * Names musst be unique and may not contain '='.
  62 + *
  63 + * Short options may be combined when passed to a programm. E.g.
  64 + * the options '-f' and '-b' can be combined to '-fb'. Only one
  65 + * of these combined options may require an argument.
  66 + *
  67 + * Short options are separated from ther arguments by space,
  68 + * long options per '='. If a long option requires an argument
  69 + * and none is passed using '=' it also uses the next commandline
  70 + * argument as it's argument (like short options).
  71 + *
  72 + * If '--' is encountered all remaining arguments are treated as
  73 + * arguments and not as options.
  74 + *
  75 + * // General fields:
  76 + * target: string, per deflault inferred from first name
  77 + * This defines the name used in the returned options object.
  78 + * Multiple options may have the same target.
  79 + * default: any, default: undefined
  80 + * The default value associated with a certain target and is
  81 + * overwritten by each new option with the same target.
  82 + * type: string, default: 'string', see below
  83 + * required: boolean, default: false
  84 + * redefinable: boolean, default: true
  85 + * help: string, optional
  86 + * details: array, optional
  87 + * short list of details shown in braces after the option name
  88 + * e.g. integer type options add 'base: '+base if base !== undefined
  89 + * metavar: string or array, per deflault inferred from type
  90 + * onOption: function (value) -> boolean, optional
  91 + * Returning true canceles any further option parsing
  92 + * and the parse() method returns null.
  93 + *
  94 + * // Type: string (alias: str)
  95 + * // Type: boolean (alias: bool)
  96 + * // Type: object (alias: obj)
  97 + *
  98 + * // Type: integer (alias: int)
  99 + * min: integer, optional
  100 + * max: integer, optional
  101 + * NaN: boolean, default: false
  102 + * base: integer, optional
  103 + *
  104 + * // Type: float (alias: number)
  105 + * min: float, optional
  106 + * max: float, optional
  107 + * NaN: boolean, default: false
  108 + *
  109 + * // Type: flag
  110 + * value: boolean, default: true
  111 + * default: boolean, default: false
  112 + *
  113 + * // Type: option
  114 + * value: any, per default inferred from first name
  115 + *
  116 + * // Type: enum
  117 + * ignoreCase: boolean, default: true
  118 + * values: array or object where the user enteres the field name of
  119 + * the object and you get the value of the field
  120 + *
  121 + * // Type: record
  122 + * create: function () -> object, default: Array
  123 + * args: array of type definitions (type part of option definitions)
  124 + *
  125 + * // Type: custom
  126 + * argc: integer, default: -1
  127 + * Number of required arguments.
  128 + * -1 means one optional argument.
  129 + * parse: function (string, ...) -> value
  130 + * stringify: function (value) -> string, optional
  131 + * }
  132 + *
  133 + * ===== Option-Arguments =====
  134 + * For the following types exactly one argument is required:
  135 + * integer, float, string, boolean, object, enum
  136 + *
  137 + * The following types have optional arguments:
  138 + * flag
  139 + *
  140 + * The following types have no arguments:
  141 + * option
  142 + *
  143 + * Custom types may set this through the argc field.
  144 + */
  145 +function OptionParser (params) {
  146 + this.optionsPerName = {};
  147 + this.defaultValues = {};
  148 + this.options = [];
  149 +
  150 + if (params !== undefined) {
  151 + this.minargs = params.minargs == 0 ? undefined : params.minargs;
  152 + this.maxargs = params.maxargs;
  153 + this.program = params.program;
  154 + this.strings = params.strings;
  155 +
  156 + if (this.minargs > this.maxargs) {
  157 + throw new Error('minargs > maxargs');
  158 + }
  159 + }
  160 +
  161 + if (this.strings === undefined) {
  162 + this.strings = {};
  163 + }
  164 +
  165 + defaults(this.strings, {
  166 + help: 'No help available for this option.',
  167 + usage: 'Usage',
  168 + options: 'OPTIONS',
  169 + arguments: 'ARGUMENTS',
  170 + required: 'required',
  171 + default: 'default',
  172 + base: 'base',
  173 + metavars: {}
  174 + });
  175 +
  176 + defaults(this.strings.metavars, METAVARS);
  177 +
  178 + if (this.program === undefined) {
  179 + this.program = process.argv[0] + ' ' + process.argv[1];
  180 + }
  181 +
  182 + if (params !== undefined && params.options !== undefined) {
  183 + for (var i in params.options) {
  184 + var opt = params.options[i];
  185 + var names;
  186 +
  187 + if (opt instanceof Array || typeof(opt) == 'string') {
  188 + opt = undefined;
  189 + names = opt;
  190 + }
  191 + else {
  192 + names = opt.names;
  193 + if (names === undefined) {
  194 + names = opt.name;
  195 + delete opt.name;
  196 + }
  197 + else {
  198 + delete opt.names;
  199 + }
  200 + }
  201 + this.add(names, opt);
  202 + }
  203 + }
  204 +}
  205 +
  206 +OptionParser.prototype = {
  207 + /**
  208 + * Parse command line options.
  209 + *
  210 + * @param array args Commandline arguments.
  211 + * If undefined process.argv.slice(2) is used.
  212 + *
  213 + * @return object
  214 + * {
  215 + * arguments: array
  216 + * options: object, { target -> value }
  217 + * }
  218 + */
  219 + parse: function (args) {
  220 + if (args === undefined) {
  221 + args = process.argv.slice(2);
  222 + }
  223 +
  224 + var data = {
  225 + options: {},
  226 + arguments: []
  227 + };
  228 +
  229 + for (var name in this.defaultValues) {
  230 + var value = this.defaultValues[name];
  231 +
  232 + if (value !== undefined) {
  233 + data.options[this.optionsPerName[name].target] = value;
  234 + }
  235 + }
  236 +
  237 + var got = {};
  238 + var i = 0;
  239 + for (; i < args.length; ++ i) {
  240 + var arg = args[i];
  241 +
  242 + if (arg == '--') {
  243 + ++ i;
  244 + break;
  245 + }
  246 + else if (/^--.+$/.test(arg)) {
  247 + var j = arg.indexOf('=');
  248 + var name, value = undefined;
  249 +
  250 + if (j == -1) {
  251 + name = arg;
  252 + }
  253 + else {
  254 + name = arg.substring(0,j);
  255 + value = arg.substring(j+1);
  256 + }
  257 +
  258 + var optdef = this.optionsPerName[name];
  259 +
  260 + if (optdef === undefined) {
  261 + throw new Error('unknown option: '+name);
  262 + }
  263 +
  264 + if (value === undefined) {
  265 + if (optdef.argc < 1) {
  266 + value = optdef.value;
  267 + }
  268 + else if ((i + optdef.argc) >= args.length) {
  269 + throw new Error('option '+name+' needs '+optdef.argc+' arguments');
  270 + }
  271 + else {
  272 + value = optdef.parse.apply(optdef, args.slice(i+1, i+1+optdef.argc));
  273 + i += optdef.argc;
  274 + }
  275 + }
  276 + else if (optdef.argc == 0) {
  277 + throw new Error('option '+name+' does not need an argument');
  278 + }
  279 + else if (optdef.argc > 1) {
  280 + throw new Error('option '+name+' needs '+optdef.argc+' arguments');
  281 + }
  282 + else {
  283 + value = optdef.parse(value);
  284 + }
  285 +
  286 + if (!optdef.redefinable && optdef.target in got) {
  287 + throw new Error('cannot redefine option '+name);
  288 + }
  289 +
  290 + got[optdef.target] = true;
  291 + data.options[optdef.target] = value;
  292 +
  293 + if (optdef.onOption && optdef.onOption(value) === true) {
  294 + return null;
  295 + }
  296 + }
  297 + else if (/^-.+$/.test(arg)) {
  298 + if (arg.indexOf('=') != -1) {
  299 + throw new Error('illegal option syntax: '+arg);
  300 + }
  301 +
  302 + var tookarg = false;
  303 + arg = arg.substring(1);
  304 +
  305 + for (var j = 0; j < arg.length; ++ j) {
  306 + var name = '-'+arg[j];
  307 + var optdef = this.optionsPerName[name];
  308 + var value;
  309 +
  310 + if (optdef === undefined) {
  311 + throw new Error('unknown option: '+name);
  312 + }
  313 +
  314 + if (optdef.argc < 1) {
  315 + value = optdef.value;
  316 + }
  317 + else {
  318 + if (tookarg || (i+optdef.argc) >= args.length) {
  319 + throw new Error('option '+name+' needs '+optdef.argc+' arguments');
  320 + }
  321 +
  322 + value = optdef.parse.apply(optdef, args.slice(i+1, i+1+optdef.argc));
  323 + i += optdef.argc;
  324 + tookarg = true;
  325 + }
  326 +
  327 + if (!optdef.redefinable && optdef.target in got) {
  328 + throw new Error('redefined option: '+name);
  329 + }
  330 +
  331 + got[optdef.target] = true;
  332 + data.options[optdef.target] = value;
  333 +
  334 + if (optdef.onOption && optdef.onOption(value) === true) {
  335 + return null;
  336 + }
  337 + }
  338 + }
  339 + else {
  340 + data.arguments.push(arg);
  341 + }
  342 + }
  343 +
  344 + for (; i < args.length; ++ i) {
  345 + data.arguments.push(args[i]);
  346 + }
  347 +
  348 + var argc = data.arguments.length;
  349 + if ((this.maxargs !== undefined && argc > this.maxargs) ||
  350 + (this.minargs !== undefined && argc < this.minargs)) {
  351 + var msg = 'illegal number of arguments: ' + argc;
  352 +
  353 + if (this.minargs !== undefined) {
  354 + msg += ', minumum is ' + this.minargs;
  355 + if (this.maxargs !== undefined) {
  356 + msg += ' and maximum is ' + this.maxargs;
  357 + }
  358 + }
  359 + else {
  360 + msg += ', maximum is ' + this.maxargs;
  361 + }
  362 +
  363 + throw new Error(msg);
  364 + }
  365 +
  366 + for (var i in this.options) {
  367 + var optdef = this.options[i];
  368 + if (optdef.required && !(optdef.target in got)) {
  369 + throw new Error('missing required option: ' + optdef.names[0]);
  370 + }
  371 + }
  372 +
  373 + return data;
  374 + },
  375 + /**
  376 + * Add an option definition.
  377 + *
  378 + * @param string or array names Option names
  379 + * @param object optdef Option definition
  380 + */
  381 + add: function (names, optdef) {
  382 + if (typeof(names) == 'string') {
  383 + names = [names];
  384 + }
  385 + else if (names === undefined || names.length == 0) {
  386 + throw new Error('no option name given');
  387 + }
  388 +
  389 + if (optdef === undefined) {
  390 + optdef = {};
  391 + }
  392 +
  393 + optdef.names = names;
  394 +
  395 + for (var i in names) {
  396 + var name = names[i];
  397 + var match = /(-*)(.*)/.exec(name);
  398 +
  399 + if (name.length == 0 || match[1].length < 1 ||
  400 + match[1].length > 2 || match[2].length == 0 ||
  401 + (match[1].length == 1 && match[2].length > 1) ||
  402 + match[2].indexOf('=') != -1) {
  403 + throw new Error('illegal option name: ' + name);
  404 + }
  405 +
  406 + if (name in this.optionsPerName) {
  407 + throw new Error('option already exists: '+name);
  408 + }
  409 + }
  410 +
  411 + if (optdef.target === undefined) {
  412 + var target = names[0].replace(/^--?/,'');
  413 +
  414 + if (target.toUpperCase() == target) {
  415 + // FOO-BAR -> FOO_BAR
  416 + target = target.replace(/[^a-zA-Z0-9]+/,'_');
  417 + }
  418 + else {
  419 + // foo-bar -> fooBar
  420 + target = target.split(/[^a-zA-Z0-9]+/);
  421 + for (var i = 1; i < target.length; ++ i) {
  422 + var part = target[i];
  423 +
  424 + if (part) {
  425 + target[i] = part[0].toUpperCase() + part.substring(1);
  426 + }
  427 + }
  428 + target = target.join('');
  429 + }
  430 +
  431 + optdef.target = target;
  432 + }
  433 +
  434 + this._initType(optdef, optdef.names[0]);
  435 +
  436 + if (optdef.redefinable === undefined) {
  437 + optdef.redefinable = true;
  438 + }
  439 +
  440 + if (optdef.required === undefined) {
  441 + optdef.required = false;
  442 + }
  443 +
  444 + if (optdef.help === undefined) {
  445 + optdef.help = this.strings.help;
  446 + }
  447 + else {
  448 + optdef.help = optdef.help.trim();
  449 + }
  450 +
  451 + for (var i in names) {
  452 + this.optionsPerName[names[i]] = optdef;
  453 + }
  454 +
  455 + if (optdef.default !== undefined) {
  456 + this.defaultValues[names[0]] = optdef.default;
  457 + }
  458 +
  459 + this.options.push(optdef);
  460 + },
  461 + /**
  462 + * Show an error message, usage and exit program with exit code 1.
  463 + *
  464 + * @param string msg The error message
  465 + * @param WriteStream out Where to write the message.
  466 + * If undefined process.stdout is used.
  467 + */
  468 + error: function (msg, out) {
  469 + if (!out) {
  470 + out = process.stdout;
  471 + }
  472 + out.write('*** '+msg+'\n\n');
  473 + this.usage(undefined, out);
  474 + process.exit(1);
  475 + },
  476 + /**
  477 + * Print usage message.
  478 + *
  479 + * @param string help Optional additional help message.
  480 + * @param WriteStream out Where to write the message.
  481 + * If undefined process.stdout is used.
  482 + */
  483 + usage: function (help, out) {
  484 + if (!out) {
  485 + out = process.stdout;
  486 + }
  487 +
  488 + out.write(this.strings.usage+': '+this.program+' ['+
  489 + this.strings.options+']'+(this.maxargs != 0 ?
  490 + ' ['+this.strings.arguments+']\n' : '\n'));
  491 + out.write('\n');
  492 + out.write(this.strings.options+':\n');
  493 +
  494 + for (var i in this.options) {
  495 + var optdef = this.options[i];
  496 + var optnames = [];
  497 + var metavar = optdef.metavar;
  498 +
  499 + if (metavar instanceof Array) {
  500 + metavar = metavar.join(' ');
  501 + }
  502 +
  503 + for (var j in optdef.names) {
  504 + var optname = optdef.names[j];
  505 +
  506 + if (metavar !== undefined) {
  507 + if (optdef.argc < 2 && optname.substring(0,2) == '--') {
  508 + if (optdef.argc < 0) {
  509 + optname = optname+'[='+metavar+']';
  510 + }
  511 + else {
  512 + optname = optname+'='+metavar;
  513 + }
  514 + }
  515 + else {
  516 + optname = optname+' '+metavar;
  517 + }
  518 + }
  519 + optnames.push(optname);
  520 + }
  521 +
  522 + var details = optdef.details !== undefined ? optdef.details.slice() : [];
  523 + if (optdef.required) {
  524 + details.push(this.strings.required);
  525 + }
  526 + else if (optdef.argc > 0 && optdef.default !== undefined) {
  527 + details.push(this.strings.default+': '+optdef.stringify(optdef.default));
  528 + }
  529 +
  530 + if (details.length > 0) {
  531 + details = ' (' + details.join(', ') + ')';
  532 + }
  533 +
  534 + if (metavar !== undefined) {
  535 + optnames[0] += details;
  536 + out.write(' '+optnames.join('\n '));
  537 + }
  538 + else {
  539 + out.write(' '+optnames.join(', ')+details);
  540 + }
  541 + if (optdef.help) {
  542 + var lines = optdef.help.split('\n');
  543 + for (var j in lines) {
  544 + out.write('\n '+lines[j]);
  545 + }
  546 + }
  547 + out.write('\n\n');
  548 + }
  549 +
  550 + if (help !== undefined) {
  551 + out.write(help);
  552 + if (help[help.length-1] != '\n') {
  553 + out.write('\n');
  554 + }
  555 + }
  556 + },
  557 + _initType: function (optdef, name) {
  558 + optdef.name = name;
  559 +
  560 + if (optdef.type === undefined) {
  561 + optdef.type = 'string';
  562 + }
  563 + else if (optdef.type in TYPE_ALIAS) {
  564 + optdef.type = TYPE_ALIAS[optdef.type];
  565 + }
  566 +
  567 + switch (optdef.type) {
  568 + case 'flag':
  569 + if (optdef.value === undefined) {
  570 + optdef.value = true;
  571 + }
  572 + optdef.parse = parseBool;
  573 + optdef.argc = -1;
  574 +
  575 + if (optdef.default === undefined) {
  576 + optdef.default = this.defaultValues[name];
  577 +
  578 + if (optdef.default === undefined) {
  579 + optdef.default = false;
  580 + }
  581 + }
  582 + break;
  583 +
  584 + case 'option':
  585 + optdef.argc = 0;
  586 +
  587 + if (optdef.value === undefined) {
  588 + optdef.value = name.replace(/^--?/,'');
  589 + }
  590 + break;
  591 +
  592 + case 'enum':
  593 + this._initEnum(optdef, name);
  594 + break;
  595 +
  596 + case 'integer':
  597 + case 'float':
  598 + this._initNumber(optdef, name);
  599 + break;
  600 +
  601 + case 'record':
  602 + if (optdef.args === undefined || optdef.args.length == 0) {
  603 + throw new Error('record '+name+' needs at least one argument');
  604 + }
  605 + optdef.argc = 0;
  606 + var metavar = [];
  607 + for (var i in optdef.args) {
  608 + var arg = optdef.args[i];
  609 + if (arg.target === undefined) {
  610 + arg.target = i;
  611 + }
  612 + this._initType(arg, name+'['+i+']');
  613 +
  614 + if (arg.argc < 1) {
  615 + throw new Error('argument '+i+' of option '+name+
  616 + ' has illegal number of arguments');
  617 + }
  618 + if (arg.metavar instanceof Array) {
  619 + for (var j in arg.metavar) {
  620 + metavar.push(arg.metavar[j]);
  621 + }
  622 + }
  623 + else {
  624 + metavar.push(arg.metavar);
  625 + }
  626 + delete arg.metavar;
  627 + optdef.argc += arg.argc;
  628 + }
  629 + if (optdef.metavar === undefined) {
  630 + optdef.metavar = metavar;
  631 + }
  632 + var onOption = optdef.onOption;
  633 + if (onOption !== undefined) {
  634 + optdef.onOption = function (values) {
  635 + return onOption.apply(this, values);
  636 + };
  637 + }
  638 + if (optdef.create === undefined) {
  639 + optdef.create = Array;
  640 + }
  641 + optdef.parse = function () {
  642 + var values = this.create();
  643 + var parserIndex = 0;
  644 + for (var i = 0; i < arguments.length;) {
  645 + var arg = optdef.args[parserIndex ++];
  646 + var raw = [];
  647 + for (var j = 0; j < arg.argc; ++ j) {
  648 + raw.push(arguments[i+j]);
  649 + }
  650 + values[arg.target] = arg.parse.apply(arg, raw);
  651 + i += arg.argc;
  652 + }
  653 + return values;
  654 + };
  655 + break;
  656 +
  657 + case 'custom':
  658 + if (optdef.argc === undefined || optdef.argc < -1) {
  659 + optdef.argc = -1;
  660 + }
  661 +
  662 + if (optdef.parse === undefined) {
  663 + throw new Error(
  664 + 'no parse function defined for custom type option '+name);
  665 + }
  666 + break;
  667 +
  668 + default:
  669 + optdef.argc = 1;
  670 + optdef.parse = PARSERS[optdef.type];
  671 +
  672 + if (optdef.parse === undefined) {
  673 + throw new Error('type of option '+name+' is unknown: '+optdef.type);
  674 + }
  675 + }
  676 +
  677 + initStringify(optdef);
  678 +
  679 + var count = 1;
  680 + if (optdef.metavar === undefined) {
  681 + optdef.metavar = this.strings.metavars[optdef.type];
  682 + }
  683 +
  684 + if (optdef.metavar === undefined) {
  685 + count = 0;
  686 + }
  687 + else if (optdef.metavar instanceof Array) {
  688 + count = optdef.metavar.length;
  689 + }
  690 +
  691 + if (optdef.argc == -1) {
  692 + if (count > 1) {
  693 + throw new Error('illegal number of metavars for option '+name+
  694 + ': '+JSON.stringify(optdef.metavar));
  695 + }
  696 + }
  697 + else if (optdef.argc != count) {
  698 + throw new Error('illegal number of metavars for option '+name+
  699 + ': '+JSON.stringify(optdef.metavar));
  700 + }
  701 + },
  702 + _initEnum: function (optdef, name) {
  703 + optdef.argc = 1;
  704 +
  705 + if (optdef.ignoreCase === undefined) {
  706 + optdef.ignoreCase = true;
  707 + }
  708 +
  709 + if (optdef.values === undefined || optdef.values.length == 0) {
  710 + throw new Error('no values for enum '+name+' defined');
  711 + }
  712 +
  713 + initStringify(optdef);
  714 +
  715 + var labels = [];
  716 + var values = {};
  717 + if (optdef.values instanceof Array) {
  718 + for (var i in optdef.values) {
  719 + var value = optdef.values[i];
  720 + var label = String(value);
  721 + values[optdef.ignoreCase ? label.toLowerCase() : label] = value;
  722 + labels.push(optdef.stringify(value));
  723 + }
  724 + }
  725 + else {
  726 + for (var label in optdef.values) {
  727 + var value = optdef.values[label];
  728 + values[optdef.ignoreCase ? label.toLowerCase() : label] = value;
  729 + labels.push(optdef.stringify(label));
  730 + }
  731 + labels.sort();
  732 + }
  733 + optdef.values = values;
  734 +
  735 +
  736 + if (optdef.metavar === undefined) {
  737 + optdef.metavar = '<'+labels.join(', ')+'>';
  738 + }
  739 +
  740 + optdef.parse = function (s) {
  741 + var value = values[optdef.ignoreCase ? s.toLowerCase() : s];
  742 + if (value !== undefined) {
  743 + return value;
  744 + }
  745 + throw new Error('illegal value for option '+name+': '+s);
  746 + };
  747 + },
  748 + _initNumber: function (optdef, name) {
  749 + optdef.argc = 1;
  750 +
  751 + if (optdef.NaN === undefined) {
  752 + optdef.NaN = false;
  753 + }
  754 +
  755 + if (optdef.min > optdef.max) {
  756 + throw new Error('min > max for option '+name);
  757 + }
  758 +
  759 + var parse, toStr;
  760 + if (optdef.type == 'integer') {
  761 + parse = function (s) {
  762 + var i = NaN;
  763 + if (s.indexOf('.') == -1) {
  764 + i = parseInt(s, optdef.base)
  765 + }
  766 + return i;
  767 + };
  768 + if (optdef.base === undefined) {
  769 + toStr = dec;
  770 + }
  771 + else {
  772 + switch (optdef.base) {
  773 + case 8: toStr = oct; break;
  774 + case 10: toStr = dec; break;
  775 + case 16: toStr = hex; break;
  776 + default: toStr = function (val) {
  777 + return val.toString(optdef.base);
  778 + };
  779 + var detail = this.strings.base+': '+optdef.base;
  780 + if (optdef.details) {
  781 + optdef.details.push(detail);
  782 + }
  783 + else {
  784 + optdef.details = [detail];
  785 + }
  786 + }
  787 + }
  788 + }
  789 + else {
  790 + parse = parseFloat;
  791 + toStr = dec;
  792 + }
  793 +
  794 + if (optdef.metavar === undefined) {
  795 + if (optdef.min === undefined && optdef.max === undefined) {
  796 + optdef.metavar = this.strings.metavars[optdef.type];
  797 + }
  798 + else if (optdef.min === undefined) {
  799 + optdef.metavar = '...'+toStr(optdef.max);
  800 + }
  801 + else if (optdef.max === undefined) {
  802 + optdef.metavar = toStr(optdef.min)+'...';
  803 + }
  804 + else {
  805 + optdef.metavar = toStr(optdef.min)+'...'+toStr(optdef.max);
  806 + }
  807 + }
  808 + optdef.parse = function (s) {
  809 + var n = parse(s);
  810 +
  811 + if ((!this.NaN && isNaN(n))
  812 + || (optdef.min !== undefined && n < optdef.min)
  813 + || (optdef.max !== undefined && n > optdef.max)) {
  814 + throw new Error('illegal value for option '+name+': '+s);
  815 + }
  816 +
  817 + return n;
  818 + };
  819 + }
  820 +};
  821 +
  822 +function initStringify (optdef) {
  823 + if (optdef.stringify === undefined) {
  824 + optdef.stringify = STRINGIFIERS[optdef.type];
  825 + }
  826 +
  827 + if (optdef.stringify === undefined) {
  828 + optdef.stringify = stringifyAny;
  829 + }
  830 +}
  831 +
  832 +function defaults (target, defaults) {
  833 + for (var name in defaults) {
  834 + if (target[name] === undefined) {
  835 + target[name] = defaults[name];
  836 + }
  837 + }
  838 +}
  839 +
  840 +function dec (val) {
  841 + return val.toString();
  842 +}
  843 +
  844 +function oct (val) {
  845 + return '0'+val.toString(8);
  846 +}
  847 +
  848 +function hex (val) {
  849 + return '0x'+val.toString(16);
  850 +}
  851 +
  852 +const TRUE_VALUES = {true: true, on: true, 1: true, yes: true};
  853 +const FALSE_VALUES = {false: true, off: true, 0: true, no: true};
  854 +
  855 +function parseBool (s) {
  856 + s = s.trim().toLowerCase();
  857 + if (s in TRUE_VALUES) {
  858 + return true;
  859 + }
  860 + else if (s in FALSE_VALUES) {
  861 + return false;
  862 + }
  863 + else {
  864 + throw new Error('illegal boolean value: '+s);
  865 + }
  866 +}
  867 +
  868 +function id (x) {
  869 + return x;
  870 +}
  871 +
  872 +const PARSERS = {
  873 + boolean: parseBool,
  874 + string: id,
  875 + object: JSON.parse
  876 +};
  877 +
  878 +const TYPE_ALIAS = {
  879 + int: 'integer',
  880 + number: 'float',
  881 + bool: 'boolean',
  882 + str: 'string',
  883 + obj: 'object'
  884 +};
  885 +
  886 +const METAVARS = {
  887 + string: 'STRING',
  888 + integer: 'INTEGER',
  889 + float: 'FLOAT',
  890 + boolean: 'BOOLEAN',
  891 + object: 'OBJECT',
  892 + enum: 'VALUE',
  893 + custom: 'VALUE'
  894 +};
  895 +
  896 +function stringifyString(s) {
  897 + if (/[\s'"\\<>,]/.test(s)) {
  898 +// s = "'"+s.replace(/\\/g,'\\\\').replace(/'/g, "'\\''")+"'";
  899 + s = JSON.stringify(s);
  900 + }
  901 + return s;
  902 +}
  903 +
  904 +function stringifyPrimitive(value) {
  905 + return ''+value;
  906 +}
  907 +
  908 +function stringifyAny (value) {
  909 + if (value instanceof Array) {
  910 + var buf = [];
  911 + for (var i in value) {
  912 + buf.push(stringifyAny(value[i]));
  913 + }
  914 + return buf.join(' ');
  915 + }
  916 + else if (typeof(value) == 'string') {
  917 + return stringifyString(value);
  918 + }
  919 + else {
  920 + return String(value);
  921 + }
  922 +}
  923 +
  924 +function stringifyInteger (value) {
  925 + if (this.base === undefined) {
  926 + return value.toString();
  927 + }
  928 +
  929 + switch (this.base) {
  930 + case 8: return oct(value);
  931 + case 16: return hex(value);
  932 + default: return value.toString(this.base);
  933 + }
  934 +}
  935 +
  936 +function stringifyRecord (record) {
  937 + var buf = [];
  938 + for (var i = 0; i < this.args.length; ++ i) {
  939 + var arg = this.args[i];
  940 + buf.push(arg.stringify(record[arg.target]));
  941 + }
  942 + return buf.join(' ');
  943 +}
  944 +
  945 +const STRINGIFIERS = {
  946 + string: stringifyString,
  947 + integer: stringifyInteger,
  948 + boolean: stringifyPrimitive,
  949 + float: stringifyPrimitive,
  950 + object: JSON.stringify,
  951 + enum: stringifyAny,
  952 + custom: stringifyAny,
  953 + record: stringifyRecord
  954 +};
  955 +
  956 +exports.OptionParser = OptionParser;
122 lib/request.js
... ... @@ -0,0 +1,122 @@
  1 +var http = require('http')
  2 + , url = require('url')
  3 + , sys = require('sys')
  4 + ;
  5 +
  6 +var toBase64 = function(str) {
  7 + return (new Buffer(str || "", "ascii")).toString("base64");
  8 +};
  9 +
  10 +function request (options, callback) {
  11 + if (!options.uri) {
  12 + throw new Error("options.uri is a required argument")
  13 + } else {
  14 + if (typeof options.uri == "string") options.uri = url.parse(options.uri);
  15 + }
  16 + if (options.proxy) {
  17 + if (typeof options.proxy == 'string') options.proxy = url.parse(options.proxy);
  18 + }
  19 +
  20 + options._redirectsFollowed = options._redirectsFollowed ? options._redirectsFollowed : 0;
  21 + options.maxRedirects = options.maxRedirects ? options.maxRedirects : 10;
  22 +
  23 + options.followRedirect = (options.followRedirect !== undefined) ? options.followRedirect : true;
  24 + options.method = options.method ? options.method : 'GET';
  25 +
  26 + options.headers = options.headers ? options.headers : {};
  27 + if (!options.headers.host) {
  28 + options.headers.host = options.uri.hostname;
  29 + if (options.uri.port) {
  30 + if ( !(options.uri.port === 80 && options.uri.protocol === 'http:') &&
  31 + !(options.uri.port === 443 && options.uri.protocol === 'https:') )
  32 + options.headers.host += (':'+options.uri.port)
  33 + }
  34 + var setHost = true;
  35 + } else {
  36 + var setHost = false;
  37 + }
  38 +
  39 + if (!options.uri.pathname) {options.uri.pathname = '/'}
  40 + if (!options.uri.port) {
  41 + if (options.uri.protocol == 'http:') {options.uri.port = 80}
  42 + else if (options.uri.protocol == 'https:') {options.uri.port = 443}
  43 + }
  44 +
  45 + if (options.bodyStream) {
  46 + sys.error('options.bodyStream is deprecated. use options.reponseBodyStream instead.');
  47 + options.responseBodyStream = options.bodyStream;
  48 + }
  49 + if (options.proxy) {
  50 + var secure = (options.proxy.protocol == 'https:') ? true : false
  51 + options.client = options.client ? options.client : http.createClient(options.proxy.port, options.proxy.hostname, secure);
  52 + } else {
  53 + var secure = (options.uri.protocol == 'https:') ? true : false
  54 + options.client = options.client ? options.client : http.createClient(options.uri.port, options.uri.hostname, secure);
  55 + }
  56 +
  57 + var clientErrorHandler = function (error) {
  58 + if (setHost) delete options.headers.host;
  59 + if (callback) callback(error);
  60 + }
  61 + options.client.addListener('error', clientErrorHandler);
  62 +
  63 + if (options.uri.auth && !options.headers.authorization) {
  64 + options.headers.authorization = "Basic " + toBase64(options.uri.auth);
  65 + }
  66 + if (options.proxy && options.proxy.auth && !options.headers['proxy-authorization']) {
  67 + options.headers['proxy-authorization'] = "Basic " + toBase64(options.proxy.auth);
  68 + }
  69 +
  70 + options.fullpath = options.uri.href.replace(options.uri.protocol + '//' + options.uri.host, '');
  71 + if (options.fullpath.length === 0) options.fullpath = '/'
  72 +
  73 + if (options.proxy) options.fullpath = (options.uri.protocol + '//' + options.uri.host + options.fullpath)
  74 +
  75 + if (options.body) {options.headers['content-length'] = options.body.length}
  76 + options.request = options.client.request(options.method, options.fullpath, options.headers);
  77 + options.request.addListener("response", function (response) {
  78 + var buffer;
  79 + if (options.responseBodyStream) {
  80 + buffer = options.responseBodyStream;
  81 + sys.pump(response, options.responseBodyStream);
  82 + }
  83 + else {
  84 + buffer = '';
  85 + response.addListener("data", function (chunk) { buffer += chunk; } )
  86 + }
  87 +
  88 + response.addListener("end", function () {
  89 + options.client.removeListener("error", clientErrorHandler);
  90 +
  91 + if (response.statusCode > 299 && response.statusCode < 400 && options.followRedirect && response.headers.location && (options._redirectsFollowed < options.maxRedirects) ) {
  92 + options._redirectsFollowed += 1
  93 + options.uri = response.headers.location;
  94 + delete options.client;
  95 + if (options.headers) {
  96 + delete options.headers.host;
  97 + }
  98 + request(options, callback);
  99 + return;
  100 + } else {options._redirectsFollowed = 0}
  101 +
  102 + if (setHost) delete options.headers.host;
  103 + if (callback) callback(null, response, buffer);
  104 + })
  105 + })
  106 +
  107 + if (options.body) {
  108 + options.request.write(options.body, 'binary');
  109 + options.request.end();
  110 + } else if (options.requestBodyStream) {
  111 + sys.pump(options.requestBodyStream, options.request);
  112 + } else {
  113 + options.request.end();
  114 + }
  115 +}
  116 +
  117 +module.exports = request;
  118 +
  119 +request.get = request;
  120 +request.post = function () {arguments[0].method = 'POST', request.apply(request, arguments)};
  121 +request.put = function () {arguments[0].method = 'PUT', request.apply(request, arguments)};
  122 +request.head = function () {arguments[0].method = 'HEAD', request.apply(request, arguments)};
558 lib/router.js
... ... @@ -0,0 +1,558 @@
  1 +/*
  2 +Copyright (c) 2010 Tim Caswell <tim@creationix.com>
  3 +
  4 +Permission is hereby granted, free of charge, to any person
  5 +obtaining a copy of this software and associated documentation
  6 +files (the "Software"), to deal in the Software without
  7 +restriction, including without limitation the rights to use,
  8 +copy, modify, merge, publish, distribute, sublicense, and/or sell
  9 +copies of the Software, and to permit persons to whom the
  10 +Software is furnished to do so, subject to the following
  11 +conditions:
  12 +
  13 +The above copyright notice and this permission notice shall be
  14 +included in all copies or substantial portions of the Software.
  15 +
  16 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17 +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  18 +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19 +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  20 +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  21 +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  22 +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  23 +OTHER DEALINGS IN THE SOFTWARE.
  24 +*/
  25 +
  26 +var sys = require('sys');
  27 +var fs = require('fs');
  28 +var path = require('path');
  29 +var http = require('http');
  30 +var url_parse = require("url").parse;
  31 +
  32 +// Used as a simple, convient 404 handler.
  33 +function notFound(req, res, message) {
  34 + message = (message || "Not Found\n") + "";
  35 + res.writeHead(404, {
  36 + "Content-Type": "text/plain",
  37 + "Content-Length": message.length
  38 + });
  39 + if (req.method !== "HEAD")
  40 + res.write(message);
  41 + res.end();
  42 +}
  43 +
  44 +// Modifies req and res to call logger with a log line on each res.end
  45 +// Think of it as "middleware"
  46 +function logify(req, res, logger) {
  47 + var end = res.end;
  48 + res.end = function () {
  49 + // Common Log Format (mostly)
  50 + logger((req.socket && req.socket.remoteAddress) + " - - [" + (new Date()).toUTCString() + "]"
  51 + + " \"" + req.method + " " + req.url
  52 + + " HTTP/" + req.httpVersionMajor + "." + req.httpVersionMinor + "\" "
  53 + + res.statusCode + " - \""
  54 + + (req.headers['referer'] || "") + "\" \"" + (req.headers["user-agent"] ? req.headers["user-agent"].split(' ')[0] : '') + "\"");
  55 + return end.apply(this, arguments);
  56 + }
  57 + var writeHead = res.writeHead;
  58 + res.writeHead = function (code) {
  59 + res.statusCode = code;
  60 + return writeHead.apply(this, arguments);
  61 + }
  62 +}
  63 +
  64 +exports.getServer = function getServer(logger) {
  65 +
  66 + logger = logger || sys.puts;
  67 +
  68 + var routes = [];
  69 +
  70 + // Adds a route the the current server
  71 + function addRoute(method, pattern, handler, format) {
  72 + if (typeof pattern === 'string') {
  73 + pattern = new RegExp("^" + pattern + "$");
  74 + }
  75 + var route = {
  76 + method: method,
  77 + pattern: pattern,
  78 + handler: handler
  79 + };
  80 + if (format !== undefined) {
  81 + route.format = format;
  82 + }
  83 + routes.push(route);
  84 + }
  85 +
  86 + // The four verbs are wrappers around addRoute
  87 + function get(pattern, handler) {
  88 + return addRoute("GET", pattern, handler);
  89 + }
  90 + function post(pattern, handler, format) {
  91 + return addRoute("POST", pattern, handler, format);
  92 + }
  93 + function put(pattern, handler, format) {
  94 + return addRoute("PUT", pattern, handler, format);
  95 + }
  96 + function del(pattern, handler) {
  97 + return addRoute("DELETE", pattern, handler);
  98 + }
  99 + function head(pattern, handler) {
  100 + return addRoute("HEAD", pattern, handler);
  101 + }
  102 +
  103 + // This is a meta pattern that expands to a common RESTful mapping
  104 + function resource(name, controller, format) {
  105 + get(new RegExp('^/' + name + '$'), controller.index);
  106 + get(new RegExp('^/' + name + '/([^/]+)$'), controller.show);
  107 + post(new RegExp('^/' + name + '$'), controller.create, format);
  108 + put(new RegExp('^/' + name + '/([^/]+)$'), controller.update, format);
  109 + del(new RegExp('^/' + name + '/([^/]+)$'), controller.destroy);
  110 + };
  111 +
  112 + function resourceController(name, data, on_change) {
  113 + data = data || [];
  114 + on_change = on_change || function () {};
  115 + return {
  116 + index: function (req, res) {
  117 + res.simpleJson(200, {content: data, self: '/' + name});
  118 + },
  119 + show: function (req, res, id) {
  120 + var item = data[id];
  121 + if (item) {
  122 + res.simpleJson(200, {content: item, self: '/' + name + '/' + id});
  123 + } else {
  124 + res.notFound();
  125 + }
  126 + },
  127 + create: function (req, res) {
  128 + req.jsonBody(function (json) {
  129 + var item, id, url;
  130 + item = json && json.content;
  131 + if (!item) {
  132 + res.notFound();
  133 + } else {
  134 + data.push(item);
  135 + id = data.length - 1;
  136 + on_change(id);
  137 + url = "/" + name + "/" + id;
  138 + res.simpleJson(201, {content: item, self: url}, [["Location", url]]);
  139 + }
  140 + });
  141 + },
  142 + update: function (req, res, id) {
  143 + req.jsonBody(function (json) {
  144 + var item = json && json.content;
  145 + if (!item) {
  146 + res.notFound();
  147 + } else {
  148 + data[id] = item;
  149 + on_change(id);
  150 + res.simpleJson(200, {content: item, self: "/" + name + "/" + id});
  151 + }
  152 + });
  153 + },
  154 + destroy: function (req, res, id) {
  155 + delete data[id];
  156 + on_change(id);
  157 + res.simpleJson(200, "200 Destroyed");
  158 + }
  159 + };
  160 + };
  161 +
  162 + // Create the http server object
  163 + var server = http.createServer(function (req, res) {
  164 +
  165 + // Enable logging on all requests using common-logger style
  166 + logify(req, res, logger);
  167 +
  168 + var uri, path;
  169 +
  170 + // Performs an HTTP 302 redirect