[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

@ -306,7 +306,7 @@ td code {
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, h3 {
h2 {
font-weight: 800;
}
h3, h2 time {
h3, h4, h2 time {
font-weight: 400;
}
@ -734,6 +734,9 @@ textarea {
<li class="nav-item">
<a href="#breaking-changes-literate-coffeescript" class="nav-link" data-action="sidebar-nav">Literate CoffeeScript Parsing</a>
</li>
<li class="nav-item">
<a href="#breaking-changes-argument-parsing-and-shebang-lines" class="nav-link" data-action="sidebar-nav">Argument Parsing and <code>#!</code> Lines</a>
</li>
</ul>
</li>
<li class="nav-item">
@ -858,7 +861,7 @@ cubes = (function() {
<section id="coffeescript-2">
<h2>CoffeeScript 2</h2>
<h3>Whats New In CoffeeScript 2?</h3>
<p>The biggest change in CoffeeScript 2 is that now the CoffeeScript compiler produces modern, ES2015+ JavaScript. A CoffeeScript <code>=&gt;</code> becomes an ES <code>=&gt;</code>, a CoffeeScript <code>class</code> becomes an ES <code>class</code> and so on. With the exception of modules (<code>import</code> and <code>export</code> statements), all the ES2015+ features that CoffeeScript supports can run natively in Node 7.6+, meaning that Node can run CoffeeScripts output without any further processing required. You can <a href="http://coffeescript.org/v2/test.html">run the tests in your browser</a> to see if your browser can do the same; Chrome has supported all features since version 55.</p>
<p>The biggest change in CoffeeScript 2 is that now the CoffeeScript compiler produces modern, ES2015+ JavaScript. A CoffeeScript <code>=&gt;</code> becomes an ES <code>=&gt;</code>, a CoffeeScript <code>class</code> becomes an ES <code>class</code> and so on. With the exception of <a href="#modules">modules</a> (<code>import</code> and <code>export</code> statements) and <a href="#jsx">JSX</a>, all the ES2015+ features that CoffeeScript supports can run natively in Node 7.6+, meaning that Node can run CoffeeScripts output without any further processing required. You can <a href="http://coffeescript.org/v2/test.html">run the tests in your browser</a> to see if your browser can do the same; Chrome has supported all features since version 55.</p>
<p>Support for ES2015+ syntax is important to ensure compatibility with frameworks that assume ES2015. Now that CoffeeScript compiles classes to the ES <code>class</code> keyword, its possible to <code>extend</code> an ES class; that wasnt possible in CoffeeScript 1. Parity in how language features work is also important on its own; CoffeeScript “is just JavaScript,” and so things like <a href="#breaking-changes-default-values">function parameter default values</a> should behave the same in CoffeeScript as in JavaScript.</p>
<p>Many ES2015+ features have been backported to CoffeeScript 1.11 and 1.12, including <a href="#modules">modules</a>, <a href="#generator-iteration"><code>for…of</code></a>, and <a href="#tagged-template-literals">tagged template literals</a>. Major new features unique to CoffeeScript 2 are support for ES2017s <a href="#async-functions">async functions</a> and for <a href="#jsx">JSX</a>. More details are in the <a href="#changelog">changelog</a>.</p>
<p>There are very few <a href="#breaking-changes">breaking changes from CoffeeScript 1.x to 2</a>; we hope the upgrade process is smooth for most projects.</p>
@ -3244,7 +3247,7 @@ renderStarRating = function({rating, maxStars}) {
</div>
</aside>
<p>Older plugins or forks of CoffeeScript supported JSX syntax and referred to it as CSX or CJSX. They also often used a <code>.cjsx</code> file extension, but this is no longer necessary; regalar <code>.coffee</code> will do.</p>
<p>Older plugins or forks of CoffeeScript supported JSX syntax and referred to it as CSX or CJSX. They also often used a <code>.cjsx</code> file extension, but this is no longer necessary; regular <code>.coffee</code> will do.</p>
</section>
</section>
@ -3619,7 +3622,7 @@ f = function*() {
<blockquote class="uneditable-code-block"><pre><code class="language-coffee"><span class="class"><span class="keyword">class</span> <span class="title">B</span> <span class="keyword">extends</span> <span class="title">A</span></span>
constructor: <span class="function">-&gt;</span> <span class="keyword">this</span> <span class="comment"># Throws a compiler error</span>
</code></pre>
</blockquote><p>ES2015 classes dont allow bound (fat arrow) methods. The CoffeeScript compiler goes through some contortions to preserve support for them, but one thing that cant be accomodated is calling a bound method before it is bound:</p>
</blockquote><p>ES2015 classes dont allow bound (fat arrow) methods. The CoffeeScript compiler goes through some contortions to preserve support for them, but one thing that cant be accommodated is calling a bound method before it is bound:</p>
<blockquote class="uneditable-code-block"><pre><code class="language-coffee"><span class="class"><span class="keyword">class</span> <span class="title">Base</span></span>
constructor: <span class="function">-&gt;</span>
@onClick() <span class="comment"># This works</span>
@ -3787,7 +3790,7 @@ B = class B extends A {
</section>
<section id="breaking-changes-jsx-and-the-less-than-and-greater-than-operators">
<h3>JSX and the <code>&lt;</code> and <code>&gt;</code> Operators</h3>
<h3>JSX and the <code>&lt;</code> and <code>&gt;</code> operators</h3>
<p>With the addition of <a href="#jsx">JSX</a>, the <code>&lt;</code> and <code>&gt;</code> characters serve as both the “less than” and “greater than” operators and as the delimiters for XML tags, like <code>&lt;div&gt;</code>. For best results, in general you should always wrap the operators in spaces to distinguish them from XML tags: <code>i &lt; len</code>, not <code>i&lt;len</code>. 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.</p>
</section>
@ -3797,6 +3800,29 @@ B = class B extends A {
<p>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 codes “left margin,” with all code in the file relative to that column.</p>
<p>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.</p>
</section>
<section id="breaking-changes-argument-parsing-and-shebang-lines">
<h3>Argument parsing and shebang (<code>#!</code>) lines</h3>
<p>In CoffeeScript 1.x, <code>--</code> 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:</p>
<blockquote class="uneditable-code-block"><pre><code class="language-bash">coffee [options] path/to/script.coffee -- [args]
</code></pre>
</blockquote><p>Now you would just type:</p>
<blockquote class="uneditable-code-block"><pre><code class="language-bash">coffee [options] path/to/script.coffee [args]
</code></pre>
</blockquote><p>The deprecated version will still work, but it will print a warning before running the script.</p>
<p>On non-Windows platforms, a <code>.coffee</code> file can be made executable by adding a shebang (<code>#!</code>) line at the top of the file and marking the file as executable. For example:</p>
<blockquote class="uneditable-code-block"><pre><code class="language-coffee"><span class="comment">#!/usr/bin/env coffee</span>
x = <span class="number">2</span> + <span class="number">2</span>
<span class="built_in">console</span>.log x
</code></pre>
</blockquote><p>If this were saved as <code>executable.coffee</code>, it could be made executable and run:</p>
<blockquote class="uneditable-code-block"><pre><code class="language-bash">▶ chmod +x ./executable.coffee
▶ ./executable.coffee
4
</code></pre>
</blockquote><p>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 <code>#!/usr/bin/env coffee --</code> as the first line of the file. That didnt 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 <code>--</code>, we recommend simply changing the shebang lines in such scripts to just <code>#!/usr/bin/env coffee</code>.</p>
</section>
</section>
<section id="changelog">

View File

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

View File

@ -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 `<div>`. For best results, in general you should always wrap the operators in spaces to distinguish them from XML tags: `i < len`, not `i<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.

View File

@ -2,7 +2,7 @@
### Whats New In CoffeeScript 2?
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 CoffeeScripts 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 CoffeeScripts 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, its possible to `extend` an ES class; that wasnt 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.

View File

@ -178,6 +178,9 @@
<section id="breaking-changes-literate-coffeescript">
<%= htmlFor('breaking_changes_literate_coffeescript') %>
</section>
<section id="breaking-changes-argument-parsing-and-shebang-lines">
<%= htmlFor('breaking_changes_argument_parsing_and_shebang_lines') %>
</section>
</section>
<section id="changelog">
<%= htmlFor('changelog') %>

View File

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

View File

@ -171,6 +171,9 @@
<li class="nav-item">
<a href="#breaking-changes-literate-coffeescript" class="nav-link" data-action="sidebar-nav">Literate CoffeeScript Parsing</a>
</li>
<li class="nav-item">
<a href="#breaking-changes-argument-parsing-and-shebang-lines" class="nav-link" data-action="sidebar-nav">Argument Parsing and <code>#!</code> Lines</a>
</li>
</ul>
</li>
<li class="nav-item">

View File

@ -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 || '<anonymous>';
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);

View File

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

View File

@ -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);

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}

View File

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

View File

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

3
test/importing/shebang.coffee Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env coffee
process.stdout.write JSON.stringify(process.argv)

View File

@ -0,0 +1,3 @@
#!/usr/bin/env coffee --
process.stdout.write JSON.stringify(process.argv)

View File

@ -0,0 +1,3 @@
#! /usr/bin/env coffee
process.stdout.write JSON.stringify(process.argv)

View File

@ -0,0 +1,3 @@
#! /usr/bin/env coffee extra
process.stdout.write JSON.stringify(process.argv)

View File

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

View File

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

View File

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