mirror of
https://github.com/jashkenas/coffeescript.git
synced 2026-05-03 03:00:14 -04:00
[CS2] Add #! support for executable scripts on Linux. (#3946)
* 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
This commit is contained in:
committed by
Geoffrey Booth
parent
d287a798cc
commit
4e57ca6833
@@ -73,6 +73,8 @@ exports.compile = compile = withPrettyErrors (code, options) ->
|
||||
generateSourceMap = options.sourceMap or options.inlineMap or not options.filename?
|
||||
filename = options.filename or '<anonymous>'
|
||||
|
||||
checkShebangLine filename, code
|
||||
|
||||
sources[filename] = code
|
||||
map = new SourceMap if generateSourceMap
|
||||
|
||||
@@ -295,3 +297,16 @@ Error.prepareStackTrace = (err, stack) ->
|
||||
" at #{formatSourcePosition frame, getSourceMapping}"
|
||||
|
||||
"#{err.toString()}\n#{frames.join '\n'}\n"
|
||||
|
||||
checkShebangLine = (file, input) ->
|
||||
firstLine = input.split(/$/m)[0]
|
||||
rest = firstLine?.match(/^#!\s*([^\s]+\s*)(.*)/)
|
||||
args = rest?[2]?.split(/\s/).filter (s) -> s isnt ''
|
||||
if args?.length > 1
|
||||
console.error '''
|
||||
The script to be run begins with a shebang line with more than one
|
||||
argument. This script will fail on platforms such as Linux which only
|
||||
allow a single argument.
|
||||
'''
|
||||
console.error "The shebang line was: '#{firstLine}' in file '#{file}'"
|
||||
console.error "The arguments were: #{JSON.stringify args}"
|
||||
|
||||
@@ -25,7 +25,7 @@ hidden = (file) -> /^\.|~$/.test file
|
||||
|
||||
# The help banner that is printed in conjunction with `-h`/`--help`.
|
||||
BANNER = '''
|
||||
Usage: coffee [options] path/to/script.coffee -- [args]
|
||||
Usage: coffee [options] path/to/script.coffee [args]
|
||||
|
||||
If called without options, `coffee` will run your script.
|
||||
'''
|
||||
@@ -69,7 +69,22 @@ exports.buildCSOptionParser = buildCSOptionParser = ->
|
||||
# `--` will be passed verbatim to your script as arguments in `process.argv`
|
||||
exports.run = ->
|
||||
optionParser = buildCSOptionParser()
|
||||
parseOptions()
|
||||
try parseOptions()
|
||||
catch err
|
||||
console.error "option parsing error: #{err.message}"
|
||||
process.exit 1
|
||||
|
||||
if (not opts.doubleDashed) and (opts.arguments[1] is '--')
|
||||
printWarn '''
|
||||
coffee was invoked with '--' as the second positional argument, which is
|
||||
now deprecated. To pass '--' as an argument to a script to run, put an
|
||||
additional '--' before the path to your script.
|
||||
|
||||
'--' will be removed from the argument list.
|
||||
'''
|
||||
printWarn "The positional arguments were: #{JSON.stringify opts.arguments}"
|
||||
opts.arguments = [opts.arguments[0]].concat opts.arguments[2..]
|
||||
|
||||
# Make the REPL *CLI* use the global context so as to (a) be consistent with the
|
||||
# `node` REPL CLI and, therefore, (b) make packages that modify native prototypes
|
||||
# (such as 'colors' and 'sugar') work as expected.
|
||||
|
||||
@@ -18,8 +18,8 @@ exports.OptionParser = class OptionParser
|
||||
# [short-flag, long-flag, description]
|
||||
#
|
||||
# Along with an optional banner for the usage help.
|
||||
constructor: (rules, @banner) ->
|
||||
@rules = buildRules rules
|
||||
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
|
||||
@@ -28,36 +28,33 @@ exports.OptionParser = class OptionParser
|
||||
# parsers that allow you to attach callback actions for every flag. Instead,
|
||||
# you're responsible for interpreting the options object.
|
||||
parse: (args) ->
|
||||
options = arguments: []
|
||||
skippingArgument = no
|
||||
originalArgs = args
|
||||
args = normalizeArguments args
|
||||
for arg, i in args
|
||||
if skippingArgument
|
||||
skippingArgument = no
|
||||
continue
|
||||
if arg is '--'
|
||||
pos = originalArgs.indexOf '--'
|
||||
options.arguments = options.arguments.concat originalArgs[(pos + 1)..]
|
||||
break
|
||||
isOption = !!(arg.match(LONG_FLAG) or arg.match(SHORT_FLAG))
|
||||
# the CS option parser is a little odd; options after the first
|
||||
# non-option argument are treated as non-option arguments themselves
|
||||
seenNonOptionArg = options.arguments.length > 0
|
||||
unless seenNonOptionArg
|
||||
matchedRule = no
|
||||
for rule in @rules
|
||||
if rule.shortFlag is arg or rule.longFlag is arg
|
||||
value = true
|
||||
if rule.hasArgument
|
||||
skippingArgument = yes
|
||||
value = args[i + 1]
|
||||
options[rule.name] = if rule.isList then (options[rule.name] or []).concat value else value
|
||||
matchedRule = yes
|
||||
break
|
||||
throw new Error "unrecognized option: #{arg}" if isOption and not matchedRule
|
||||
if seenNonOptionArg or not isOption
|
||||
options.arguments.push arg
|
||||
# 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
|
||||
@@ -65,7 +62,7 @@ exports.OptionParser = class OptionParser
|
||||
help: ->
|
||||
lines = []
|
||||
lines.unshift "#{@banner}\n" if @banner
|
||||
for rule in @rules
|
||||
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 ' '
|
||||
@@ -75,26 +72,39 @@ exports.OptionParser = class OptionParser
|
||||
# Helpers
|
||||
# -------
|
||||
|
||||
# Regex matchers for option flags.
|
||||
# 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 = (rules) ->
|
||||
for tuple in rules
|
||||
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, options = {}) ->
|
||||
buildRule = (shortFlag, longFlag, description) ->
|
||||
match = longFlag.match(OPTIONAL)
|
||||
shortFlag = shortFlag?.match(SHORT_FLAG)[1]
|
||||
longFlag = longFlag.match(LONG_FLAG)[1]
|
||||
{
|
||||
name: longFlag.substr 2
|
||||
name: longFlag.replace /^--/, ''
|
||||
shortFlag: shortFlag
|
||||
longFlag: longFlag
|
||||
description: description
|
||||
@@ -102,14 +112,54 @@ buildRule = (shortFlag, longFlag, description, options = {}) ->
|
||||
isList: !!(match and match[2])
|
||||
}
|
||||
|
||||
# Normalize arguments by expanding merged flags into multiple flags. This allows
|
||||
# you to have `-wl` be the same as `--watch --lint`.
|
||||
normalizeArguments = (args) ->
|
||||
args = args[..]
|
||||
result = []
|
||||
for arg in args
|
||||
if match = arg.match MULTI_FLAG
|
||||
result.push '-' + l for l in match[1].split ''
|
||||
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
|
||||
result.push arg
|
||||
result
|
||||
# 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}
|
||||
|
||||
Reference in New Issue
Block a user