Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 6638e827d5371fed2a9d394649d3e07cf2c25566 0 parents
@frodwith authored
1  .gitignore
@@ -0,0 +1 @@
+*.js
16 Makefile
@@ -0,0 +1,16 @@
+%.js : %.coffee
+ coffee -cs < $< > $@
+
+JSFILES := $(shell find . -name '*.coffee' | sed s/\.coffee$$/.js/)
+
+coffee: $(JSFILES)
+
+all: coffee
+
+test: coffee
+ nodeunit test
+
+clean:
+ rm $(JSFILES)
+
+.PHONY : all test coffee
122 README.md
@@ -0,0 +1,122 @@
+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.
185 getopt.coffee
@@ -0,0 +1,185 @@
+exports[t] = t.toUpperCase() for t in ['flag', 'scalar', 'array', 'object']
+
+exports.BaseError = class BaseError extends Error
+ constructor: (@desc) ->
+ Error.captureStackTrace(this, this.constructor)
+ @message = @describe()
+
+ toString: () ->
+ 'getopt.' + @constructor.name + ': ' + @describe()
+
+ describe: () ->
+ JSON.stringify(@desc)
+
+exports.InternalError = class InternalError extends BaseError
+exports.UnvalidatedType = class UnvalidatedType extends InternalError
+
+exports.SpecError = class SpecError extends BaseError
+exports.InvalidShort = class InvalidShort extends SpecError
+exports.Conflict = class Conflict extends SpecError
+exports.NoType = class NoType extends SpecError
+exports.UnknownType = class UnknownType extends SpecError
+
+exports.ParseError = class ParseError extends BaseError
+
+exports.UnknownOption = class UnknownOption extends ParseError
+ describe: () -> "Unknown option #{ @desc } encountered"
+
+exports.NoValue = class NoValue extends ParseError
+ describe: () -> @desc + " requires a value, but none was given"
+
+exports.FlagWithValue = class FlagWithArguments extends ParseError
+ describe: () ->
+ "#{ @desc.name } was given \"#{ @desc.value }\", but takes no arguments"
+
+exports.ScalarWithValues = class ScalarWithValues extends ParseError
+ describe: () -> @desc + " was given more than one value"
+
+exports.ObjectNotPair = class ObjectNotPair extends ParseError
+ describe: () ->
+ @desc.name + " expects a key=value pair, but got " + @desc.value
+
+exports.ObjectKeyRepeated = class ObjectKeyRepeated extends ParseError
+ describe: () -> "#{ @desc.name }.#{ @desc.key } given more than once"
+
+exports.RequiredMissing = class RequiredMissing extends ParseError
+ describe: () -> "Required option #{ @desc } was missing"
+
+class Result
+ constructor: (@parser) ->
+ @argv = []
+
+ # Add a positional argument to the result
+ arg: (blob) -> @argv.push blob
+
+ # Mark a flag by given option name
+ flag: (name) ->
+ throw new NoValue name unless @parser.isFlag name
+ target = @parser.index[name]
+ this[target] = true
+
+ # Mark a param (scalar, array, or object) by name with value.
+ param: (name, value) ->
+ p = @parser
+ target = p.index[name]
+ throw new UnknownOption name unless target
+ spec = p.spec[target]
+ switch spec.type
+ when exports.flag
+ throw new FlagWithValue name: name, value: value
+ when exports.scalar
+ if this[target]
+ throw new ScalarWithValues name
+ this[target] = value
+ when exports.array
+ o = this[target] or= []
+ o.push value
+ when exports.object
+ pair = value.split '='
+ unless pair.length is 2
+ throw new ObjectNotPair name: name, value: value
+ o = this[target] or= {}
+ [k,v] = pair
+ if o[k]
+ throw new ObjectKeyRepeated name: name, key: k
+ o[k] = v
+ else
+ throw new UnvalidatedType(name)
+
+ # Set defaults, throw errors if required parameters weren't seen.
+ finalize: () ->
+ p = @parser
+ for own target, spec of p.spec
+ if (spec.type isnt exports.flag) and this[target]?
+ this[target] = spec.default
+
+ if spec.required and (
+ (spec.type is exports.scalar and not value) or
+ (spec.type is exports.array and value.length < 1)
+ )
+ throw new RequiredMissing(target)
+
+ return this
+
+exports.Parser = class Parser
+ constructor: (spec) ->
+ @spec = spec
+ @index = {}
+ for own target, subspec of spec
+
+ # upgrade scalars to arrays
+ longs = subspec.long or [target]
+ longs = [longs] unless longs.length?
+ subspec.long = longs
+
+ shorts = subspec.short or []
+ shorts = [shorts] unless shorts.length?
+ subspec.short = shorts
+
+ alias = longs.slice(0)
+
+ for s in shorts
+ if s.length isnt 1
+ throw new InvalidShort(s)
+ alias.push s
+
+ for a in alias
+ existing = @index[a]
+ throw new Conflict(target, a, existing) if existing
+ @index[a] = target
+
+ t = subspec.type
+ throw new NoType(target) unless t
+
+ if t is exports.array
+ spec.default or= []
+ else if t is exports.object
+ spec.default or= {}
+ else if t isnt exports.scalar and t isnt exports.flag
+ throw new UnknownType name: target, type: t
+
+
+ isFlag: (name) ->
+ target = @index[name]
+ throw new UnknownOption(name) unless target
+ @spec[target].type is exports.flag
+
+ parse: (argv) ->
+ result = new Result(this)
+ i = 0
+ while i < argv.length
+ blob = argv[i]
+
+ # special case for single - (usually means stdin)
+ if blob is '-'
+ result.arg blob
+
+ # -- stop processing and treat the rest of argv
+ else if blob is '--'
+ result.arg(a) for a in argv.slice(i+1)
+ break
+
+ # -vax is equivalent to -v -a -x
+ else if match = /^-(\w\w+)$/.exec(blob)
+ result.flag(f) for f in match[1].split()
+
+ # -a=42 or --answer=42 or even -answer=42 (DWIM a bit)
+ else if match = /^--?(\w+)=(.*)$/.exec(blob)
+ result.param(match[1], match[2])
+
+ # -v or --verbose
+ else if match = /^-(\w)$/.exec(blob) or
+ /^--(\w+)$/.exec(blob)
+ name = match[1]
+ if @isFlag name
+ result.flag name
+ else
+ # If it's not a flag, consume the next argument as value
+ result.param name, argv[++i]
+ else
+ # treat everything else as positional
+ result.arg(blob)
+
+ i += 1
+
+ return result.finalize()
20 test/usage.coffee
@@ -0,0 +1,20 @@
+getopt = require('getopt')
+
+exports.oneFlag = (t) ->
+ t.expect 5
+
+ p = new getopt.Parser
+ verbose:
+ type: getopt.flag
+ short: 'v'
+
+ o = p.parse ['-v']
+ t.ok o.verbose
+ o = p.parse []
+ t.ok not o.verbose
+ o = p.parse ['--verbose']
+ t.ok o.verbose
+ o = p.parse ['--verbose', '-v', '--verbose', '-v']
+ t.ok o.verbose
+ t.throws (() -> p.parse ['-v', '--frobnicate']), getopt.UnknownOption
+ t.done()
Please sign in to comment.
Something went wrong with that request. Please try again.