Merge pull request #306 from sass/cli-refactor

Improve CLI option handling
This commit is contained in:
Natalie Weizenbaum 2018-04-25 15:15:59 -07:00 committed by GitHub
commit 8c4180685a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 257 additions and 157 deletions

View File

@ -5,145 +5,88 @@
import 'dart:async';
import 'dart:isolate';
import 'package:args/args.dart';
import 'package:stack_trace/stack_trace.dart';
import '../sass.dart';
import 'exception.dart';
import 'executable_options.dart';
import 'io.dart';
import 'util/path.dart';
main(List<String> args) async {
var argParser = new ArgParser(allowTrailingOptions: true)
..addOption('precision', hide: true)
..addFlag('stdin', help: 'Read the stylesheet from stdin.')
..addFlag('indented', help: 'Use the indented syntax for input from stdin.')
..addMultiOption('load-path',
abbr: 'I',
valueHelp: 'PATH',
help: 'A path to use when resolving imports.\n'
'May be passed multiple times.',
splitCommas: false)
..addOption('style',
abbr: 's',
valueHelp: 'NAME',
help: 'Output style.',
allowed: ['expanded', 'compressed'],
defaultsTo: 'expanded')
..addFlag('color', abbr: 'c', help: 'Whether to emit terminal colors.')
..addFlag('quiet', abbr: 'q', help: "Don't print warnings.")
..addFlag('trace', help: 'Print full Dart stack traces for exceptions.')
..addFlag('help',
abbr: 'h', help: 'Print this usage information.', negatable: false)
..addFlag('version',
help: 'Print the version of Dart Sass.', negatable: false)
// This is used when testing to ensure that the asynchronous evaluator path
// works the same as the synchronous one.
..addFlag('async', hide: true);
ArgResults options;
ExecutableOptions options;
try {
options = argParser.parse(args);
} on FormatException catch (error) {
_printUsage(argParser, error.message);
exitCode = 64;
return;
}
if (options['version'] as bool) {
_loadVersion().then((version) {
print(version);
options = new ExecutableOptions.parse(args);
if (options.version) {
print(await _loadVersion());
exitCode = 0;
});
return;
}
return;
}
var stdinFlag = options['stdin'] as bool;
if (options['help'] as bool ||
(stdinFlag
? options.rest.length > 1
: options.rest.isEmpty || options.rest.length > 2)) {
_printUsage(argParser, "Compile Sass to CSS.");
exitCode = 64;
return;
}
var indented =
options.wasParsed('indented') ? options['indented'] as bool : null;
var color =
options.wasParsed('color') ? options['color'] as bool : hasTerminal;
var logger =
options['quiet'] as bool ? Logger.quiet : new Logger.stderr(color: color);
var style = options['style'] == 'compressed'
? OutputStyle.compressed
: OutputStyle.expanded;
var loadPaths = options['load-path'] as List<String>;
var asynchronous = options['async'] as bool;
try {
String css;
String destination;
if (stdinFlag) {
if (options.rest.isNotEmpty) destination = options.rest.first;
css = await _compileStdin(
indented: indented,
logger: logger,
style: style,
loadPaths: loadPaths,
asynchronous: asynchronous);
} else {
var source = options.rest.first;
if (options.rest.length > 1) destination = options.rest.last;
if (source == '-') {
css = await _compileStdin(
indented: indented,
logger: logger,
style: style,
loadPaths: loadPaths,
asynchronous: asynchronous);
} else if (asynchronous) {
css = await compileAsync(source,
logger: logger, style: style, loadPaths: loadPaths);
try {
var text =
options.readFromStdin ? await readStdin() : readFile(options.source);
var url = options.readFromStdin ? null : p.toUri(options.source);
var importer = new FilesystemImporter('.');
String css;
if (options.asynchronous) {
css = await compileStringAsync(text,
indented: options.indented,
logger: options.logger,
style: options.style,
importer: importer,
loadPaths: options.loadPaths,
url: url);
} else {
css =
compile(source, logger: logger, style: style, loadPaths: loadPaths);
css = compileString(text,
indented: options.indented,
logger: options.logger,
style: options.style,
importer: importer,
loadPaths: options.loadPaths,
url: url);
}
if (options.writeToStdout) {
if (css.isNotEmpty) print(css);
} else {
ensureDir(p.dirname(options.destination));
writeFile(options.destination, css + "\n");
}
} on SassException catch (error, stackTrace) {
stderr.writeln(error.toString(color: options.color));
if (options.trace) {
stderr.writeln();
stderr.write(new Trace.from(stackTrace).terse.toString());
stderr.flush();
}
// Exit code 65 indicates invalid data per
// http://www.freebsd.org/cgi/man.cgi?query=sysexits.
exitCode = 65;
} on FileSystemException catch (error, stackTrace) {
stderr.writeln(
"Error reading ${p.relative(error.path)}: ${error.message}.");
// Error 66 indicates no input.
exitCode = 66;
if (options.trace) {
stderr.writeln();
stderr.write(new Trace.from(stackTrace).terse.toString());
stderr.flush();
}
}
if (destination != null) {
ensureDir(p.dirname(destination));
writeFile(destination, css + "\n");
} else if (css.isNotEmpty) {
print(css);
}
} on SassException catch (error, stackTrace) {
stderr.writeln(error.toString(color: color));
if (options['trace'] as bool) {
stderr.writeln();
stderr.write(new Trace.from(stackTrace).terse.toString());
stderr.flush();
}
// Exit code 65 indicates invalid data per
// http://www.freebsd.org/cgi/man.cgi?query=sysexits.
exitCode = 65;
} on FileSystemException catch (error, stackTrace) {
stderr
.writeln("Error reading ${p.relative(error.path)}: ${error.message}.");
// Error 66 indicates no input.
exitCode = 66;
if (options['trace'] as bool) {
stderr.writeln();
stderr.write(new Trace.from(stackTrace).terse.toString());
stderr.flush();
}
} on UsageException catch (error) {
print("${error.message}\n");
print("Usage: sass <input> [output]\n");
print(ExecutableOptions.usage);
exitCode = 64;
} catch (error, stackTrace) {
if (color) stderr.write('\u001b[31m\u001b[1m');
if (options != null && options.color) stderr.write('\u001b[31m\u001b[1m');
stderr.write('Unexpected exception:');
if (color) stderr.write('\u001b[0m');
if (options != null && options.color) stderr.write('\u001b[0m');
stderr.writeln();
stderr.writeln(error);
@ -172,36 +115,3 @@ Future<String> _loadVersion() async {
.split(" ")
.last;
}
/// Compiles Sass from standard input and returns the result.
Future<String> _compileStdin(
{bool indented,
Logger logger,
OutputStyle style,
List<String> loadPaths,
bool asynchronous: false}) async {
var text = await readStdin();
var importer = new FilesystemImporter('.');
if (asynchronous) {
return await compileStringAsync(text,
indented: indented ?? false,
logger: logger,
style: style,
importer: importer,
loadPaths: loadPaths);
} else {
return compileString(text,
indented: indented ?? false,
logger: logger,
style: style,
importer: importer,
loadPaths: loadPaths);
}
}
/// Print the usage information for Sass, with [message] as a header.
void _printUsage(ArgParser parser, String message) {
print("$message\n");
print("Usage: sass <input> [output]\n");
print(parser.usage);
}

View File

@ -0,0 +1,190 @@
// Copyright 2018 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:args/args.dart';
import 'package:meta/meta.dart';
import '../sass.dart';
import 'io.dart';
import 'util/path.dart';
/// The parsed and processed command-line options for the Sass executable.
///
/// The constructor and any members may throw [UsageException]s indicating that
/// invalid arguments were passed.
class ExecutableOptions {
/// The bar character to use in help separators.
static final _separatorBar = isWindows ? '=' : '';
/// The total length of help separators, including text.
static final _separatorLength = 40;
/// The parser that defines the arguments the executable allows.
static final ArgParser _parser = () {
var parser = new ArgParser(allowTrailingOptions: true)
// This is used for compatibility with sass-spec, even though we don't
// support setting the precision.
..addOption('precision', hide: true)
// This is used when testing to ensure that the asynchronous evaluator path
// works the same as the synchronous one.
..addFlag('async', hide: true);
parser
..addSeparator(_separator('Input and Output'))
..addFlag('stdin', help: 'Read the stylesheet from stdin.')
..addFlag('indented',
help: 'Use the indented syntax for input from stdin.')
..addMultiOption('load-path',
abbr: 'I',
valueHelp: 'PATH',
help: 'A path to use when resolving imports.\n'
'May be passed multiple times.',
splitCommas: false)
..addOption('style',
abbr: 's',
valueHelp: 'NAME',
help: 'Output style.',
allowed: ['expanded', 'compressed'],
defaultsTo: 'expanded');
parser
..addSeparator(_separator('Other'))
..addFlag('color', abbr: 'c', help: 'Whether to emit terminal colors.')
..addFlag('quiet', abbr: 'q', help: "Don't print warnings.")
..addFlag('trace', help: 'Print full Dart stack traces for exceptions.')
..addFlag('help',
abbr: 'h', help: 'Print this usage information.', negatable: false)
..addFlag('version',
help: 'Print the version of Dart Sass.', negatable: false);
return parser;
}();
/// Creates a styled separator with the given [text].
static String _separator(String text) =>
_separatorBar * 3 +
" " +
(hasTerminal ? '\u001b[1m' : '') +
text +
(hasTerminal ? '\u001b[0m' : '') +
' ' +
// Three separators + two spaces = 5
_separatorBar * (_separatorLength - 5 - text.length);
/// A human-readable description of how to invoke the Sass executable.
static String get usage => _parser.usage;
/// Shorthand for throwing a [UsageException] with the given [message].
@alwaysThrows
static void _fail(String message) => throw new UsageException(message);
/// The parsed options passed by the user to the executable.
final ArgResults _options;
/// Whether to print the version of Sass and exit.
bool get version => _options['version'] as bool;
/// Whether to parse the source file with the indented syntax.
bool get indented =>
_ifParsed('indented') as bool ??
(source != null && p.extension(source) == '.sass');
/// Whether to use ANSI terminal colors.
bool get color =>
_options.wasParsed('color') ? _options['color'] as bool : hasTerminal;
/// The logger to use to emit messages from Sass.
Logger get logger => _options['quiet'] as bool
? Logger.quiet
: new Logger.stderr(color: color);
/// The style to use for the generated CSS.
OutputStyle get style => _options['style'] == 'compressed'
? OutputStyle.compressed
: OutputStyle.expanded;
/// The set of paths Sass in which should look for imported files.
List<String> get loadPaths => _options['load-path'] as List<String>;
/// Whether to run the evaluator in asynchronous mode, for debugging purposes.
bool get asynchronous => _options['async'] as bool;
/// Whether to print the full Dart stack trace on exceptions.
bool get trace => _options['trace'] as bool;
/// The entrypoint Sass file, or `null` if the source should be read from
/// stdin.
String get source {
_ensureSourceAndDestination();
return _source;
}
String _source;
/// Whether to read the source file from stdin rather than a file on disk.
bool get readFromStdin => source == null;
/// The path to which to write the CSS, or `null` if the CSS should be printed
/// to stdout.
String get destination {
_ensureSourceAndDestination();
return _destination;
}
String _destination;
/// Whether to write the output CSS to stdout rather than a file on disk.
bool get writeToStdout => destination == null;
/// Whether [_source] and [_destination] have been parsed from [_options] yet.
var _parsedSourceAndDestination = false;
/// Parses options from [args].
///
/// Throws a [UsageException] if parsing fails.
factory ExecutableOptions.parse(List<String> args) {
try {
var options = new ExecutableOptions._(_parser.parse(args));
if (options._options['help'] as bool) _fail("Compile Sass to CSS.");
return options;
} on FormatException catch (error) {
_fail(error.message);
}
}
ExecutableOptions._(this._options);
/// Parses [source] and [destination] from [_options] if they haven't been
/// parsed yet.
void _ensureSourceAndDestination() {
if (_parsedSourceAndDestination) return;
_parsedSourceAndDestination = true;
if (_options['stdin'] as bool) {
if (_options.rest.length > 1) _fail("Compile Sass to CSS.");
if (_options.rest.isNotEmpty) _destination = _options.rest.first;
} else if (_options.rest.isEmpty || _options.rest.length > 2) {
_fail("Compile Sass to CSS.");
} else if (_options.rest.first == '-') {
if (_options.rest.length > 1) _destination = _options.rest.last;
} else {
_source = _options.rest.first;
if (_options.rest.length > 1) _destination = _options.rest.last;
}
}
/// Returns the value of [name] in [options] if it was explicitly provided by
/// the user, and `null` otherwise.
Object _ifParsed(String name) =>
_options.wasParsed(name) ? _options[name] : null;
}
/// An exception indicating that invalid arguments were passed.
class UsageException implements Exception {
final String message;
UsageException(this.message);
}

View File

@ -17,7 +17,7 @@ dependencies:
collection: "^1.8.0"
convert: "^2.0.1"
dart2_constant: "^1.0.0"
meta: "^1.0.0"
meta: "^1.1.0"
path: "^1.0.0"
source_maps: "^0.10.0"
source_span: "^1.4.0"