mirror of
https://github.com/jashkenas/coffeescript.git
synced 2026-01-14 17:27:59 -05:00
* Add #! support for executable scripts on Linux. Pass arguments to executable script unchanged if using "#!/usr/bin/env coffee". (Previously, "./test.coffee -abck" would be turned into "-a -b -c -k", for example.) Fixes #1752. * refactor option parsing clean up parsing code and in the process fix oustanding bug where coffeescript modified arguments meant for an executable script * address comments * intermediate save * add note saying where OptionParser is used in coffee command * add some more work * fix flatten functions * refactor tests * make argument processing less confusing * add basic test * remove unused file * compilation now hangs * remove unnecessary changes * add tests!!! * add/fix some tests * clarify a test * fix helpers * fix opt parsing * fix infinite loop * make rule building easier to read * add tests for flag overlap * revamp argument parsing again and add more thorough testing * add tests, comment, clean unused method * address review comments * add test for direct invocation of shebang scripts * move shebang parsing test to separate file and check for browser * remove TODO * example backwards compatible warnings * add correct tests for warning 1 * add tests for warnings * commit output js libs and update docs * respond to review comments also add tests for help text * respond to review comments * fix example output * Rewrite argument parsing documentation to be more concise; add it to sidebar and body; add new output * Don’t mention deprecated syntax; clean up variable names
166 lines
6.0 KiB
CoffeeScript
166 lines
6.0 KiB
CoffeeScript
{repeat} = require './helpers'
|
||
|
||
# A simple **OptionParser** class to parse option flags from the command-line.
|
||
# Use it like so:
|
||
#
|
||
# parser = new OptionParser switches, helpBanner
|
||
# options = parser.parse process.argv
|
||
#
|
||
# The first non-option is considered to be the start of the file (and file
|
||
# option) list, and all subsequent arguments are left unparsed.
|
||
#
|
||
# The `coffee` command uses an instance of **OptionParser** to parse its
|
||
# command-line arguments in `src/command.coffee`.
|
||
exports.OptionParser = class OptionParser
|
||
|
||
# Initialize with a list of valid options, in the form:
|
||
#
|
||
# [short-flag, long-flag, description]
|
||
#
|
||
# Along with an optional banner for the usage help.
|
||
constructor: (ruleDeclarations, @banner) ->
|
||
@rules = buildRules ruleDeclarations
|
||
|
||
# Parse the list of arguments, populating an `options` object with all of the
|
||
# specified options, and return it. Options after the first non-option
|
||
# argument are treated as arguments. `options.arguments` will be an array
|
||
# containing the remaining arguments. This is a simpler API than many option
|
||
# parsers that allow you to attach callback actions for every flag. Instead,
|
||
# you're responsible for interpreting the options object.
|
||
parse: (args) ->
|
||
# The CoffeeScript option parser is a little odd; options after the first
|
||
# non-option argument are treated as non-option arguments themselves.
|
||
# Optional arguments are normalized by expanding merged flags into multiple
|
||
# flags. This allows you to have `-wl` be the same as `--watch --lint`.
|
||
# Note that executable scripts with a shebang (`#!`) line should use the
|
||
# line `#!/usr/bin/env coffee`, or `#!/absolute/path/to/coffee`, without a
|
||
# `--` argument after, because that will fail on Linux (see #3946).
|
||
{rules, positional} = normalizeArguments args, @rules.flagDict
|
||
options = {}
|
||
|
||
# The `argument` field is added to the rule instance non-destructively by
|
||
# `normalizeArguments`.
|
||
for {hasArgument, argument, isList, name} in rules
|
||
if hasArgument
|
||
if isList
|
||
options[name] ?= []
|
||
options[name].push argument
|
||
else
|
||
options[name] = argument
|
||
else
|
||
options[name] = true
|
||
|
||
if positional[0] is '--'
|
||
options.doubleDashed = yes
|
||
positional = positional[1..]
|
||
|
||
options.arguments = positional
|
||
options
|
||
|
||
# Return the help text for this **OptionParser**, listing and describing all
|
||
# of the valid options, for `--help` and such.
|
||
help: ->
|
||
lines = []
|
||
lines.unshift "#{@banner}\n" if @banner
|
||
for rule in @rules.ruleList
|
||
spaces = 15 - rule.longFlag.length
|
||
spaces = if spaces > 0 then repeat ' ', spaces else ''
|
||
letPart = if rule.shortFlag then rule.shortFlag + ', ' else ' '
|
||
lines.push ' ' + letPart + rule.longFlag + spaces + rule.description
|
||
"\n#{ lines.join('\n') }\n"
|
||
|
||
# Helpers
|
||
# -------
|
||
|
||
# Regex matchers for option flags on the command line and their rules.
|
||
LONG_FLAG = /^(--\w[\w\-]*)/
|
||
SHORT_FLAG = /^(-\w)$/
|
||
MULTI_FLAG = /^-(\w{2,})/
|
||
# Matches the long flag part of a rule for an option with an argument. Not
|
||
# applied to anything in process.argv.
|
||
OPTIONAL = /\[(\w+(\*?))\]/
|
||
|
||
# Build and return the list of option rules. If the optional *short-flag* is
|
||
# unspecified, leave it out by padding with `null`.
|
||
buildRules = (ruleDeclarations) ->
|
||
ruleList = for tuple in ruleDeclarations
|
||
tuple.unshift null if tuple.length < 3
|
||
buildRule tuple...
|
||
flagDict = {}
|
||
for rule in ruleList
|
||
# `shortFlag` is null if not provided in the rule.
|
||
for flag in [rule.shortFlag, rule.longFlag] when flag?
|
||
if flagDict[flag]?
|
||
throw new Error "flag #{flag} for switch #{rule.name}
|
||
was already declared for switch #{flagDict[flag].name}"
|
||
flagDict[flag] = rule
|
||
|
||
{ruleList, flagDict}
|
||
|
||
# Build a rule from a `-o` short flag, a `--output [DIR]` long flag, and the
|
||
# description of what the option does.
|
||
buildRule = (shortFlag, longFlag, description) ->
|
||
match = longFlag.match(OPTIONAL)
|
||
shortFlag = shortFlag?.match(SHORT_FLAG)[1]
|
||
longFlag = longFlag.match(LONG_FLAG)[1]
|
||
{
|
||
name: longFlag.replace /^--/, ''
|
||
shortFlag: shortFlag
|
||
longFlag: longFlag
|
||
description: description
|
||
hasArgument: !!(match and match[1])
|
||
isList: !!(match and match[2])
|
||
}
|
||
|
||
normalizeArguments = (args, flagDict) ->
|
||
rules = []
|
||
positional = []
|
||
needsArgOpt = null
|
||
for arg, argIndex in args
|
||
# If the previous argument given to the script was an option that uses the
|
||
# next command-line argument as its argument, create copy of the option’s
|
||
# rule with an `argument` field.
|
||
if needsArgOpt?
|
||
withArg = Object.assign {}, needsArgOpt.rule, {argument: arg}
|
||
rules.push withArg
|
||
needsArgOpt = null
|
||
continue
|
||
|
||
multiFlags = arg.match(MULTI_FLAG)?[1]
|
||
.split('')
|
||
.map (flagName) -> "-#{flagName}"
|
||
if multiFlags?
|
||
multiOpts = multiFlags.map (flag) ->
|
||
rule = flagDict[flag]
|
||
unless rule?
|
||
throw new Error "unrecognized option #{flag} in multi-flag #{arg}"
|
||
{rule, flag}
|
||
# Only the last flag in a multi-flag may have an argument.
|
||
[innerOpts..., lastOpt] = multiOpts
|
||
for {rule, flag} in innerOpts
|
||
if rule.hasArgument
|
||
throw new Error "cannot use option #{flag} in multi-flag #{arg} except
|
||
as the last option, because it needs an argument"
|
||
rules.push rule
|
||
if lastOpt.rule.hasArgument
|
||
needsArgOpt = lastOpt
|
||
else
|
||
rules.push lastOpt.rule
|
||
else if ([LONG_FLAG, SHORT_FLAG].some (pat) -> arg.match(pat)?)
|
||
singleRule = flagDict[arg]
|
||
unless singleRule?
|
||
throw new Error "unrecognized option #{arg}"
|
||
if singleRule.hasArgument
|
||
needsArgOpt = {rule: singleRule, flag: arg}
|
||
else
|
||
rules.push singleRule
|
||
else
|
||
# This is a positional argument.
|
||
positional = args[argIndex..]
|
||
break
|
||
|
||
if needsArgOpt?
|
||
throw new Error "value required for #{needsArgOpt.flag}, but it was the last
|
||
argument provided"
|
||
{rules, positional}
|