diff --git a/docs/v2/index.html b/docs/v2/index.html index 5ddc172cbc..ac291e2b49 100644 --- a/docs/v2/index.html +++ b/docs/v2/index.html @@ -306,7 +306,7 @@ white-space: nowrap; } -h2, h3 { +h2, h3, h4 { margin-top: 1.3em; margin-bottom: 0.6em; font-family: 'Alegreya Sans'; @@ -314,7 +314,7 @@ h2 { font-weight: 800; } -h3, h2 time { +h3, h4, h2 time { font-weight: 400; } @@ -734,6 +734,9 @@
#!
Lines
+ The biggest change in CoffeeScript 2 is that now the CoffeeScript compiler produces modern, ES2015+ JavaScript. A CoffeeScript =>
becomes an ES =>
, a CoffeeScript class
becomes an ES class
and so on. With the exception of modules (import
and export
statements), all the ES2015+ features that CoffeeScript supports can run natively in Node 7.6+, meaning that Node can run CoffeeScript’s output without any further processing required. You can run the tests in your browser to see if your browser can do the same; Chrome has supported all features since version 55.
The biggest change in CoffeeScript 2 is that now the CoffeeScript compiler produces modern, ES2015+ JavaScript. A CoffeeScript =>
becomes an ES =>
, a CoffeeScript class
becomes an ES class
and so on. With the exception of modules (import
and export
statements) and JSX, all the ES2015+ features that CoffeeScript supports can run natively in Node 7.6+, meaning that Node can run CoffeeScript’s output without any further processing required. You can run the tests in your browser to see if your browser can do the same; Chrome has supported all features since version 55.
Support for ES2015+ syntax is important to ensure compatibility with frameworks that assume ES2015. Now that CoffeeScript compiles classes to the ES class
keyword, it’s possible to extend
an ES class; that wasn’t possible in CoffeeScript 1. Parity in how language features work is also important on its own; CoffeeScript “is just JavaScript,” and so things like function parameter default values should behave the same in CoffeeScript as in JavaScript.
Many ES2015+ features have been backported to CoffeeScript 1.11 and 1.12, including modules, for…of
, and tagged template literals. Major new features unique to CoffeeScript 2 are support for ES2017’s async functions and for JSX. More details are in the changelog.
There are very few breaking changes from CoffeeScript 1.x to 2; we hope the upgrade process is smooth for most projects.
@@ -3244,7 +3247,7 @@Older plugins or forks of CoffeeScript supported JSX syntax and referred to it as CSX or CJSX. They also often used a .cjsx
file extension, but this is no longer necessary; regalar .coffee
will do.
Older plugins or forks of CoffeeScript supported JSX syntax and referred to it as CSX or CJSX. They also often used a .cjsx
file extension, but this is no longer necessary; regular .coffee
will do.
-class B extends A constructor: -> this # Throws a compiler error
ES2015 classes don’t allow bound (fat arrow) methods. The CoffeeScript compiler goes through some contortions to preserve support for them, but one thing that can’t be accomodated is calling a bound method before it is bound:
+ES2015 classes don’t allow bound (fat arrow) methods. The CoffeeScript compiler goes through some contortions to preserve support for them, but one thing that can’t be accommodated is calling a bound method before it is bound:
class Base constructor: -> @onClick() # This works @@ -3787,7 +3790,7 @@
super
andextends
- @@ -3797,6 +3800,29 @@JSX and the
+<
and>
OperatorsJSX and the
<
and>
operatorsWith the addition of JSX, the
<
and>
characters serve as both the “less than” and “greater than” operators and as the delimiters for XML tags, like<div>
. For best results, in general you should always wrap the operators in spaces to distinguish them from XML tags:i < len
, noti<len
. The compiler tries to be forgiving when it can be sure what you intend, but always putting spaces around the “less than” and “greater than” operators will remove ambiguity.Literate CoffeeScript parsing
Code blocks should also now maintain a consistent indentation level—so an indentation of one tab (or whatever you consider to be a tab stop, like 2 spaces or 4 spaces) should be treated as your code’s “left margin,” with all code in the file relative to that column.
Code blocks that you want to be part of the commentary, and not executed, must have at least one line (ideally the first line of the block) completely unindented.
+ ++ Argument parsing and shebang (
+#!
) linesIn CoffeeScript 1.x,
+--
was required after the path and filename of the script to be run, but before any arguments passed to that script. This convention is now deprecated. So instead of:+coffee [options] path/to/script.coffee -- [args] +
Now you would just type:
++coffee [options] path/to/script.coffee [args] +
The deprecated version will still work, but it will print a warning before running the script.
+On non-Windows platforms, a
+.coffee
file can be made executable by adding a shebang (#!
) line at the top of the file and marking the file as executable. For example:+#!/usr/bin/env coffee + +x = 2 + 2 +console.log x +
If this were saved as
+executable.coffee
, it could be made executable and run:+▶ chmod +x ./executable.coffee +▶ ./executable.coffee +4 +
In CoffeeScript 1.x, this used to fail when trying to pass arguments to the script. Some users on OS X worked around the problem by using
+#!/usr/bin/env coffee --
as the first line of the file. That didn’t work on Linux, however, which cannot parse shebang lines with more than a single argument. While such scripts will still run on OS X, CoffeeScript will now display a warning before compiling or evaluating files that begin with a too-long shebang line. Now that CoffeeScript 2 supports passing arguments without needing--
, we recommend simply changing the shebang lines in such scripts to just#!/usr/bin/env coffee
.diff --git a/documentation/sections/breaking_changes_argument_parsing_and_shebang_lines.md b/documentation/sections/breaking_changes_argument_parsing_and_shebang_lines.md new file mode 100644 index 0000000000..c18693f211 --- /dev/null +++ b/documentation/sections/breaking_changes_argument_parsing_and_shebang_lines.md @@ -0,0 +1,34 @@ +### Argument parsing and shebang (`#!`) lines + +In CoffeeScript 1.x, `--` was required after the path and filename of the script to be run, but before any arguments passed to that script. This convention is now deprecated. So instead of: + +```bash +coffee [options] path/to/script.coffee -- [args] +``` + +Now you would just type: + +```bash +coffee [options] path/to/script.coffee [args] +``` + +The deprecated version will still work, but it will print a warning before running the script. + +On non-Windows platforms, a `.coffee` file can be made executable by adding a shebang (`#!`) line at the top of the file and marking the file as executable. For example: + +```coffee +#!/usr/bin/env coffee + +x = 2 + 2 +console.log x +``` + +If this were saved as `executable.coffee`, it could be made executable and run: + +```bash +▶ chmod +x ./executable.coffee +▶ ./executable.coffee +4 +``` + +In CoffeeScript 1.x, this used to fail when trying to pass arguments to the script. Some users on OS X worked around the problem by using `#!/usr/bin/env coffee --` as the first line of the file. That didn’t work on Linux, however, which cannot parse shebang lines with more than a single argument. While such scripts will still run on OS X, CoffeeScript will now display a warning before compiling or evaluating files that begin with a too-long shebang line. Now that CoffeeScript 2 supports passing arguments without needing `--`, we recommend simply changing the shebang lines in such scripts to just `#!/usr/bin/env coffee`. \ No newline at end of file diff --git a/documentation/sections/breaking_changes_jsx_and_the_less_than_and_greater_than_operators.md b/documentation/sections/breaking_changes_jsx_and_the_less_than_and_greater_than_operators.md index a6c8843377..57c581d098 100644 --- a/documentation/sections/breaking_changes_jsx_and_the_less_than_and_greater_than_operators.md +++ b/documentation/sections/breaking_changes_jsx_and_the_less_than_and_greater_than_operators.md @@ -1,3 +1,3 @@ -### JSX and the `<` and `>` Operators +### JSX and the `<` and `>` operators With the addition of [JSX](#jsx), the `<` and `>` characters serve as both the “less than” and “greater than” operators and as the delimiters for XML tags, like ` `. For best results, in general you should always wrap the operators in spaces to distinguish them from XML tags: `i < len`, not `i` becomes an ES `=>`, a CoffeeScript `class` becomes an ES `class` and so on. With the exception of modules (`import` and `export` statements), all the ES2015+ features that CoffeeScript supports can run natively in Node 7.6+, meaning that Node can run CoffeeScript’s output without any further processing required. You can [run the tests in your browser](http://coffeescript.org/v<%= majorVersion %>/test.html) to see if your browser can do the same; Chrome has supported all features since version 55. +The biggest change in CoffeeScript 2 is that now the CoffeeScript compiler produces modern, ES2015+ JavaScript. A CoffeeScript `=>` becomes an ES `=>`, a CoffeeScript `class` becomes an ES `class` and so on. With the exception of [modules](#modules) (`import` and `export` statements) and [JSX](#jsx), all the ES2015+ features that CoffeeScript supports can run natively in Node 7.6+, meaning that Node can run CoffeeScript’s output without any further processing required. You can [run the tests in your browser](http://coffeescript.org/v<%= majorVersion %>/test.html) to see if your browser can do the same; Chrome has supported all features since version 55. Support for ES2015+ syntax is important to ensure compatibility with frameworks that assume ES2015. Now that CoffeeScript compiles classes to the ES `class` keyword, it’s possible to `extend` an ES class; that wasn’t possible in CoffeeScript 1. Parity in how language features work is also important on its own; CoffeeScript “is just JavaScript,” and so things like [function parameter default values](#breaking-changes-default-values) should behave the same in CoffeeScript as in JavaScript. diff --git a/documentation/v2/body.html b/documentation/v2/body.html index 0af708fbaf..9af6bfee1c 100644 --- a/documentation/v2/body.html +++ b/documentation/v2/body.html @@ -178,6 +178,9 @@ <%= htmlFor('breaking_changes_literate_coffeescript') %> ++ <%= htmlFor('breaking_changes_argument_parsing_and_shebang_lines') %> + <%= htmlFor('changelog') %> diff --git a/documentation/v2/docs.css b/documentation/v2/docs.css index be35485ed5..908a4a2cb7 100644 --- a/documentation/v2/docs.css +++ b/documentation/v2/docs.css @@ -286,7 +286,7 @@ td code { white-space: nowrap; } -h2, h3 { +h2, h3, h4 { margin-top: 1.3em; margin-bottom: 0.6em; font-family: 'Alegreya Sans'; @@ -294,7 +294,7 @@ h2, h3 { h2 { font-weight: 800; } -h3, h2 time { +h3, h4, h2 time { font-weight: 400; } diff --git a/documentation/v2/sidebar.html b/documentation/v2/sidebar.html index 60ca1ba0b2..cdbc02cad8 100644 --- a/documentation/v2/sidebar.html +++ b/documentation/v2/sidebar.html @@ -171,6 +171,9 @@ Literate CoffeeScript Parsing ++ Argument Parsing and #!
Lines +diff --git a/lib/coffeescript/coffeescript.js b/lib/coffeescript/coffeescript.js index 203af19d1e..97a0df3d35 100644 --- a/lib/coffeescript/coffeescript.js +++ b/lib/coffeescript/coffeescript.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 2.0.0-beta3 (function() { - var Lexer, SourceMap, base64encode, compile, formatSourcePosition, getSourceMap, helpers, lexer, packageJson, parser, sourceMaps, sources, withPrettyErrors; + var Lexer, SourceMap, base64encode, checkShebangLine, compile, formatSourcePosition, getSourceMap, helpers, lexer, packageJson, parser, sourceMaps, sources, withPrettyErrors; ({Lexer} = require('./lexer')); @@ -56,6 +56,7 @@ options = extend({}, options); generateSourceMap = options.sourceMap || options.inlineMap || (options.filename == null); filename = options.filename || ' '; + checkShebangLine(filename, code); sources[filename] = code; if (generateSourceMap) { map = new SourceMap; @@ -290,4 +291,18 @@ return `${err.toString()}\n${frames.join('\n')}\n`; }; + checkShebangLine = function(file, input) { + var args, firstLine, ref, rest; + firstLine = input.split(/$/m)[0]; + rest = firstLine != null ? firstLine.match(/^#!\s*([^\s]+\s*)(.*)/) : void 0; + args = rest != null ? (ref = rest[2]) != null ? ref.split(/\s/).filter(function(s) { + return s !== ''; + }) : void 0 : void 0; + if ((args != null ? args.length : void 0) > 1) { + console.error('The script to be run begins with a shebang line with more than one\nargument. This script will fail on platforms such as Linux which only\nallow a single argument.'); + console.error(`The shebang line was: '${firstLine}' in file '${file}'`); + return console.error(`The arguments were: ${JSON.stringify(args)}`); + } + }; + }).call(this); diff --git a/lib/coffeescript/command.js b/lib/coffeescript/command.js index 510c46d7ce..3f964d51cf 100644 --- a/lib/coffeescript/command.js +++ b/lib/coffeescript/command.js @@ -33,7 +33,7 @@ return /^\.|~$/.test(file); }; - BANNER = 'Usage: coffee [options] path/to/script.coffee -- [args]\n\nIf called without options, `coffee` will run your script.'; + BANNER = 'Usage: coffee [options] path/to/script.coffee [args]\n\nIf called without options, `coffee` will run your script.'; SWITCHES = [['-b', '--bare', 'compile without a top-level function wrapper'], ['-c', '--compile', 'compile to JavaScript and save as .js files'], ['-e', '--eval', 'pass a string from the command line as input'], ['-h', '--help', 'display this help message'], ['-i', '--interactive', 'run an interactive CoffeeScript REPL'], ['-j', '--join [FILE]', 'concatenate the source CoffeeScript before compiling'], ['-m', '--map', 'generate source map and save as .js.map files'], ['-M', '--inline-map', 'generate source map and include it directly in output'], ['-n', '--nodes', 'print out the parse tree that the parser produces'], ['--nodejs [ARGS]', 'pass options directly to the "node" binary'], ['--no-header', 'suppress the "Generated by" header'], ['-o', '--output [DIR]', 'set the output directory for compiled JavaScript'], ['-p', '--print', 'print out the compiled JavaScript'], ['-r', '--require [MODULE*]', 'require the given module before eval or REPL'], ['-s', '--stdio', 'listen for and compile scripts over stdio'], ['-l', '--literate', 'treat stdio as literate style coffeescript'], ['-t', '--tokens', 'print out the tokens that the lexer/rewriter produce'], ['-v', '--version', 'display the version number'], ['-w', '--watch', 'watch scripts for changes and rerun commands']]; @@ -54,9 +54,20 @@ }; exports.run = function() { - var i, len, literals, ref, replCliOpts, results, source; + var err, i, len, literals, ref, replCliOpts, results, source; optionParser = buildCSOptionParser(); - parseOptions(); + try { + parseOptions(); + } catch (error) { + err = error; + console.error(`option parsing error: ${err.message}`); + process.exit(1); + } + if ((!opts.doubleDashed) && (opts.arguments[1] === '--')) { + printWarn('coffee was invoked with \'--\' as the second positional argument, which is\nnow deprecated. To pass \'--\' as an argument to a script to run, put an\nadditional \'--\' before the path to your script.\n\n\'--\' will be removed from the argument list.'); + printWarn(`The positional arguments were: ${JSON.stringify(opts.arguments)}`); + opts.arguments = [opts.arguments[0]].concat(opts.arguments.slice(2)); + } replCliOpts = { useGlobal: true }; diff --git a/lib/coffeescript/optparse.js b/lib/coffeescript/optparse.js index c5c8b4b336..01986542b5 100644 --- a/lib/coffeescript/optparse.js +++ b/lib/coffeescript/optparse.js @@ -1,72 +1,52 @@ // Generated by CoffeeScript 2.0.0-beta3 (function() { - var LONG_FLAG, MULTI_FLAG, OPTIONAL, OptionParser, SHORT_FLAG, buildRule, buildRules, normalizeArguments, repeat; + var LONG_FLAG, MULTI_FLAG, OPTIONAL, OptionParser, SHORT_FLAG, buildRule, buildRules, normalizeArguments, repeat, + slice = [].slice; ({repeat} = require('./helpers')); exports.OptionParser = OptionParser = class OptionParser { - constructor(rules, banner) { + constructor(ruleDeclarations, banner) { this.banner = banner; - this.rules = buildRules(rules); + this.rules = buildRules(ruleDeclarations); } parse(args) { - var arg, i, isOption, j, k, len, len1, matchedRule, options, originalArgs, pos, ref, rule, seenNonOptionArg, skippingArgument, value; - options = { - arguments: [] - }; - skippingArgument = false; - originalArgs = args; - args = normalizeArguments(args); - for (i = j = 0, len = args.length; j < len; i = ++j) { - arg = args[i]; - if (skippingArgument) { - skippingArgument = false; - continue; - } - if (arg === '--') { - pos = originalArgs.indexOf('--'); - options.arguments = options.arguments.concat(originalArgs.slice(pos + 1)); - break; - } - isOption = !!(arg.match(LONG_FLAG) || arg.match(SHORT_FLAG)); - seenNonOptionArg = options.arguments.length > 0; - if (!seenNonOptionArg) { - matchedRule = false; - ref = this.rules; - for (k = 0, len1 = ref.length; k < len1; k++) { - rule = ref[k]; - if (rule.shortFlag === arg || rule.longFlag === arg) { - value = true; - if (rule.hasArgument) { - skippingArgument = true; - value = args[i + 1]; - } - options[rule.name] = rule.isList ? (options[rule.name] || []).concat(value) : value; - matchedRule = true; - break; + var argument, hasArgument, i, isList, len, name, options, positional, rules; + ({rules, positional} = normalizeArguments(args, this.rules.flagDict)); + options = {}; + for (i = 0, len = rules.length; i < len; i++) { + ({hasArgument, argument, isList, name} = rules[i]); + if (hasArgument) { + if (isList) { + if (options[name] == null) { + options[name] = []; } + options[name].push(argument); + } else { + options[name] = argument; } - if (isOption && !matchedRule) { - throw new Error(`unrecognized option: ${arg}`); - } - } - if (seenNonOptionArg || !isOption) { - options.arguments.push(arg); + } else { + options[name] = true; } } + if (positional[0] === '--') { + options.doubleDashed = true; + positional = positional.slice(1); + } + options.arguments = positional; return options; } help() { - var j, len, letPart, lines, ref, rule, spaces; + var i, len, letPart, lines, ref, rule, spaces; lines = []; if (this.banner) { lines.unshift(`${this.banner}\n`); } - ref = this.rules; - for (j = 0, len = ref.length; j < len; j++) { - rule = ref[j]; + ref = this.rules.ruleList; + for (i = 0, len = ref.length; i < len; i++) { + rule = ref[i]; spaces = 15 - rule.longFlag.length; spaces = spaces > 0 ? repeat(' ', spaces) : ''; letPart = rule.shortFlag ? rule.shortFlag + ', ' : ' '; @@ -85,25 +65,45 @@ OPTIONAL = /\[(\w+(\*?))\]/; - buildRules = function(rules) { - var j, len, results, tuple; - results = []; - for (j = 0, len = rules.length; j < len; j++) { - tuple = rules[j]; - if (tuple.length < 3) { - tuple.unshift(null); + buildRules = function(ruleDeclarations) { + var flag, flagDict, i, j, len, len1, ref, rule, ruleList, tuple; + ruleList = (function() { + var i, len, results; + results = []; + for (i = 0, len = ruleDeclarations.length; i < len; i++) { + tuple = ruleDeclarations[i]; + if (tuple.length < 3) { + tuple.unshift(null); + } + results.push(buildRule(...tuple)); + } + return results; + })(); + flagDict = {}; + for (i = 0, len = ruleList.length; i < len; i++) { + rule = ruleList[i]; + ref = [rule.shortFlag, rule.longFlag]; + for (j = 0, len1 = ref.length; j < len1; j++) { + flag = ref[j]; + if (!(flag != null)) { + continue; + } + if (flagDict[flag] != null) { + throw new Error(`flag ${flag} for switch ${rule.name} was already declared for switch ${flagDict[flag].name}`); + } + flagDict[flag] = rule; } - results.push(buildRule(...tuple)); } - return results; + return {ruleList, flagDict}; }; - buildRule = function(shortFlag, longFlag, description, options = {}) { + buildRule = function(shortFlag, longFlag, description) { var match; match = longFlag.match(OPTIONAL); + shortFlag = shortFlag != null ? shortFlag.match(SHORT_FLAG)[1] : void 0; longFlag = longFlag.match(LONG_FLAG)[1]; return { - name: longFlag.substr(2), + name: longFlag.replace(/^--/, ''), shortFlag: shortFlag, longFlag: longFlag, description: description, @@ -112,23 +112,70 @@ }; }; - normalizeArguments = function(args) { - var arg, j, k, l, len, len1, match, ref, result; - args = args.slice(0); - result = []; - for (j = 0, len = args.length; j < len; j++) { - arg = args[j]; - if (match = arg.match(MULTI_FLAG)) { - ref = match[1].split(''); - for (k = 0, len1 = ref.length; k < len1; k++) { - l = ref[k]; - result.push('-' + l); + normalizeArguments = function(args, flagDict) { + var arg, argIndex, flag, i, innerOpts, j, k, lastOpt, len, len1, multiFlags, multiOpts, needsArgOpt, positional, ref, rule, rules, singleRule, withArg; + rules = []; + positional = []; + needsArgOpt = null; + for (argIndex = i = 0, len = args.length; i < len; argIndex = ++i) { + arg = args[argIndex]; + if (needsArgOpt != null) { + withArg = Object.assign({}, needsArgOpt.rule, { + argument: arg + }); + rules.push(withArg); + needsArgOpt = null; + continue; + } + multiFlags = (ref = arg.match(MULTI_FLAG)) != null ? ref[1].split('').map(function(flagName) { + return `-${flagName}`; + }) : void 0; + if (multiFlags != null) { + multiOpts = multiFlags.map(function(flag) { + var rule; + rule = flagDict[flag]; + if (rule == null) { + throw new Error(`unrecognized option ${flag} in multi-flag ${arg}`); + } + return {rule, flag}; + }); + innerOpts = 2 <= multiOpts.length ? slice.call(multiOpts, 0, j = multiOpts.length - 1) : (j = 0, []), lastOpt = multiOpts[j++]; + for (k = 0, len1 = innerOpts.length; k < len1; k++) { + ({rule, flag} = innerOpts[k]); + 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(function(pat) { + return arg.match(pat) != null; + })) { + singleRule = flagDict[arg]; + if (singleRule == null) { + throw new Error(`unrecognized option ${arg}`); + } + if (singleRule.hasArgument) { + needsArgOpt = { + rule: singleRule, + flag: arg + }; + } else { + rules.push(singleRule); } } else { - result.push(arg); + positional = args.slice(argIndex); + break; } } - return result; + if (needsArgOpt != null) { + throw new Error(`value required for ${needsArgOpt.flag}, but it was the last argument provided`); + } + return {rules, positional}; }; }).call(this); diff --git a/src/coffeescript.coffee b/src/coffeescript.coffee index 0a2a9a341b..bdc2b4ef04 100644 --- a/src/coffeescript.coffee +++ b/src/coffeescript.coffee @@ -73,6 +73,8 @@ exports.compile = compile = withPrettyErrors (code, options) -> generateSourceMap = options.sourceMap or options.inlineMap or not options.filename? filename = options.filename or ' ' + 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}" diff --git a/src/command.coffee b/src/command.coffee index ceff61b5a2..bfd89aadce 100644 --- a/src/command.coffee +++ b/src/command.coffee @@ -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. diff --git a/src/optparse.coffee b/src/optparse.coffee index b107b2f7c4..c9771a3e97 100644 --- a/src/optparse.coffee +++ b/src/optparse.coffee @@ -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} diff --git a/test/argument-parsing.coffee b/test/argument-parsing.coffee deleted file mode 100644 index 6ed05c0587..0000000000 --- a/test/argument-parsing.coffee +++ /dev/null @@ -1,75 +0,0 @@ -return unless require? - -{buildCSOptionParser} = require '../lib/coffeescript/command' - -optionParser = buildCSOptionParser() - -sameOptions = (opts1, opts2, msg) -> - ownKeys = Object.keys(opts1).sort() - otherKeys = Object.keys(opts2).sort() - arrayEq ownKeys, otherKeys, msg - for k in ownKeys - arrayEq opts1[k], opts2[k], msg - yes - -test "combined options are still split after initial file name", -> - argv = ['some-file.coffee', '-bc'] - parsed = optionParser.parse argv - expected = arguments: ['some-file.coffee', '-b', '-c'] - sameOptions parsed, expected - - argv = ['some-file.litcoffee', '-bc'] - parsed = optionParser.parse argv - expected = arguments: ['some-file.litcoffee', '-b', '-c'] - sameOptions parsed, expected - - argv = ['-c', 'some-file.coffee', '-bc'] - parsed = optionParser.parse argv - expected = - compile: yes - arguments: ['some-file.coffee', '-b', '-c'] - sameOptions parsed, expected - - argv = ['-bc', 'some-file.coffee', '-bc'] - parsed = optionParser.parse argv - expected = - bare: yes - compile: yes - arguments: ['some-file.coffee', '-b', '-c'] - sameOptions parsed, expected - -test "combined options are not split after a '--'", -> - argv = ['--', '-bc'] - parsed = optionParser.parse argv - expected = arguments: ['-bc'] - sameOptions parsed, expected - - argv = ['-bc', '--', '-bc'] - parsed = optionParser.parse argv - expected = - bare: yes - compile: yes - arguments: ['-bc'] - sameOptions parsed, expected - -test "options are not split after any '--'", -> - argv = ['--', '--', '-bc'] - parsed = optionParser.parse argv - expected = arguments: ['--', '-bc'] - sameOptions parsed, expected - - argv = ['--', 'some-file.coffee', '--', 'arg'] - parsed = optionParser.parse argv - expected = arguments: ['some-file.coffee', '--', 'arg'] - sameOptions parsed, expected - - argv = ['--', 'arg', 'some-file.coffee', '--', '-bc'] - parsed = optionParser.parse argv - expected = arguments: ['arg', 'some-file.coffee', '--', '-bc'] - sameOptions parsed, expected - -test "later '--' are removed", -> - argv = ['some-file.coffee', '--', '-bc'] - parsed = optionParser.parse argv - expected = arguments: ['some-file.coffee', '-bc'] - sameOptions parsed, expected diff --git a/test/argument_parsing.coffee b/test/argument_parsing.coffee new file mode 100644 index 0000000000..c82a3136e9 --- /dev/null +++ b/test/argument_parsing.coffee @@ -0,0 +1,142 @@ +return unless require? +{buildCSOptionParser} = require '../lib/coffeescript/command' + +optionParser = buildCSOptionParser() + +sameOptions = (opts1, opts2, msg) -> + ownKeys = Object.keys(opts1).sort() + otherKeys = Object.keys(opts2).sort() + arrayEq ownKeys, otherKeys, msg + for k in ownKeys + arrayEq opts1[k], opts2[k], msg + yes + +test "combined options are not split after initial file name", -> + argv = ['some-file.coffee', '-bc'] + parsed = optionParser.parse argv + expected = arguments: ['some-file.coffee', '-bc'] + sameOptions parsed, expected + + argv = ['some-file.litcoffee', '-bc'] + parsed = optionParser.parse argv + expected = arguments: ['some-file.litcoffee', '-bc'] + sameOptions parsed, expected + + argv = ['-c', 'some-file.coffee', '-bc'] + parsed = optionParser.parse argv + expected = + compile: yes + arguments: ['some-file.coffee', '-bc'] + sameOptions parsed, expected + + argv = ['-bc', 'some-file.coffee', '-bc'] + parsed = optionParser.parse argv + expected = + bare: yes + compile: yes + arguments: ['some-file.coffee', '-bc'] + sameOptions parsed, expected + +test "combined options are not split after a '--', which is discarded", -> + argv = ['--', '-bc'] + parsed = optionParser.parse argv + expected = + doubleDashed: yes + arguments: ['-bc'] + sameOptions parsed, expected + + argv = ['-bc', '--', '-bc'] + parsed = optionParser.parse argv + expected = + bare: yes + compile: yes + doubleDashed: yes + arguments: ['-bc'] + sameOptions parsed, expected + +test "options are not split after any '--'", -> + argv = ['--', '--', '-bc'] + parsed = optionParser.parse argv + expected = + doubleDashed: yes + arguments: ['--', '-bc'] + sameOptions parsed, expected + + argv = ['--', 'some-file.coffee', '--', 'arg'] + parsed = optionParser.parse argv + expected = + doubleDashed: yes + arguments: ['some-file.coffee', '--', 'arg'] + sameOptions parsed, expected + + argv = ['--', 'arg', 'some-file.coffee', '--', '-bc'] + parsed = optionParser.parse argv + expected = + doubleDashed: yes + arguments: ['arg', 'some-file.coffee', '--', '-bc'] + sameOptions parsed, expected + +test "any non-option argument stops argument parsing", -> + argv = ['arg', '-bc'] + parsed = optionParser.parse argv + expected = arguments: ['arg', '-bc'] + sameOptions parsed, expected + +test "later '--' are not removed", -> + argv = ['some-file.coffee', '--', '-bc'] + parsed = optionParser.parse argv + expected = arguments: ['some-file.coffee', '--', '-bc'] + sameOptions parsed, expected + +test "throw on invalid options", -> + argv = ['-k'] + throws -> optionParser.parse argv + + argv = ['-ck'] + throws (-> optionParser.parse argv), /multi-flag/ + + argv = ['-kc'] + throws (-> optionParser.parse argv), /multi-flag/ + + argv = ['-oc'] + throws (-> optionParser.parse argv), /needs an argument/ + + argv = ['-o'] + throws (-> optionParser.parse argv), /value required/ + + argv = ['-co'] + throws (-> optionParser.parse argv), /value required/ + + # Check if all flags in a multi-flag are recognized before checking if flags + # before the last need arguments. + argv = ['-ok'] + throws (-> optionParser.parse argv), /unrecognized option/ + +test "has expected help text", -> + ok optionParser.help() is ''' + +Usage: coffee [options] path/to/script.coffee [args] + +If called without options, `coffee` will run your script. + + -b, --bare compile without a top-level function wrapper + -c, --compile compile to JavaScript and save as .js files + -e, --eval pass a string from the command line as input + -h, --help display this help message + -i, --interactive run an interactive CoffeeScript REPL + -j, --join concatenate the source CoffeeScript before compiling + -m, --map generate source map and save as .js.map files + -M, --inline-map generate source map and include it directly in output + -n, --nodes print out the parse tree that the parser produces + --nodejs pass options directly to the "node" binary + --no-header suppress the "Generated by" header + -o, --output set the output directory for compiled JavaScript + -p, --print print out the compiled JavaScript + -r, --require require the given module before eval or REPL + -s, --stdio listen for and compile scripts over stdio + -l, --literate treat stdio as literate style coffeescript + -t, --tokens print out the tokens that the lexer/rewriter produce + -v, --version display the version number + -w, --watch watch scripts for changes and rerun commands + + ''' diff --git a/test/importing/shebang.coffee b/test/importing/shebang.coffee new file mode 100755 index 0000000000..fbd15960a1 --- /dev/null +++ b/test/importing/shebang.coffee @@ -0,0 +1,3 @@ +#!/usr/bin/env coffee + +process.stdout.write JSON.stringify(process.argv) diff --git a/test/importing/shebang_extra_args.coffee b/test/importing/shebang_extra_args.coffee new file mode 100755 index 0000000000..414e2faefd --- /dev/null +++ b/test/importing/shebang_extra_args.coffee @@ -0,0 +1,3 @@ +#!/usr/bin/env coffee -- + +process.stdout.write JSON.stringify(process.argv) diff --git a/test/importing/shebang_initial_space.coffee b/test/importing/shebang_initial_space.coffee new file mode 100755 index 0000000000..3a39f078e8 --- /dev/null +++ b/test/importing/shebang_initial_space.coffee @@ -0,0 +1,3 @@ +#! /usr/bin/env coffee + +process.stdout.write JSON.stringify(process.argv) diff --git a/test/importing/shebang_initial_space_extra_args.coffee b/test/importing/shebang_initial_space_extra_args.coffee new file mode 100644 index 0000000000..b7c8885683 --- /dev/null +++ b/test/importing/shebang_initial_space_extra_args.coffee @@ -0,0 +1,3 @@ +#! /usr/bin/env coffee extra + +process.stdout.write JSON.stringify(process.argv) diff --git a/test/invocation_argument_parsing.coffee b/test/invocation_argument_parsing.coffee new file mode 100644 index 0000000000..5a5b0e6a39 --- /dev/null +++ b/test/invocation_argument_parsing.coffee @@ -0,0 +1,108 @@ +return unless require? + +path = require 'path' +{spawnSync, execFileSync} = require 'child_process' + +# Get directory containing the compiled `coffee` executable and prepend it to +# the path so `#!/usr/bin/env coffee` resolves to our locally built file. +coffeeBinDir = path.dirname require.resolve('../bin/coffee') +patchedPath = "#{coffeeBinDir}:#{process.env.PATH}" +patchedEnv = Object.assign {}, process.env, {PATH: patchedPath} + +shebangScript = require.resolve './importing/shebang.coffee' +initialSpaceScript = require.resolve './importing/shebang_initial_space.coffee' +extraArgsScript = require.resolve './importing/shebang_extra_args.coffee' +initialSpaceExtraArgsScript = require.resolve './importing/shebang_initial_space_extra_args.coffee' + +test "parse arguments for shebang scripts correctly (on unix platforms)", -> + return if isWindows() + + stdout = execFileSync shebangScript, ['-abck'], {env: patchedEnv} + expectedArgs = ['coffee', shebangScript, '-abck'] + realArgs = JSON.parse stdout + arrayEq expectedArgs, realArgs + + stdout = execFileSync initialSpaceScript, ['-abck'], {env: patchedEnv} + expectedArgs = ['coffee', initialSpaceScript, '-abck'] + realArgs = JSON.parse stdout + arrayEq expectedArgs, realArgs + +test "warn and remove -- if it is the second positional argument", -> + result = spawnSync 'coffee', [shebangScript, '--'], {env: patchedEnv} + stderr = result.stderr.toString() + arrayEq JSON.parse(result.stdout), ['coffee', shebangScript] + ok stderr.match /^coffee was invoked with '--'/m + posArgs = stderr.match(/^The positional arguments were: (.*)$/m)[1] + arrayEq JSON.parse(posArgs), [shebangScript, '--'] + ok result.status is 0 + + result = spawnSync 'coffee', ['-b', shebangScript, '--'], {env: patchedEnv} + stderr = result.stderr.toString() + arrayEq JSON.parse(result.stdout), ['coffee', shebangScript] + ok stderr.match /^coffee was invoked with '--'/m + posArgs = stderr.match(/^The positional arguments were: (.*)$/m)[1] + arrayEq JSON.parse(posArgs), [shebangScript, '--'] + ok result.status is 0 + + result = spawnSync( + 'coffee', ['-b', shebangScript, '--', 'ANOTHER ONE'], {env: patchedEnv}) + stderr = result.stderr.toString() + arrayEq JSON.parse(result.stdout), ['coffee', shebangScript, 'ANOTHER ONE'] + ok stderr.match /^coffee was invoked with '--'/m + posArgs = stderr.match(/^The positional arguments were: (.*)$/m)[1] + arrayEq JSON.parse(posArgs), [shebangScript, '--', 'ANOTHER ONE'] + ok result.status is 0 + + result = spawnSync( + 'coffee', ['--', initialSpaceScript, 'arg'], {env: patchedEnv}) + expectedArgs = ['coffee', initialSpaceScript, 'arg'] + realArgs = JSON.parse result.stdout + arrayEq expectedArgs, realArgs + ok result.stderr.toString() is '' + ok result.status is 0 + +test "warn about non-portable shebang lines", -> + result = spawnSync 'coffee', [extraArgsScript, 'arg'], {env: patchedEnv} + stderr = result.stderr.toString() + arrayEq JSON.parse(result.stdout), ['coffee', extraArgsScript, 'arg'] + ok stderr.match /^The script to be run begins with a shebang line with more than one/m + [_, firstLine, file] = stderr.match(/^The shebang line was: '([^']+)' in file '([^']+)'/m) + ok (firstLine is '#!/usr/bin/env coffee --') + ok (file is extraArgsScript) + args = stderr.match(/^The arguments were: (.*)$/m)[1] + arrayEq JSON.parse(args), ['coffee', '--'] + ok result.status is 0 + + result = spawnSync 'coffee', [initialSpaceScript, 'arg'], {env: patchedEnv} + stderr = result.stderr.toString() + ok stderr is '' + arrayEq JSON.parse(result.stdout), ['coffee', initialSpaceScript, 'arg'] + ok result.status is 0 + + result = spawnSync( + 'coffee', [initialSpaceExtraArgsScript, 'arg'], {env: patchedEnv}) + stderr = result.stderr.toString() + arrayEq JSON.parse(result.stdout), ['coffee', initialSpaceExtraArgsScript, 'arg'] + ok stderr.match /^The script to be run begins with a shebang line with more than one/m + [_, firstLine, file] = stderr.match(/^The shebang line was: '([^']+)' in file '([^']+)'/m) + ok (firstLine is '#! /usr/bin/env coffee extra') + ok (file is initialSpaceExtraArgsScript) + args = stderr.match(/^The arguments were: (.*)$/m)[1] + arrayEq JSON.parse(args), ['coffee', 'extra'] + ok result.status is 0 + +test "both warnings will be shown at once", -> + result = spawnSync( + 'coffee', [initialSpaceExtraArgsScript, '--', 'arg'], {env: patchedEnv}) + stderr = result.stderr.toString() + arrayEq JSON.parse(result.stdout), ['coffee', initialSpaceExtraArgsScript, 'arg'] + ok stderr.match /^The script to be run begins with a shebang line with more than one/m + [_, firstLine, file] = stderr.match(/^The shebang line was: '([^']+)' in file '([^']+)'/m) + ok (firstLine is '#! /usr/bin/env coffee extra') + ok (file is initialSpaceExtraArgsScript) + args = stderr.match(/^The arguments were: (.*)$/m)[1] + arrayEq JSON.parse(args), ['coffee', 'extra'] + ok stderr.match /^coffee was invoked with '--'/m + posArgs = stderr.match(/^The positional arguments were: (.*)$/m)[1] + arrayEq JSON.parse(posArgs), [initialSpaceExtraArgsScript, '--', 'arg'] + ok result.status is 0 diff --git a/test/option_parser.coffee b/test/option_parser.coffee index 5401ae67bb..3449899cfe 100644 --- a/test/option_parser.coffee +++ b/test/option_parser.coffee @@ -1,18 +1,22 @@ # Option Parser # ------------- -# TODO: refactor option parser tests - # Ensure that the OptionParser handles arguments correctly. return unless require? {OptionParser} = require './../lib/coffeescript/optparse' -opt = new OptionParser [ +flags = [ ['-r', '--required [DIR]', 'desc required'] ['-o', '--optional', 'desc optional'] ['-l', '--list [FILES*]', 'desc list'] ] +banner = ''' + banner text +''' + +opt = new OptionParser flags, banner + test "basic arguments", -> args = ['one', 'two', 'three', '-r', 'dir'] result = opt.parse args @@ -41,3 +45,50 @@ test "-- and interesting combinations", -> eq undefined, result.optional eq undefined, result.required arrayEq args[1..], result.arguments + +test "throw if multiple flags try to use the same short or long name", -> + throws -> new OptionParser [ + ['-r', '--required [DIR]', 'required'] + ['-r', '--long', 'switch'] + ] + + throws -> new OptionParser [ + ['-a', '--append [STR]', 'append'] + ['-b', '--append', 'append with -b short opt'] + ] + + throws -> new OptionParser [ + ['--just-long', 'desc'] + ['--just-long', 'another desc'] + ] + + throws -> new OptionParser [ + ['-j', '--just-long', 'desc'] + ['--just-long', 'another desc'] + ] + + throws -> new OptionParser [ + ['--just-long', 'desc'] + ['-j', '--just-long', 'another desc'] + ] + +test "outputs expected help text", -> + expectedBanner = ''' + +banner text + + -r, --required desc required + -o, --optional desc optional + -l, --list desc list + + ''' + ok opt.help() is expectedBanner + + expected = [ + '' + ' -r, --required desc required' + ' -o, --optional desc optional' + ' -l, --list desc list' + '' + ].join('\n') + ok new OptionParser(flags).help() is expected diff --git a/test/support/helpers.coffee b/test/support/helpers.coffee index a7cbbc1df0..b758b4c027 100644 --- a/test/support/helpers.coffee +++ b/test/support/helpers.coffee @@ -30,3 +30,5 @@ exports.eqJS = (input, expectedOutput, msg) -> #{reset}#{expectedOutput}#{red} but instead it was: #{reset}#{actualOutput}#{red}""" + +exports.isWindows = -> process.platform is 'win32'