├── .gitignore ├── examples ├── example_nysql.js ├── example_test.js ├── example_commands.js ├── example_nit.js └── example.js ├── package.json ├── test └── options.js ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | package-lock.json 4 | .flowconfig 5 | -------------------------------------------------------------------------------- /examples/example_nysql.js: -------------------------------------------------------------------------------- 1 | const Operetta = require('../index.js') 2 | const operetta = new Operetta() 3 | operetta.parameters(['-D', '--database'], 'Database') 4 | operetta.parameters(['-H', '--host'], 'Host') 5 | operetta.parameters(['-u', '--user'], 'User') 6 | operetta.parameters(['-p', '--password'], 'Password') 7 | operetta.banner = 'NySQL. The Nultimate Natabase!\n' 8 | operetta.start((values) => { 9 | console.log(values) 10 | }) 11 | -------------------------------------------------------------------------------- /examples/example_test.js: -------------------------------------------------------------------------------- 1 | 2 | var Operetta = require('../index.js') 3 | 4 | var operetta = new Operetta() 5 | operetta.command('say', 'Say Something', (command) => { 6 | command.start(function (values) { 7 | console.log(values.positional.join(' ')) 8 | console.log(values) 9 | }) 10 | }) 11 | operetta.parameters(['-p', '--param'], 'A Parameter', (value) => { 12 | console.log('Value:', value) 13 | }) 14 | operetta.start((values) => { 15 | console.log(values) 16 | }) 17 | 18 | operetta.start() 19 | -------------------------------------------------------------------------------- /examples/example_commands.js: -------------------------------------------------------------------------------- 1 | const Operetta = require('../index.js') 2 | 3 | const args = ['say', '-x', '--name', 'Gilbert', 'hello', 'there'] 4 | 5 | const operetta = new Operetta(args) 6 | operetta.command('say', 'say something', (command) => { 7 | command.parameters(['-n', '--name'], 'Add Name ') 8 | command.options('-x', 'Add Exlamation Mark') 9 | command.start(function (values) { 10 | let saying = values.positional.join(' ') 11 | if (values['-n']) { 12 | saying = saying + ', ' + values['-n'][0] 13 | } 14 | if (values['-x']) { 15 | saying = saying + '!' 16 | } 17 | console.log(saying) 18 | }) 19 | }) 20 | 21 | operetta.start() 22 | -------------------------------------------------------------------------------- /examples/example_nit.js: -------------------------------------------------------------------------------- 1 | const Operetta = require('../index.js') 2 | const operetta = new Operetta() 3 | operetta.command('clone', 'Clone a Repo', (command) => { 4 | command.start((values) => { 5 | console.log('url', values.positional[0]) 6 | }) 7 | }) 8 | operetta.command('commit', 'Commit Changes', (command) => { 9 | command.options(['-a', '--all'], 'Tell the command to automatically stage files that have been modified and deleted, but new files you have not told git about are not affected.') 10 | command.parameters(['-m', '--message'], 'Use the given message as the commit message.', (value) => { 11 | console.log('Staging modified files.') 12 | }) 13 | command.start() 14 | }) 15 | operetta.command('push', 'Push To Remote Repo', (command) => { 16 | command.start((values) => { 17 | console.log('remote', values.positional[0]) 18 | console.log('branch', values.positional[1]) 19 | }) 20 | }) 21 | operetta.start() 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "operetta", 3 | "description": "The Node Option Parser That Sings!", 4 | "version": "2.0.0", 5 | "author": { 6 | "name": "Dmytri Kleiner", 7 | "email": "dk@trick.ca" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/dmytri/node-operetta/issues" 11 | }, 12 | "engines": { 13 | "node": ">=8.10.0" 14 | }, 15 | "homepage": "https://www.npmjs.com/package/operetta", 16 | "keywords": [ 17 | "parser", 18 | "parsing", 19 | "option parser", 20 | "option parsing", 21 | "gnuopt", 22 | "options", 23 | "option", 24 | "paramaters", 25 | "subcommands", 26 | "command", 27 | "cli", 28 | "argument", 29 | "args" 30 | ], 31 | "license": "Apache-2.0", 32 | "main": "index.js", 33 | "repository": { 34 | "type": "git", 35 | "url": "git://github.com/dmytri/node-operetta.git" 36 | }, 37 | "scripts": { 38 | "test": "standard --fix *.js test/*.js && tape test/*.js" 39 | }, 40 | "devDependencies": { 41 | "standard": "^14.3.4", 42 | "tape": "^5.0.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | var Operetta = require('../index.js') 2 | 3 | var operetta = new Operetta() 4 | 5 | operetta.banner = ' _____ _\n' 6 | operetta.banner += '|_ _|__ ___| |_\n' 7 | operetta.banner += ' | |/ _ \\/ __| __|\n' 8 | operetta.banner += ' | | __/\\__ \\ |_\n' 9 | operetta.banner += ' |_|\\___||___/\\__|\n' 10 | 11 | // Operetta is an Event Emitter 12 | // Values that are not preceeded by an option 13 | // Are passed to the 'positional' event; 14 | operetta.on('positional', function (value) { 15 | console.log('positional:', value) 16 | }) 17 | 18 | operetta.parameters(['-t', '--test'], 'A Test Argument', (value) => { 19 | if (value === undefined) { 20 | // if no value follows options value is undefined 21 | console.log('Test Nothing') 22 | } else { 23 | console.log('Test', value) 24 | } 25 | }) 26 | 27 | operetta.options('--flag', 'A Test Option', () => { 28 | console.log('Flagged!') 29 | }) 30 | operetta.options('-x', 'x!', () => { 31 | console.log('x!') 32 | }) 33 | 34 | operetta.start(function (values) { 35 | console.log(values) 36 | }) 37 | -------------------------------------------------------------------------------- /test/options.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const Operetta = require('../index.js') 3 | 4 | const A = 'one two -X --parameter parameter -Avalue --key=value -s set -s again -z' 5 | 6 | test('no arguments', (t) => { 7 | t.plan(1) 8 | const result = { positional: [] } 9 | new Operetta([]).start((v) => { 10 | t.same(v, result, 'expected result') 11 | }) 12 | }) 13 | 14 | test('no configuration', (t) => { 15 | t.plan(1) 16 | const result = { 17 | positional: ['one', 'two', 'parameter', 'set', 'again'], 18 | '-X': true, 19 | '--parameter': true, 20 | '-A': true, 21 | '-v': true, 22 | '-a': true, 23 | '-l': true, 24 | '-u': true, 25 | '-e': true, 26 | '-s': [true, true], 27 | '--key': 'value', 28 | '-z': true 29 | } 30 | 31 | new Operetta(A.split(' ')).start((v) => { 32 | t.same(v, result, 'expected result') 33 | }) 34 | }) 35 | 36 | test('parameters', (t) => { 37 | t.plan(1) 38 | 39 | const result = { 40 | positional: ['one', 'two'], 41 | '-X': true, 42 | '-p': 'parameter', 43 | '-A': 'value', 44 | '-s': ['set', 'again'], 45 | '--key': 'value', 46 | '-z': true 47 | } 48 | 49 | const operetta = new Operetta(A.split(' ')) 50 | operetta.parameters(['-p', '--parameter'], 'Parameter') 51 | operetta.parameters(['-A', '--value'], 'Value') 52 | operetta.parameters(['-s', '--set'], 'Set') 53 | operetta.start((v) => { 54 | t.same(v, result, 'expected result') 55 | }) 56 | }) 57 | 58 | test('events', (t) => { 59 | t.plan(6) 60 | 61 | const result = { 62 | positional: ['one', 'two', 'parameter'], 63 | '-X': true, 64 | '--parameter': true, 65 | '-A': true, 66 | '-v': true, 67 | '-a': true, 68 | '-l': true, 69 | '-u': true, 70 | '-e': true, 71 | '-s': ['set', 'again'], 72 | '--key': 'value' 73 | } 74 | 75 | const usage = 'Banner\nUsage:\n-s,--set Set\n-z,--zap Zap' 76 | 77 | const operetta = new Operetta(A.split(' ')) 78 | operetta.banner = 'Banner' 79 | operetta.parameters(['-s', '--set'], 'Set') 80 | operetta.parameters(['-z', '--zap'], 'Zap') 81 | operetta.on('-z', (v) => { 82 | t.fail('unset parameter should not raise event') 83 | }) 84 | const vals = ['set', 'again'] 85 | operetta.on('-s', (v) => { 86 | t.same(v, vals.shift(), 'set parameter value correct') 87 | }) 88 | operetta.on('-X', (v) => { 89 | t.same(v, true, 'option value is true') 90 | }) 91 | operetta.usage((h) => { 92 | t.same(h, usage, 'expected usage') 93 | }) 94 | operetta.start((v) => { 95 | t.same(v, result, 'expected result') 96 | t.same(vals, [], 'multiple events') 97 | }) 98 | }) 99 | 100 | test('subcommands', (t) => { 101 | t.plan(3) 102 | 103 | const result = { 104 | positional: ['two', 'parameter'], 105 | '-X': true, 106 | '--parameter': true, 107 | '-A': true, 108 | '-v': true, 109 | '-a': true, 110 | '-l': true, 111 | '-u': true, 112 | '-e': true, 113 | '-s': ['set', 'again'], 114 | '--key': 'value' 115 | } 116 | 117 | const usage = '\nUsage:\n-s,--set Set\n-z,--zap Zap' 118 | 119 | const operetta = new Operetta(A.split(' ')) 120 | operetta.command('one', 'one', (c) => { 121 | c.parameters(['-s', '--set'], 'Set') 122 | c.parameters(['-z', '--zap'], 'Zap') 123 | c.on('-X', (v) => { 124 | t.same(v, true, 'option value is true') 125 | }) 126 | c.on('-z', (v) => { 127 | t.fail('unset parameter should not raise event') 128 | }) 129 | c.usage((h) => { 130 | t.same(h, usage, 'expected usage') 131 | }) 132 | c.start((v) => { 133 | t.same(v, result, 'expected result') 134 | }) 135 | }) 136 | 137 | operetta.start() 138 | }) 139 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /************************************************** 2 | * 3 | * Operetta: A Node Option Parser That Sings! 4 | * Dmytri Kleiner 5 | * 6 | **********************************/ 7 | 8 | 'use strict' 9 | const EventEmitter = require('events') 10 | 11 | module.exports = class Operetta extends EventEmitter { 12 | constructor (argv) { 13 | super() 14 | 15 | if (argv) this.argv = argv 16 | else { 17 | if (process.argv[0].slice(-4) === 'node') this.argv = process.argv.slice(2) 18 | else this.argv = process.argv.slice(1) 19 | } 20 | // options which are parameters 21 | this.params = {} 22 | // options which are not parameters 23 | this.opts = {} 24 | this.commands = {} 25 | this.values = {} 26 | this.values.positional = [] 27 | this.banner = '' 28 | this.help = 'Usage:' 29 | this.parent = null 30 | // universal option detector 31 | this.re = /^(-[^-])([A-Za-z0-9_-]+)?$|^(--[A-Za-z0-9_-]+)[=]?(.+)?$/ 32 | this.parse = (listener) => { 33 | let parameter 34 | const sing = (argument, data) => { 35 | argument = argument || current 36 | this.values[argument] = this.values[argument] || [] 37 | this.values[argument].push(data) 38 | if (this.listeners(argument).length > 0) this.emit(argument, data) 39 | parameter = undefined 40 | current = undefined 41 | } 42 | const process = (option, data) => { 43 | parameter = this.params[option] 44 | if (data || !parameter) sing(parameter || this.opts[option] || option, data || true) 45 | } 46 | while (this.argv.length > 0) { 47 | var current = this.argv.shift() 48 | var m = this.re.exec(current) 49 | if (m) { 50 | if (parameter) sing(parameter, null) 51 | if (m[2]) { 52 | var options = m[1][1] + m[2] 53 | for (const i in options) { 54 | var a = this.params['-' + options[i]] 55 | if (a) { 56 | process(a, options.slice(parseInt(i) + 1)) 57 | break 58 | } else process('-' + options[i]) 59 | } 60 | } else process(m[1] || m[3], m[4]) 61 | } else if (parameter) sing(parameter, current) 62 | else sing('positional', current) 63 | } 64 | if (listener) listener(this.argopt) 65 | } 66 | 67 | this.argopt = new Proxy(this.values, { 68 | get (target, name) { 69 | const v = Reflect.get(target, name) 70 | if (v) { 71 | if (Array.isArray(v) && v.length === 1) { 72 | return v[0] 73 | } else { 74 | return v 75 | } 76 | } 77 | } 78 | }) 79 | } 80 | 81 | start (callback) { 82 | if (this.parent && this.argv.length === 0 && this.noop === false) this.usage() 83 | else { 84 | if (!this.opts['-h']) { 85 | this.options(['-h', '--help'], 'Show Help', () => { 86 | this.usage() 87 | process.exit(1) 88 | }) 89 | } 90 | const arg = this.argv[0] 91 | const command = this.commands[arg] 92 | if (command) { 93 | this.argv.shift() 94 | const child = new Operetta(this.argv) 95 | child.parent = this 96 | command(child) 97 | } 98 | this.parse(callback) 99 | } 100 | } 101 | 102 | bind (argv, description, listener, takesArguments) { 103 | if (argv) { 104 | if (!(typeof argv === 'object' && 'join' in argv)) argv = [argv] 105 | const key = argv[0] 106 | const sargv = argv.join(',') 107 | this.help += '\n' + argv + Array(16 - sargv.length).join(' ') + description 108 | argv.forEach((option) => { 109 | if (takesArguments) this.params[option] = key 110 | else this.opts[option] = key 111 | }) 112 | if (listener) this.on(key, listener) 113 | } 114 | } 115 | 116 | parameters (argv, description, listener) { 117 | this.bind(argv, description, listener, true) 118 | } 119 | 120 | options (argv, description, listener) { 121 | this.bind(argv, description, listener, false) 122 | } 123 | 124 | command (command, description, listener) { 125 | this.help += '\n' + command + Array(16 - command.length).join(' ') + description 126 | this.commands[command] = listener 127 | } 128 | 129 | usage (listener) { 130 | const usage = [this.banner, this.help].join('\n') 131 | if (listener) { 132 | listener(usage) 133 | } else { 134 | console.log(usage) 135 | } 136 | } 137 | 138 | static get operetta () { 139 | return new Operetta() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
  2 | ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~
  3 |  _____  ____  ____  ____  ____  ____  ____   __
  4 | (  _  )(  _ \( ___)(  _ \( ___)(_  _)(_  _) /__\
  5 |  )(_)(  )___/ )__)  )   / )__)   )(    )(  /(__)\
  6 | (_____)(__)  (____)(_)\_)(____) (__)  (__)(__)(__)
  7 | 
  8 |         A Node Option Parser That Sings!
  9 | 
 10 | ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~ ~~~
 11 | 
12 | 13 | # Plot Summary # 14 | 15 | ## Options ## 16 | 17 | **All options are arguments, but not all arguments are options.** 18 | 19 | $ nurl -I --insecure https://topsyturvey.onion/mikado.mkv 20 | 21 | In the example above, the program nurl has three arguments, two of which are 22 | options. Options are arguments that are one letter long and start with a dash 23 | (-), these are "short options," or many letters long and start with a double 24 | dash (--), these are long options. Arguments that are not options are called 25 | "positional" arguments, because they have no name, so can only be referred to 26 | by their position following the command. 27 | 28 | Operetta would parse the above example as follows: 29 | 30 |
 31 | { positional: [ 'https://topsyturvey.onion/mikado.mkv' ],
 32 |  '-I': [ true ],
 33 |  '--insecure': [ true ]}
 34 | 
35 | 36 | For the program to receive these values, it calls the start function with a 37 | callback. 38 | 39 |
 40 | var Operetta = require("operetta");
 41 | operetta = new Operetta();
 42 | operetta.start(function(values) {
 43 |   console.log(values);
 44 | });
 45 | 
46 | 47 | Simple, right? And quite enough for many programs. But that is not all, oh no 48 | that is not all! 49 | 50 | ## Parameters ## 51 | 52 | **All parameters are options but not all options are parameters.** 53 | 54 | $ nysql --database secret_database --host=control.onion -usmart -p Iheart99 55 | 56 | Sometimes options take a value. We call these Parameters. 57 | 58 | The above shows the four valid forms to set values. Without any further 59 | instruction, Operetta would parse the above as follows: 60 | 61 |
 62 | { positional: [ 'secret_database', 'Iheart99' ],
 63 |  '--database': [ true ],
 64 |  '--host': [ 'control.onion' ],
 65 |  '-u': [ true ],
 66 |  '-s': [ true ],
 67 |  '-m': [ true ],
 68 |  '-a': [ true ],
 69 |  '-r': [ true ],
 70 |  '-t': [ true ],
 71 |  '-p': [ true ]
 72 | }
 73 | 
74 | 75 | Uhgg. That's probably not what we want. It got --host right, because that is 76 | the most unambiguous form for a parameter to take, a long option connected to a 77 | value by an equal sign. However the rest, what a mess! Since it doesn't know 78 | that --database and -p are parameters, it treats "secret_database" and 79 | "Iheart99" as positional arguments, and since short options can be chained 80 | together, Operetta thinks "usmart" is a chain of 6 options. We're going to have 81 | to give operetta more information to handle these correctly. 82 | 83 |
 84 | var Operetta = require("operetta").Operetta;
 85 | operetta = new Operetta();
 86 | operetta.parameters(['-D','--database'], "Database");
 87 | operetta.parameters(['-H','--host'], "Host");
 88 | operetta.parameters(['-u','--user'], "User");
 89 | operetta.parameters(['-p','--password'], "Password");
 90 | operetta.start(function(values) {
 91 |   console.log(values);
 92 | });
 93 | 
94 | 95 | We use the parameters function to tell Operetta some things about our parameters, 96 | first we pass a list of options, i.e. ['-D','--database'], this gives the long 97 | and short form of the option, then we give a description. 98 | 99 | Now, we get the follow values: 100 | 101 |
102 | { positional: [],
103 |  '-D': [ 'secret_database' ],
104 |  '-H': [ 'control.onion' ],
105 |  '-u': [ 'smart' ],
106 |  '-p': [ 'Iheart99' ]}
107 | 
108 | 109 | Much better! Note that the key for the value is always the first item in the 110 | options list passed, so -D is present, even though --database was used. 111 | 112 | ## Help ## 113 | 114 | What's more is now that we have descriptions, operetta will automatically bind 115 | the options -h and --help to show these descriptions as help. 116 | 117 |
118 | $ nysql --help
119 | 
120 | Usage:
121 | -D,--database  Database
122 | -H,--host      Host
123 | -u,--user      User
124 | -p,--password  Password
125 | -h,--help      Show Help
126 | 
127 | 128 | Nifty, huh? But what about plain old options? We may want to give these 129 | descriptions too. For example, in our earlier nurl example, we may want to 130 | provide descriptions for -I and --insecure. We can use the options function for 131 | this. 132 | 133 |
134 | operetta.options(['-I','--head'], "Show document info only");
135 | operetta.options(['-k','--insecure'], "Allow connections to SSL sites without certs");
136 | 
137 | 138 | If you really insist, you can can override -h and --help using either the 139 | options or parameters function, you can then then get the help output by 140 | calling the usage function, either with or without a callback. 141 | 142 |
143 | // this will call console.log with help output.
144 | operetta.usage();
145 | // this will pass usage text to a callback.
146 | operetta.usage(function(help) {
147 |   console.log(help);
148 | });
149 | 
150 | 151 | We can add a banner above line that says "Usage." 152 | 153 |
154 | operetta.banner = "NySQL. The Nultimate Natabase!\n";
155 | 
156 | 157 | Now we get the following Help: 158 |
159 | NySQL. The Nultimate Natabase!
160 | 
161 | Usage:
162 | -D,--database  Database
163 | -H,--host      Host
164 | -u,--user      User
165 | -p,--password  Password
166 | -h,--help      Show Help
167 | 
168 | 169 | There you go! Now you can add options and parameters to your program and have 170 | it display nice help with the descriptions. That's all you need right? But that 171 | is not all operetta can do! Oh no, that is not all! 172 | 173 | ## Events ## 174 | 175 | Sometimes you don't just want all the options parsed and dumped to a single 176 | callback as a values object, but you wold rather have an event triggered for 177 | each option. Here is where Operetta Sings! 178 | 179 | The operetta object is an EventEmitter, so you can bind events with the on 180 | option. 181 | 182 |
183 | operetta.on('-k', function(value) {
184 |   console.log('Warning! The url you are requesting has not given any money to the SSL root certificate racketeers, and so while it's probably perfectly secure, it is not contributing to the profits of any money grubbing certificate authority!');
185 | });
186 | 
187 | 188 | Since -k is just an option, value will always be true when this event is 189 | called, in the case of a parameter, value will be the value passed or null if 190 | none was passed. 191 | 192 | While using the on function works, the preferred way to set a callback is to 193 | pass it as the third argument to either the options or parameters function. 194 | 195 |
196 | operetta.options(['-k','--insecure'], "Allow connections to SSL sites without certs", function(value) {
197 |   console.log('Danger! Danger, Will Robinson!');
198 | });
199 | 
200 | 201 | So there you have it, Options, Parameters, Help and Events. Surely that's the 202 | end of this interminable readme file? No! That's not all. And stop calling me 203 | Shirley. 204 | 205 | ## Subcommands 206 | 207 | Sometimes programs have different commands, each with their own options, i.e. 208 | 209 |
210 |  $ nit clone http://nitnub.onion/nit.nit
211 |  $ nit commit -am "lotsa great codez"
212 |  $ nit push origin master
213 | 
214 | 215 | If the program nit has many subcommands, i.e. clone, commit, push then each of 216 | these could have their own options and help. Operetta has a command function 217 | that allows you to define these and get a new instance of operetta for 218 | each command. 219 | 220 |
221 | operetta.command('clone', "Clone a Repo", function(command) {
222 |   command.start(function(values) {
223 |     var url = values.positional[0];
224 |   });.
225 | });
226 | operetta.command('commit', "Commit Changes", function(command) {
227 |   command.options(['-a','--all'], "Tell the command to automatically stage files that have been modified and deleted, but new files you have not told git about are not affected.");
228 |   command.parameters(['-m','--message'], "Use the given message as the commit message.", function(value) {
229 |     console.log("Staging modified files.");
230 |   });.
231 |   command.start();
232 | });
233 | operetta.command('push', "Push To Remote Repo", function(command) {
234 |   command.start(function(values) {
235 |     var remote = values.positional[0],
236 |       branch = values.positional[1];
237 |   });.
238 | });
239 | operetta.start();
240 | 
241 | 242 | Now, if you called help without a subcommand: 243 | 244 | $ nit -h 245 | 246 |
247 | Usage:
248 | clone          Clone a Repo
249 | commit         Commit Changes
250 | push           Push To Remote Repo
251 | -h,--help      Show Help
252 | 
253 | 254 | You get a list of the subcommands. 255 | 256 | However, if you call help on commit: 257 | 258 | $ nit commit --help 259 | 260 |
261 | Usage:
262 | -a,--all       Tell the command to automatically stage files that have been modified and deleted, but new files you have not told git about are not affected.
263 | -m,--message   Use the given message as the commit message.
264 | -h,--help      Show Help
265 | 
266 | 267 | You get the descriptions of the options defined for commit. 268 | 269 | And yes, if you really want, subcommand can have subcommands: 270 | 271 |
272 | operetta.command('submodule', "Manage Submodules", function(command) {
273 |   command.command('add', "Add A submodule to the repo", function(subcommand) {
274 |     subcommand.start();
275 |   });.
276 | });
277 | 
278 | 279 | Now you could do: 280 | 281 | $ nit submodule add http://nitorious.onion/nitorious.nit 282 | 283 | # Coda # 284 | 285 | So, options, parameters, help, events and subcommands. Shirley, you're thinking 286 | operetta must be some big, baroque, bloated, blob of blubbery JavaScript! Well, 287 | here's what SLOCcount has to say: 288 | 289 |
290 | Total Physical Source Lines of Code (SLOC)                = 107
291 | Development Effort Estimate, Person-Years (Person-Months) = 0.02 (0.23)
292 |  (Basic COCOMO model, Person-Months = 2.4 * (KSLOC**1.05))
293 |  Schedule Estimate, Years (Months)                         = 0.12 (1.43)
294 |   (Basic COCOMO model, Months = 2.5 * (person-months**0.38))
295 | 
296 | 297 | That's right, small and cheap. So far it's only got One Hundred and Seven Lines 298 | of Code. So get it while it's small, before I add thousands of lines to support 299 | such must-have features as sending and receiving email and impersonating 300 | a teenager in IRC channels. 301 | 302 | And yes, I called you Shirley. 303 | 304 | 305 | --------------------------------------------------------------------------------