forked from jashkenas/coffeescript
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoptparse.coffee
More file actions
165 lines (150 loc) · 5.98 KB
/
optparse.coffee
File metadata and controls
165 lines (150 loc) · 5.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
{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}