[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:
Danny McClanahan
2017-07-19 18:25:06 -05:00
committed by Geoffrey Booth
parent d287a798cc
commit 4e57ca6833
22 changed files with 672 additions and 213 deletions

View File

@@ -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}"

View File

@@ -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.

View File

@@ -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 options
# 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}