mirror of
https://github.com/jashkenas/coffeescript.git
synced 2022-11-09 12:23:24 -05:00
[CS2] Add #! support for executable scripts on Linux. (#3946)
* Add #! support for executable scripts on Linux. Pass arguments to executable script unchanged if using "#!/usr/bin/env coffee". (Previously, "./test.coffee -abck" would be turned into "-a -b -c -k", for example.) Fixes #1752. * refactor option parsing clean up parsing code and in the process fix oustanding bug where coffeescript modified arguments meant for an executable script * address comments * intermediate save * add note saying where OptionParser is used in coffee command * add some more work * fix flatten functions * refactor tests * make argument processing less confusing * add basic test * remove unused file * compilation now hangs * remove unnecessary changes * add tests!!! * add/fix some tests * clarify a test * fix helpers * fix opt parsing * fix infinite loop * make rule building easier to read * add tests for flag overlap * revamp argument parsing again and add more thorough testing * add tests, comment, clean unused method * address review comments * add test for direct invocation of shebang scripts * move shebang parsing test to separate file and check for browser * remove TODO * example backwards compatible warnings * add correct tests for warning 1 * add tests for warnings * commit output js libs and update docs * respond to review comments also add tests for help text * respond to review comments * fix example output * Rewrite argument parsing documentation to be more concise; add it to sidebar and body; add new output * Don’t mention deprecated syntax; clean up variable names
This commit is contained in:
parent
d287a798cc
commit
4e57ca6833
22 changed files with 672 additions and 213 deletions
|
@ -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>What’s 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>=></code> becomes an ES <code>=></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 CoffeeScript’s 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>=></code> becomes an ES <code>=></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 CoffeeScript’s 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, it’s possible to <code>extend</code> 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 <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 ES2017’s <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">-></span> <span class="keyword">this</span> <span class="comment"># Throws a compiler error</span>
|
||||
</code></pre>
|
||||
</blockquote><p>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:</p>
|
||||
</blockquote><p>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:</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">-></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><</code> and <code>></code> Operators</h3>
|
||||
<h3>JSX and the <code><</code> and <code>></code> operators</h3>
|
||||
<p>With the addition of <a href="#jsx">JSX</a>, the <code><</code> and <code>></code> characters serve as both the “less than” and “greater than” operators and as the delimiters for XML tags, like <code><div></code>. For best results, in general you should always wrap the operators in spaces to distinguish them from XML tags: <code>i < len</code>, not <code>i<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 code’s “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 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 <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">
|
||||
|
|
|
@ -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`.
|
|
@ -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.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
### What’s 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 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.
|
||||
|
||||
|
|
|
@ -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') %>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -73,6 +73,8 @@ exports.compile = compile = withPrettyErrors (code, options) ->
|
|||
generateSourceMap = options.sourceMap or options.inlineMap or not options.filename?
|
||||
filename = options.filename or '<anonymous>'
|
||||
|
||||
checkShebangLine filename, code
|
||||
|
||||
sources[filename] = code
|
||||
map = new SourceMap if generateSourceMap
|
||||
|
||||
|
@ -295,3 +297,16 @@ Error.prepareStackTrace = (err, stack) ->
|
|||
" at #{formatSourcePosition frame, getSourceMapping}"
|
||||
|
||||
"#{err.toString()}\n#{frames.join '\n'}\n"
|
||||
|
||||
checkShebangLine = (file, input) ->
|
||||
firstLine = input.split(/$/m)[0]
|
||||
rest = firstLine?.match(/^#!\s*([^\s]+\s*)(.*)/)
|
||||
args = rest?[2]?.split(/\s/).filter (s) -> s isnt ''
|
||||
if args?.length > 1
|
||||
console.error '''
|
||||
The script to be run begins with a shebang line with more than one
|
||||
argument. This script will fail on platforms such as Linux which only
|
||||
allow a single argument.
|
||||
'''
|
||||
console.error "The shebang line was: '#{firstLine}' in file '#{file}'"
|
||||
console.error "The arguments were: #{JSON.stringify args}"
|
||||
|
|
|
@ -25,7 +25,7 @@ hidden = (file) -> /^\.|~$/.test file
|
|||
|
||||
# The help banner that is printed in conjunction with `-h`/`--help`.
|
||||
BANNER = '''
|
||||
Usage: coffee [options] path/to/script.coffee -- [args]
|
||||
Usage: coffee [options] path/to/script.coffee [args]
|
||||
|
||||
If called without options, `coffee` will run your script.
|
||||
'''
|
||||
|
@ -69,7 +69,22 @@ exports.buildCSOptionParser = buildCSOptionParser = ->
|
|||
# `--` will be passed verbatim to your script as arguments in `process.argv`
|
||||
exports.run = ->
|
||||
optionParser = buildCSOptionParser()
|
||||
parseOptions()
|
||||
try parseOptions()
|
||||
catch err
|
||||
console.error "option parsing error: #{err.message}"
|
||||
process.exit 1
|
||||
|
||||
if (not opts.doubleDashed) and (opts.arguments[1] is '--')
|
||||
printWarn '''
|
||||
coffee was invoked with '--' as the second positional argument, which is
|
||||
now deprecated. To pass '--' as an argument to a script to run, put an
|
||||
additional '--' before the path to your script.
|
||||
|
||||
'--' will be removed from the argument list.
|
||||
'''
|
||||
printWarn "The positional arguments were: #{JSON.stringify opts.arguments}"
|
||||
opts.arguments = [opts.arguments[0]].concat opts.arguments[2..]
|
||||
|
||||
# Make the REPL *CLI* use the global context so as to (a) be consistent with the
|
||||
# `node` REPL CLI and, therefore, (b) make packages that modify native prototypes
|
||||
# (such as 'colors' and 'sugar') work as expected.
|
||||
|
|
|
@ -18,8 +18,8 @@ exports.OptionParser = class OptionParser
|
|||
# [short-flag, long-flag, description]
|
||||
#
|
||||
# Along with an optional banner for the usage help.
|
||||
constructor: (rules, @banner) ->
|
||||
@rules = buildRules rules
|
||||
constructor: (ruleDeclarations, @banner) ->
|
||||
@rules = buildRules ruleDeclarations
|
||||
|
||||
# Parse the list of arguments, populating an `options` object with all of the
|
||||
# specified options, and return it. Options after the first non-option
|
||||
|
@ -28,36 +28,33 @@ exports.OptionParser = class OptionParser
|
|||
# parsers that allow you to attach callback actions for every flag. Instead,
|
||||
# you're responsible for interpreting the options object.
|
||||
parse: (args) ->
|
||||
options = arguments: []
|
||||
skippingArgument = no
|
||||
originalArgs = args
|
||||
args = normalizeArguments args
|
||||
for arg, i in args
|
||||
if skippingArgument
|
||||
skippingArgument = no
|
||||
continue
|
||||
if arg is '--'
|
||||
pos = originalArgs.indexOf '--'
|
||||
options.arguments = options.arguments.concat originalArgs[(pos + 1)..]
|
||||
break
|
||||
isOption = !!(arg.match(LONG_FLAG) or arg.match(SHORT_FLAG))
|
||||
# the CS option parser is a little odd; options after the first
|
||||
# non-option argument are treated as non-option arguments themselves
|
||||
seenNonOptionArg = options.arguments.length > 0
|
||||
unless seenNonOptionArg
|
||||
matchedRule = no
|
||||
for rule in @rules
|
||||
if rule.shortFlag is arg or rule.longFlag is arg
|
||||
value = true
|
||||
if rule.hasArgument
|
||||
skippingArgument = yes
|
||||
value = args[i + 1]
|
||||
options[rule.name] = if rule.isList then (options[rule.name] or []).concat value else value
|
||||
matchedRule = yes
|
||||
break
|
||||
throw new Error "unrecognized option: #{arg}" if isOption and not matchedRule
|
||||
if seenNonOptionArg or not isOption
|
||||
options.arguments.push arg
|
||||
# The CoffeeScript option parser is a little odd; options after the first
|
||||
# non-option argument are treated as non-option arguments themselves.
|
||||
# Optional arguments are normalized by expanding merged flags into multiple
|
||||
# flags. This allows you to have `-wl` be the same as `--watch --lint`.
|
||||
# Note that executable scripts with a shebang (`#!`) line should use the
|
||||
# line `#!/usr/bin/env coffee`, or `#!/absolute/path/to/coffee`, without a
|
||||
# `--` argument after, because that will fail on Linux (see #3946).
|
||||
{rules, positional} = normalizeArguments args, @rules.flagDict
|
||||
options = {}
|
||||
|
||||
# The `argument` field is added to the rule instance non-destructively by
|
||||
# `normalizeArguments`.
|
||||
for {hasArgument, argument, isList, name} in rules
|
||||
if hasArgument
|
||||
if isList
|
||||
options[name] ?= []
|
||||
options[name].push argument
|
||||
else
|
||||
options[name] = argument
|
||||
else
|
||||
options[name] = true
|
||||
|
||||
if positional[0] is '--'
|
||||
options.doubleDashed = yes
|
||||
positional = positional[1..]
|
||||
|
||||
options.arguments = positional
|
||||
options
|
||||
|
||||
# Return the help text for this **OptionParser**, listing and describing all
|
||||
|
@ -65,7 +62,7 @@ exports.OptionParser = class OptionParser
|
|||
help: ->
|
||||
lines = []
|
||||
lines.unshift "#{@banner}\n" if @banner
|
||||
for rule in @rules
|
||||
for rule in @rules.ruleList
|
||||
spaces = 15 - rule.longFlag.length
|
||||
spaces = if spaces > 0 then repeat ' ', spaces else ''
|
||||
letPart = if rule.shortFlag then rule.shortFlag + ', ' else ' '
|
||||
|
@ -75,26 +72,39 @@ exports.OptionParser = class OptionParser
|
|||
# Helpers
|
||||
# -------
|
||||
|
||||
# Regex matchers for option flags.
|
||||
# Regex matchers for option flags on the command line and their rules.
|
||||
LONG_FLAG = /^(--\w[\w\-]*)/
|
||||
SHORT_FLAG = /^(-\w)$/
|
||||
MULTI_FLAG = /^-(\w{2,})/
|
||||
# Matches the long flag part of a rule for an option with an argument. Not
|
||||
# applied to anything in process.argv.
|
||||
OPTIONAL = /\[(\w+(\*?))\]/
|
||||
|
||||
# Build and return the list of option rules. If the optional *short-flag* is
|
||||
# unspecified, leave it out by padding with `null`.
|
||||
buildRules = (rules) ->
|
||||
for tuple in rules
|
||||
buildRules = (ruleDeclarations) ->
|
||||
ruleList = for tuple in ruleDeclarations
|
||||
tuple.unshift null if tuple.length < 3
|
||||
buildRule tuple...
|
||||
flagDict = {}
|
||||
for rule in ruleList
|
||||
# `shortFlag` is null if not provided in the rule.
|
||||
for flag in [rule.shortFlag, rule.longFlag] when flag?
|
||||
if flagDict[flag]?
|
||||
throw new Error "flag #{flag} for switch #{rule.name}
|
||||
was already declared for switch #{flagDict[flag].name}"
|
||||
flagDict[flag] = rule
|
||||
|
||||
{ruleList, flagDict}
|
||||
|
||||
# Build a rule from a `-o` short flag, a `--output [DIR]` long flag, and the
|
||||
# description of what the option does.
|
||||
buildRule = (shortFlag, longFlag, description, options = {}) ->
|
||||
buildRule = (shortFlag, longFlag, description) ->
|
||||
match = longFlag.match(OPTIONAL)
|
||||
shortFlag = shortFlag?.match(SHORT_FLAG)[1]
|
||||
longFlag = longFlag.match(LONG_FLAG)[1]
|
||||
{
|
||||
name: longFlag.substr 2
|
||||
name: longFlag.replace /^--/, ''
|
||||
shortFlag: shortFlag
|
||||
longFlag: longFlag
|
||||
description: description
|
||||
|
@ -102,14 +112,54 @@ buildRule = (shortFlag, longFlag, description, options = {}) ->
|
|||
isList: !!(match and match[2])
|
||||
}
|
||||
|
||||
# Normalize arguments by expanding merged flags into multiple flags. This allows
|
||||
# you to have `-wl` be the same as `--watch --lint`.
|
||||
normalizeArguments = (args) ->
|
||||
args = args[..]
|
||||
result = []
|
||||
for arg in args
|
||||
if match = arg.match MULTI_FLAG
|
||||
result.push '-' + l for l in match[1].split ''
|
||||
normalizeArguments = (args, flagDict) ->
|
||||
rules = []
|
||||
positional = []
|
||||
needsArgOpt = null
|
||||
for arg, argIndex in args
|
||||
# If the previous argument given to the script was an option that uses the
|
||||
# next command-line argument as its argument, create copy of the option’s
|
||||
# rule with an `argument` field.
|
||||
if needsArgOpt?
|
||||
withArg = Object.assign {}, needsArgOpt.rule, {argument: arg}
|
||||
rules.push withArg
|
||||
needsArgOpt = null
|
||||
continue
|
||||
|
||||
multiFlags = arg.match(MULTI_FLAG)?[1]
|
||||
.split('')
|
||||
.map (flagName) -> "-#{flagName}"
|
||||
if multiFlags?
|
||||
multiOpts = multiFlags.map (flag) ->
|
||||
rule = flagDict[flag]
|
||||
unless rule?
|
||||
throw new Error "unrecognized option #{flag} in multi-flag #{arg}"
|
||||
{rule, flag}
|
||||
# Only the last flag in a multi-flag may have an argument.
|
||||
[innerOpts..., lastOpt] = multiOpts
|
||||
for {rule, flag} in innerOpts
|
||||
if rule.hasArgument
|
||||
throw new Error "cannot use option #{flag} in multi-flag #{arg} except
|
||||
as the last option, because it needs an argument"
|
||||
rules.push rule
|
||||
if lastOpt.rule.hasArgument
|
||||
needsArgOpt = lastOpt
|
||||
else
|
||||
rules.push lastOpt.rule
|
||||
else if ([LONG_FLAG, SHORT_FLAG].some (pat) -> arg.match(pat)?)
|
||||
singleRule = flagDict[arg]
|
||||
unless singleRule?
|
||||
throw new Error "unrecognized option #{arg}"
|
||||
if singleRule.hasArgument
|
||||
needsArgOpt = {rule: singleRule, flag: arg}
|
||||
else
|
||||
rules.push singleRule
|
||||
else
|
||||
result.push arg
|
||||
result
|
||||
# This is a positional argument.
|
||||
positional = args[argIndex..]
|
||||
break
|
||||
|
||||
if needsArgOpt?
|
||||
throw new Error "value required for #{needsArgOpt.flag}, but it was the last
|
||||
argument provided"
|
||||
{rules, positional}
|
||||
|
|
|
@ -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
|
142
test/argument_parsing.coffee
Normal file
142
test/argument_parsing.coffee
Normal 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
3
test/importing/shebang.coffee
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env coffee
|
||||
|
||||
process.stdout.write JSON.stringify(process.argv)
|
3
test/importing/shebang_extra_args.coffee
Executable file
3
test/importing/shebang_extra_args.coffee
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env coffee --
|
||||
|
||||
process.stdout.write JSON.stringify(process.argv)
|
3
test/importing/shebang_initial_space.coffee
Executable file
3
test/importing/shebang_initial_space.coffee
Executable file
|
@ -0,0 +1,3 @@
|
|||
#! /usr/bin/env coffee
|
||||
|
||||
process.stdout.write JSON.stringify(process.argv)
|
3
test/importing/shebang_initial_space_extra_args.coffee
Normal file
3
test/importing/shebang_initial_space_extra_args.coffee
Normal file
|
@ -0,0 +1,3 @@
|
|||
#! /usr/bin/env coffee extra
|
||||
|
||||
process.stdout.write JSON.stringify(process.argv)
|
108
test/invocation_argument_parsing.coffee
Normal file
108
test/invocation_argument_parsing.coffee
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue