/src/optparse.coffee
CoffeeScript | 165 lines | 102 code | 15 blank | 48 comment | 27 complexity | 3392d54189a9c8b38a0bbd1e9ce78877 MD5 | raw file
1{repeat} = require './helpers' 2 3# A simple **OptionParser** class to parse option flags from the command-line. 4# Use it like so: 5# 6# parser = new OptionParser switches, helpBanner 7# options = parser.parse process.argv 8# 9# The first non-option is considered to be the start of the file (and file 10# option) list, and all subsequent arguments are left unparsed. 11# 12# The `coffee` command uses an instance of **OptionParser** to parse its 13# command-line arguments in `src/command.coffee`. 14exports.OptionParser = class OptionParser 15 16 # Initialize with a list of valid options, in the form: 17 # 18 # [short-flag, long-flag, description] 19 # 20 # Along with an optional banner for the usage help. 21 constructor: (ruleDeclarations, @banner) -> 22 @rules = buildRules ruleDeclarations 23 24 # Parse the list of arguments, populating an `options` object with all of the 25 # specified options, and return it. Options after the first non-option 26 # argument are treated as arguments. `options.arguments` will be an array 27 # containing the remaining arguments. This is a simpler API than many option 28 # parsers that allow you to attach callback actions for every flag. Instead, 29 # you're responsible for interpreting the options object. 30 parse: (args) -> 31 # The CoffeeScript option parser is a little odd; options after the first 32 # non-option argument are treated as non-option arguments themselves. 33 # Optional arguments are normalized by expanding merged flags into multiple 34 # flags. This allows you to have `-wl` be the same as `--watch --lint`. 35 # Note that executable scripts with a shebang (`#!`) line should use the 36 # line `#!/usr/bin/env coffee`, or `#!/absolute/path/to/coffee`, without a 37 # `--` argument after, because that will fail on Linux (see #3946). 38 {rules, positional} = normalizeArguments args, @rules.flagDict 39 options = {} 40 41 # The `argument` field is added to the rule instance non-destructively by 42 # `normalizeArguments`. 43 for {hasArgument, argument, isList, name} in rules 44 if hasArgument 45 if isList 46 options[name] ?= [] 47 options[name].push argument 48 else 49 options[name] = argument 50 else 51 options[name] = true 52 53 if positional[0] is '--' 54 options.doubleDashed = yes 55 positional = positional[1..] 56 57 options.arguments = positional 58 options 59 60 # Return the help text for this **OptionParser**, listing and describing all 61 # of the valid options, for `--help` and such. 62 help: -> 63 lines = [] 64 lines.unshift "#{@banner}\n" if @banner 65 for rule in @rules.ruleList 66 spaces = 15 - rule.longFlag.length 67 spaces = if spaces > 0 then repeat ' ', spaces else '' 68 letPart = if rule.shortFlag then rule.shortFlag + ', ' else ' ' 69 lines.push ' ' + letPart + rule.longFlag + spaces + rule.description 70 "\n#{ lines.join('\n') }\n" 71 72# Helpers 73# ------- 74 75# Regex matchers for option flags on the command line and their rules. 76LONG_FLAG = /^(--\w[\w\-]*)/ 77SHORT_FLAG = /^(-\w)$/ 78MULTI_FLAG = /^-(\w{2,})/ 79# Matches the long flag part of a rule for an option with an argument. Not 80# applied to anything in process.argv. 81OPTIONAL = /\[(\w+(\*?))\]/ 82 83# Build and return the list of option rules. If the optional *short-flag* is 84# unspecified, leave it out by padding with `null`. 85buildRules = (ruleDeclarations) -> 86 ruleList = for tuple in ruleDeclarations 87 tuple.unshift null if tuple.length < 3 88 buildRule tuple... 89 flagDict = {} 90 for rule in ruleList 91 # `shortFlag` is null if not provided in the rule. 92 for flag in [rule.shortFlag, rule.longFlag] when flag? 93 if flagDict[flag]? 94 throw new Error "flag #{flag} for switch #{rule.name} 95 was already declared for switch #{flagDict[flag].name}" 96 flagDict[flag] = rule 97 98 {ruleList, flagDict} 99 100# Build a rule from a `-o` short flag, a `--output [DIR]` long flag, and the 101# description of what the option does. 102buildRule = (shortFlag, longFlag, description) -> 103 match = longFlag.match(OPTIONAL) 104 shortFlag = shortFlag?.match(SHORT_FLAG)[1] 105 longFlag = longFlag.match(LONG_FLAG)[1] 106 { 107 name: longFlag.replace /^--/, '' 108 shortFlag: shortFlag 109 longFlag: longFlag 110 description: description 111 hasArgument: !!(match and match[1]) 112 isList: !!(match and match[2]) 113 } 114 115normalizeArguments = (args, flagDict) -> 116 rules = [] 117 positional = [] 118 needsArgOpt = null 119 for arg, argIndex in args 120 # If the previous argument given to the script was an option that uses the 121 # next command-line argument as its argument, create copy of the option’s 122 # rule with an `argument` field. 123 if needsArgOpt? 124 withArg = Object.assign {}, needsArgOpt.rule, {argument: arg} 125 rules.push withArg 126 needsArgOpt = null 127 continue 128 129 multiFlags = arg.match(MULTI_FLAG)?[1] 130 .split('') 131 .map (flagName) -> "-#{flagName}" 132 if multiFlags? 133 multiOpts = multiFlags.map (flag) -> 134 rule = flagDict[flag] 135 unless rule? 136 throw new Error "unrecognized option #{flag} in multi-flag #{arg}" 137 {rule, flag} 138 # Only the last flag in a multi-flag may have an argument. 139 [innerOpts..., lastOpt] = multiOpts 140 for {rule, flag} in innerOpts 141 if rule.hasArgument 142 throw new Error "cannot use option #{flag} in multi-flag #{arg} except 143 as the last option, because it needs an argument" 144 rules.push rule 145 if lastOpt.rule.hasArgument 146 needsArgOpt = lastOpt 147 else 148 rules.push lastOpt.rule 149 else if ([LONG_FLAG, SHORT_FLAG].some (pat) -> arg.match(pat)?) 150 singleRule = flagDict[arg] 151 unless singleRule? 152 throw new Error "unrecognized option #{arg}" 153 if singleRule.hasArgument 154 needsArgOpt = {rule: singleRule, flag: arg} 155 else 156 rules.push singleRule 157 else 158 # This is a positional argument. 159 positional = args[argIndex..] 160 break 161 162 if needsArgOpt? 163 throw new Error "value required for #{needsArgOpt.flag}, but it was the last 164 argument provided" 165 {rules, positional}