Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Documentation, help tests passing

  • Loading branch information...
commit 4a08e8dcec18995e3f88f8a435c17cd933d94a61 1 parent f22aaf2
@frodwith authored
View
2  Makefile
@@ -11,6 +11,6 @@ test: coffee
nodeunit test/*.js
clean:
- rm $(JSFILES)
+ rm -f $(JSFILES)
.PHONY : all test coffee
View
239 README.markdown
@@ -0,0 +1,239 @@
+Getopt
+======
+
+For the impatient
+-----------------
+If you're in a hurry and you just want to parse your options already:
+
+ getopt = require('getopt');
+ options = getopt.simple({
+ verbose: {
+ type: getopt.flag,
+ short: 'v',
+ },
+ input: {
+ type: getopt.list,
+ short: 'i',
+ },
+ output: {
+ type: getopt.scalar,
+ description: 'output file (- for stdout)',
+ default: '-',
+ short: 'o',
+ required: true,
+ },
+ });
+
+options.verbose is true or false, input is an array (possibly empty), and
+output is a string (or undefined). You also get a --help option for free that
+prints out:
+
+ node myscript.pl [OPTIONS]
+
+ The following options are accepted:
+
+ -h
+ --help Prints this message and exits.
+
+ -v
+ --verbose
+
+
+ -i VAL
+ --input=VAL
+
+ -o VAL
+ --output=VAL output file (- for stdout). Required. Default: -
+
+getopt.simple() doesn't do what I want!
+=======================================
+Yeah, sorry about that. getopt.simple() does what I usually want with a
+minimum of fuss and ceremony, but have a look at the lower level APIs. Very
+probably, you can coerce them into doing what you want, though you might have
+to write try/catch blocks (the horror!) and call process.exit() yourself, and
+so on. The whole API is designed to allow you to use just the parts of it that
+are useful for you. So, read the rest of the documentation :)
+
+Why?
+----
+
+As if Node didn't have enough option parsers. As of this writing, most of them
+are almost good enough for me. None of them quite measures up to the power of
+perl's Getopt::Long though. Getopt::Long's interface sucks, but its parser is
+very flexible. This module aims to have an interface that doesn't suck and
+still be flexible, but you'll be the judge.
+
+Specification
+-------------
+
+The primary way you give information to getopt is through a specification
+object. The keys are the names of targets (keys in the result object), and the
+values are objects with the following keys:
+
+### type
+
+This tells getopt what kind of thing you're trying to parse. It absolutely
+must be one of the following - you cannot pass a string, and there is no
+default.
+
+#### getopt.flag (or getopt.bool)
+
+Just an "on" switch. It'll be true if it was passed, and false if it wasn't.
+All the following forms are valid:
+
+ -vax
+ -v -a -x
+ --verbose --anthropomorphic --xenophobic
+
+#### getopt.scalar (or getopt.string)
+
+Expects one (and only one) value. If present at all, its value will be some
+kind of string. Passing more than one argument for options of this type will
+make getopt throw an error. The following forms are all valid:
+
+ -a42
+ -a 42
+ --answer 42
+ --answer=42
+
+#### getopt.array (or getopt.list)
+
+Expects zero or more values. You'll get an array in the result object of all
+the strings passed for this argument.
+
+ node script.js --foo=one --foo=two --foo=three
+***
+ { foo: ['one', 'two', 'three'] }
+
+#### getopt.object (or getopt.hash)
+
+This one is a little odd: like list, you can pass it multiple times, but the
+result will be an object (or hash) instead of an array, and the value will be
+split into key/value on the = sign. An example will probably explain better:
+
+ define: {
+ type: getopt.object,
+ short: '-D'
+ }
+***
+ node --script.js -Dfoo=bar -Dbar=baz
+***
+ { define: { foo: "bar", bar: "baz" } }
+
+### short
+
+Either a string or an array of strings. All must be one character long (leave
+off the -). This creates short aliases for your argument. The target will be
+the same, though, and aliases can be mixed. In addition, for flag type shorts,
+they can be chained together. No short aliases are created by default.
+
+### long
+
+A list of long names for your option, either a string or an array of strings
+(leave off the --). By default, this is just the name of your target. If you
+do specify some longs, you must also include the name of your target if you
+wish it to be a long alias.
+
+### required
+
+A boolean. This causes getopt.parse to throw an exception if the argument
+wasn't given, and is only valid for scalars and lists. In the list case, at
+least one value must be given.
+
+### default
+
+A default value for your option if none is parsed from the command line.
+Arrays and objects default to empty arrays and objects unless you say
+otherwise.
+
+### description
+
+Purely optional, this should be a string explaining what your option does. The
+help generator makes use of this -- see getopt.help() for details.
+
+Positional Arguments
+--------------------
+Anything that doesn't look like an option will get aggregated into the result
+object's .argv property, in the order it was found in the actual argv. '-' is
+treated as positional, and '--' will cause getopt to stop processing and
+report the rest of the args as positional.
+
+Unrecognized Arguments
+----------------------
+Any options given to the program that aren't specified in the options spec
+will cause an error to be thrown.
+
+API
+===
+
+### getopt.parse(spec, argv)
+
+Processes argv without modifying it and returns a result object, which will
+have an argv property and other properties depending on the option spec. Any
+errors in parsing will throw an exception. If you don't specify argv,
+process.argv (minus the first two elements) will be used.
+
+### getopt.tryParse(spec, argv)
+
+Wraps getopt.parse in a try/catch block. If exceptions are encountered, they
+are printed to stderr and the process will exit with a non-zero code.
+
+### getopt.zero()
+
+Returns the program name (e.g. "node myscript.js"). Useful when generating a
+usage message.
+
+### getopt.usage()
+
+Uses getopt.zero() to generate a usage message of the form:
+"Usage: node myscript.js [OPTIONS]". This is the default message used by
+getopt.simple().
+
+### getopt.help(spec)
+
+Returns a formatted string describing the options specified. Used internally
+by getopt.simple(), but you are encouraged to use it outside that context.
+
+The description field of the spec is examined. If you didn't include a period
+at the end of the description, one will be added. Other bits of explanatory
+text (like "Required", "Default: " or "Can be specified multiple times") will
+be added to the end of the description to keep you from having to duplicate
+spec information in the description.
+
+### getopt.simple(spec, banner, argv)
+
+Behaves similarly to getopt.tryParse(), except that it adds a "help" option
+and then checks for it. If --help is passed, spec will be passed to
+getopt.help() to generate some the help. The given banner will be printed,
+followed by this help. Banner and argv are both optional: banner defaults to
+getopt.usage(), and argv() defaults to process.argv as in getopt.parse().
+
+Errors
+======
+There is a whole heirarchy of error classes, mostly for testing purposes. If
+you don't catch them, node will print a stacktrace for them like the common
+errors. If you do, you can use their "message" member to print out something
+useful to the user.
+
+Any time you create a Parser or Help object (internal classes used by the api
+methods above), an exception will be thrown if there is something inconsistent
+about your option specification. This helps you catch errors sooner. These
+exceptions are instances of getopt.SpecError, but you probably shouldn't try
+to catch them. getopt.tryParse et al will rethrow them.
+
+Calling getopt.parse() an throw exceptions if the user has given bad options
+on the command line. These are generally the ones you want to try to catch and
+print. They are all instances of getopt.ParseError.
+
+I think getopt should behave differently.
+=========================================
+
+I value your opinion, I really do. I also have a job, and it isn't maintaining
+getopt. Please, please either include a patch in your correspondance or send
+me a pull request on github. Otherwise, your issue may or may not be
+addressed, but I'm gonna go out on a limb and say it probably won't be. Thanks
+in advance for your contributions :)
+
+If you insist on not fixing my code for me, though, you can report an issue
+through github. If it's a bug that effects me, it will very likely get fixed.
+If not, perhaps you should reconsider fixing my code for me :)
View
122 README.md
@@ -1,122 +0,0 @@
-special flags
-=============
-A single dash '-' is treated as positional.
-Double dash '--' is removed from the array and stops processing, so that
-everything that comes after it is treated as positional.
-
-the specification object
-========================
-
-keys are target names (what you check for in the result object)
-values are the specification for that target. Keys for the specification are:
-
-short:
- array, each element is a one-character string. If you specify nothing,
- there will be no short alias.
-
-long:
- array, each element is a string. If you specify nothing, the name of the
- target will be used.
-
-type:
- needs to be one of getopt.boolean, getopt.string, getopt.array, or
- getopt.object. See the "types" section.
-
-types
-=====
-
-flag:
- No value is allowed. Short flags can appear in groups such that
- -vax
- is equivalent to
- -v -a -x
-
- Long flags look like:
- --verbose --add --xerox
-
- If the user tries to give a value for a flag with an equals, e.g.
- -v=42
- --verbose=42
- then an error will be thrown. If they try to give one with a space,
- though, , e.g.
- -v 42
- --verbose 42
-
- then -v will be set and 42 will be treated as a positional argument.
-
- A flag can be specified multiple times without error.
-
-scalar:
- A single value is allowed. Not giving a value throws an error. Short
- scalars look like:
- -v 42
- -v=42
-
- Long scalars look like:
- --verbosity 42
- --verbosity=42
-
- Scalars take an additional option, "required" (defaults to false). If this
- is set, an error will be thrown if no value is supplied or the supplied
- value is the empty string. Specifying this will cause "(required)" to be
- appended to your description.
-
- Scalars can also have a "default". This will be the value if none is
- specified, although an empty value can still be specified like
- --verbosity=
-
- Specifying this will cause "(default: value)" to be appended to the
- description.
-
-array:
- The option can be specified zero, one, or many times, and the value will
- be aggregated. This looks like:
-
- --foo one --foo two --foo three
- -f=one -f=two -f=three
- etc.
-
- Arrays can use "required" and "default" just like scalars, except that
- default should be an array. If any values are supplied, the default is not
- used.
-
-object:
- The values supplied must be in the form "name=value". This looks like:
- -D foo=bar --define bar=baz -D=baz=qux --define=qux=quux
-
- Supplying the same key twice will cause an error to be thrown.
-
- Objects can use "default" just like scalars, except
- that default should be an object. If any key/value pairs are supplied, the
- default is ignored.
-
- Objects cannot be "required".
-
-positional arguments
-====================
-
-Anything that doesn't look like an option will be aggregated into the
-"positional" array, and can appear anywhere in the argument list.
-
-unrecognized arguments
-======================
-
-Any options given to the program that aren't specified in the options spec
-will cause an error to be thrown.
-
-errors
-======
-Errors are thrown when you call "parse", and are string exceptions.
-Typical usage would be something like:
-
-parser = new GetOpt(spec)
-
-try {
- parser.parse(process.argv)
-}
-catch (e) {
- console.log(e)
- console.log(parser.usage())
-}
-
-But you can of course do something different.
View
14 examples/simple.coffee
@@ -0,0 +1,14 @@
+getopt = require 'getopt'
+sys = require 'sys'
+
+o = getopt.simple
+ munge:
+ type: getopt.flag
+ description: 'Whether or not to munge'
+ ickiness:
+ type: getopt.scalar
+ default: 1
+ description: 'How icky to make the munging'
+
+if o.munge
+ sys.puts "Munge..." for [1..o.ickiness]
View
40 lib/Help.coffee
@@ -32,33 +32,40 @@ exports.Help = class Help
left.push leftCol(sub, s, true) for s in sub.long.slice(0).sort()
line.left = left
- line.fl = left[0]
- line.ll = left[left.length-1]
- line.right = sub.description.trim()
- line.right += '.' unless line.right.match(/\.$/)
- line.right += ' Required.' if sub.required
+ line.first = left[0]
+ line.last = left[left.length-1]
+
+ right = []
+ if desc = sub.description.trim().replace(/\.$/, '')
+ right.push desc
+
+ right.push 'Required' if sub.required
+ if sub.type is type.array or sub.type is type.object
+ right.push 'Can be specified multiple times'
if sub.default
- line.right += ' Default: ' + JSON.stringify(sub.default)
+ right.push 'Default: ' + JSON.stringify(sub.default)
+
+ line.right = right.join('. ')
+ line.right += '.' if right.length
lines.push line
# Right-justify the left sides
- lengths = (l.ll.length for l in lines)
+ lengths = (l.last.length for l in lines)
max = Math.max.apply(Math, lengths)
for line in lines
for l, i in line.left
pad = max - l.length
l = ' ' + l for [1..pad] if pad
- line.left[i] = l + ' '
-
- # Left-justify the right sides two spaces away
+ line.left[i] = l
+ line.left[line.left.length-1] += ' '
+ # Left-justify the right sides two spaces away, wrapped to tw
remaining = @tw - max - 2
sep = "\n"
sep += ' ' for [1..max+2]
-
for l, i in lines
words = (w.trim() for w in l.right.split /\s/)
words = (w for w in words if w.length > 0)
@@ -73,7 +80,10 @@ exports.Help = class Help
cur = w
lines[i].right = all + cur
- lines.sort (a, b) -> (a.rank - b.rank) or a.fl.localeCompare(b.fl)
- out = (l.left.join("\n") + l.right for l in lines).join "\n\n"
- console.log out
- out
+ # Flags first, then scalars, etc -- sorted by beginning of line in
+ # lex order. This is as much so that we can have a canonical order to
+ # test as anything.
+ lines.sort (a, b) ->
+ (a.rank - b.rank) or a.first.localeCompare(b.first)
+
+ (l.left.join("\n") + l.right for l in lines).join "\n\n"
View
57 lib/getopt.coffee
@@ -1,3 +1,6 @@
+file = require 'file'
+path = require 'path'
+
reexport = (module) ->
m = require module
exports[key] = val for own key, val of m
@@ -8,3 +11,57 @@ reexport m for m in [
'./Parser'
'./Help'
]
+
+getargs = (argv) -> argv or process.argv.slice 2
+
+exports.parse = (spec, argv) ->
+ new exports.Parser(spec).parse getargs argv
+
+exports.zero = () ->
+ abs = path.normalize process.argv[1]
+ rel = file.path.relativePath process.cwd(), abs
+ script = if rel.length < abs.length then rel else abs
+ "#{ process.argv[0] } #{ script }"
+
+exports.usage = () -> "\nUsage: #{ exports.zero() } [OPTIONS]\n"
+exports.help = (spec) -> new exports.Help(spec).toString()
+
+exports.tryParse = (spec, argv) ->
+ p = new exports.Parser(spec)
+ try
+ o = p.parse getargs argv
+ catch e
+ throw e unless e instanceof exports.ParseError
+ process.stderr.write e.message + "\n"
+ process.exit 1
+
+ return o
+
+exports.simple = exports.tryParseWithHelp = (spec, banner, argv) ->
+ spec.help or=
+ type: exports.flag
+ short: 'h'
+ description: 'Print this message and exit'
+
+ p = new exports.Parser spec
+ try
+ argv = getargs argv
+ o = p.parse getargs argv
+ if o.help
+ h = exports.help spec
+ banner or= exports.usage()
+ process.stdout.write [
+ banner
+ "The following options are recognized:\n"
+ h,
+ ].join("\n") + "\n\n"
+ process.exit 0
+ catch e
+ throw e unless e instanceof exports.ParseError
+ h = spec.help
+ flag = if h.long then "--#{ h.long }" else "-#{ h.short }"
+ process.stderr.write e.message +
+ ". Pass #{ flag } for usage information.\n"
+ process.exit 1
+
+ return o
View
1  lib/types.coffee
@@ -1,6 +1,7 @@
exports[t] = t.toUpperCase() for t in ['flag', 'scalar', 'array', 'object']
exports[key] = exports[val] for own key, val of {
+ string: 'scalar'
list: 'array'
hash: 'object'
bool: 'flag'
View
4 package.json
@@ -10,10 +10,10 @@
"test" : "./test"
},
"maintainers" : [
- { name: "Paul Drvier", email: "frodwith@gmail.com" }
+ { "name": "Paul Driver", "email": "frodwith@gmail.com" }
],
"contributors" : [
- { name: "Paul Drvier", email: "frodwith@gmail.com" }
+ { "name": "Paul Drvier", "email": "frodwith@gmail.com" }
],
"licences" : [
{
View
42 test/help.coffee
@@ -1,5 +1,25 @@
getopt = require '../lib/getopt'
+expected = '''
+ -v
+ --verbose Print debugging messages.
+
+ --password=VAL Secret string to use when connecting to server. This
+ description is going to be ridiculously long so that we can
+ test the line breaking a bit. Required.
+
+ -o VAL
+ --output=VAL Filename (- for stdout) to write output to. Default: "-".
+
+ -i VAL
+ --input=VAL Filename(s) (- for stdin) to read input from. Can be
+ specified multiple times. Default: ["-"].
+
+ -D KEY=VAL
+ --define KEY=VAL Symbols to define during processing. Can be specified
+ multiple times. Default: {}.
+ '''
+
exports.basic = (t) ->
t.expect 1
spec =
@@ -19,7 +39,6 @@ exports.basic = (t) ->
default: ['-']
password:
type: getopt.scalar
- short: 'p'
description: 'Secret string to use when connecting to server. This description is going to be ridiculously long so that we can test the line breaking a bit'
required: true
symbols:
@@ -28,23 +47,6 @@ exports.basic = (t) ->
long: 'define'
description: 'Symbols to define during processing'
u = new getopt.Help spec
- t.equals u.toString(), '''
--v
---verbose Print debugging messages.
-
--o VAL
---output=VAL Filename (- for stdout) to write output to. default: -
-
---password=VAL Secret string to use when connecting to server. This
- description is going to be ridiculously long so that we can
- test the line breaking a bit. Required.
-
--i VAL
---input=VAL Filename(s) (- for stdin) to read input from. Can be
- specified multiple times.
-
--D KEY=VAL
---define KEY=VAL Symbols to define during processing. Can be specified
- multiple times.
-'''
+ console.log(u.toString())
+ t.equals u.toString(), expected
t.done()
View
3  test/parse.coffee
@@ -178,10 +178,11 @@ exports.long = (t) ->
exports['aliased types'] = (t) ->
- t.expect 3
+ t.expect 4
t.equal(getopt.list, getopt.array)
t.equal(getopt.hash, getopt.object)
t.equal(getopt.bool, getopt.flag)
+ t.equal(getopt.string, getopt.scalar)
t.done()
exports.complicated = (t) ->
Please sign in to comment.
Something went wrong with that request. Please try again.