Use colors in output

This is a pretty gross commit, since it also includes a lot of
unrelated refactoring, especially of how error messages are printed.

Also adds a lint recipe that prints lines over 100 characters

To test, I added a `--color=[auto|always|never]` option that defaults to
auto in normal use, but can be forced to `always` for testing. In `auto`
mode it defers to `atty` to figure out if the current stream is a
terminal and uses color if so.

Color printing is controlled by the `alternate` formatting flag.

When printing an error message, using `{:#}` will print it with colors
and `{}` will print it normally.
This commit is contained in:
Casey Rodarmor 2016-11-07 21:01:27 -08:00
parent 6b888bbfe4
commit 4d20ffeac4
7 changed files with 250 additions and 86 deletions

13
Cargo.lock generated
View File

@ -2,6 +2,8 @@
name = "just" name = "just"
version = "0.2.16" version = "0.2.16"
dependencies = [ dependencies = [
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"atty 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"brev 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "brev 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.17.1 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.17.1 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
@ -23,6 +25,16 @@ name = "ansi_term"
version = "0.9.0" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "atty"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "0.7.0" version = "0.7.0"
@ -195,6 +207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[metadata] [metadata]
"checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" "checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66"
"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6" "checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"
"checksum atty 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d7b4cb091c727ebec026331c1b7092981c9cdde34d8df109fa36f29a37532026"
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" "checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
"checksum brev 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "79571b60a8aa293f43b46370d8ba96fed28a5bee1303ea0e015d175ed0c63b40" "checksum brev 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "79571b60a8aa293f43b46370d8ba96fed28a5bee1303ea0e015d175ed0c63b40"
"checksum clap 2.17.1 (registry+https://github.com/rust-lang/crates.io-index)" = "27dac76762fb56019b04aed3ccb43a770a18f80f9c2eb62ee1a18d9fb4ea2430" "checksum clap 2.17.1 (registry+https://github.com/rust-lang/crates.io-index)" = "27dac76762fb56019b04aed3ccb43a770a18f80f9c2eb62ee1a18d9fb4ea2430"

View File

@ -7,6 +7,8 @@ license = "WTFPL/MIT/Apache-2.0"
homepage = "https://github.com/casey/just" homepage = "https://github.com/casey/just"
[dependencies] [dependencies]
ansi_term = "^0.9.0"
atty = "^0.2.1"
brev = "^0.1.6" brev = "^0.1.6"
clap = "^2.0.0" clap = "^2.0.0"
itertools = "^0.5.5" itertools = "^0.5.5"

View File

@ -42,8 +42,14 @@ install-nightly:
sloc: sloc:
@cat src/*.rs | wc -l @cat src/*.rs | wc -l
long:
! grep --color -n '.\{100\}' src/*.rs
nop: nop:
fail:
exit 1
# make a quine, compile it, and verify it # make a quine, compile it, and verify it
quine: create quine: create
cc tmp/gen0.c -o tmp/gen0 cc tmp/gen0.c -o tmp/gen0

2
notes
View File

@ -1,2 +1,2 @@
todo todo
---- ====

View File

@ -1,5 +1,6 @@
extern crate clap; extern crate clap;
extern crate regex; extern crate regex;
extern crate atty;
use std::{io, fs, env, process}; use std::{io, fs, env, process};
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -21,6 +22,32 @@ macro_rules! die {
}}; }};
} }
#[derive(Copy, Clone)]
enum UseColor {
Auto,
Always,
Never,
}
impl UseColor {
fn from_argument(use_color: &str) -> UseColor {
match use_color {
"auto" => UseColor::Auto,
"always" => UseColor::Always,
"never" => UseColor::Never,
_ => panic!("Invalid argument to --color. This is a bug in just."),
}
}
fn should_color_stream(self, stream: atty::Stream) -> bool {
match self {
UseColor::Auto => atty::is(stream),
UseColor::Always => true,
UseColor::Never => true,
}
}
}
pub fn app() { pub fn app() {
let matches = App::new("just") let matches = App::new("just")
.version(concat!("v", env!("CARGO_PKG_VERSION"))) .version(concat!("v", env!("CARGO_PKG_VERSION")))
@ -41,6 +68,12 @@ pub fn app() {
.arg(Arg::with_name("evaluate") .arg(Arg::with_name("evaluate")
.long("evaluate") .long("evaluate")
.help("Print evaluated variables")) .help("Print evaluated variables"))
.arg(Arg::with_name("color")
.long("color")
.takes_value(true)
.possible_values(&["auto", "always", "never"])
.default_value("auto")
.help("Print colorful output"))
.arg(Arg::with_name("show") .arg(Arg::with_name("show")
.short("s") .short("s")
.long("show") .long("show")
@ -79,6 +112,9 @@ pub fn app() {
die!("--dry-run and --quiet may not be used together"); die!("--dry-run and --quiet may not be used together");
} }
let use_color_argument = matches.value_of("color").expect("--color had no value");
let use_color = UseColor::from_argument(use_color_argument);
let justfile_option = matches.value_of("justfile"); let justfile_option = matches.value_of("justfile");
let working_directory_option = matches.value_of("working-directory"); let working_directory_option = matches.value_of("working-directory");
@ -125,7 +161,13 @@ pub fn app() {
.unwrap_or_else(|error| die!("Error reading justfile: {}", error)); .unwrap_or_else(|error| die!("Error reading justfile: {}", error));
} }
let justfile = super::parse(&text).unwrap_or_else(|error| die!("{}", error)); let justfile = super::parse(&text).unwrap_or_else(|error|
if use_color.should_color_stream(atty::Stream::Stderr) {
die!("{:#}", error);
} else {
die!("{}", error);
}
);
if matches.is_present("list") { if matches.is_present("list") {
if justfile.count() == 0 { if justfile.count() == 0 {
@ -185,8 +227,12 @@ pub fn app() {
if let Err(run_error) = justfile.run(&arguments, &options) { if let Err(run_error) = justfile.run(&arguments, &options) {
if !options.quiet { if !options.quiet {
if use_color.should_color_stream(atty::Stream::Stderr) {
warn!("{:#}", run_error);
} else {
warn!("{}", run_error); warn!("{}", run_error);
} }
}
match run_error { match run_error {
RunError::Code{code, .. } | RunError::BacktickCode{code, ..} => process::exit(code), RunError::Code{code, .. } | RunError::BacktickCode{code, ..} => process::exit(code),
_ => process::exit(-1), _ => process::exit(-1),

View File

@ -13,7 +13,8 @@ fn integration_test(
expected_stderr: &str, expected_stderr: &str,
) { ) {
let tmp = TempDir::new("just-integration") let tmp = TempDir::new("just-integration")
.unwrap_or_else(|err| panic!("integration test: failed to create temporary directory: {}", err)); .unwrap_or_else(
|err| panic!("integration test: failed to create temporary directory: {}", err));
let mut path = tmp.path().to_path_buf(); let mut path = tmp.path().to_path_buf();
path.push("justfile"); path.push("justfile");
brev::dump(path, justfile); brev::dump(path, justfile);
@ -300,7 +301,7 @@ recipe:
text, text,
100, 100,
"", "",
"Recipe \"recipe\" failed with exit code 100\n", "error: Recipe `recipe` failed with exit code 100\n",
); );
} }
@ -348,7 +349,7 @@ fn backtick_code_assignment() {
"b = a\na = `exit 100`\nbar:\n echo '{{`exit 200`}}'", "b = a\na = `exit 100`\nbar:\n echo '{{`exit 200`}}'",
100, 100,
"", "",
"backtick failed with exit code 100 "error: backtick failed with exit code 100
| |
2 | a = `exit 100` 2 | a = `exit 100`
| ^^^^^^^^^^ | ^^^^^^^^^^
@ -363,7 +364,7 @@ fn backtick_code_interpolation() {
"b = a\na = `echo hello`\nbar:\n echo '{{`exit 200`}}'", "b = a\na = `echo hello`\nbar:\n echo '{{`exit 200`}}'",
200, 200,
"", "",
"backtick failed with exit code 200 "error: backtick failed with exit code 200
| |
4 | echo '{{`exit 200`}}' 4 | echo '{{`exit 200`}}'
| ^^^^^^^^^^ | ^^^^^^^^^^
@ -378,7 +379,7 @@ fn backtick_code_long() {
"\n\n\n\n\n\nb = a\na = `echo hello`\nbar:\n echo '{{`exit 200`}}'", "\n\n\n\n\n\nb = a\na = `echo hello`\nbar:\n echo '{{`exit 200`}}'",
200, 200,
"", "",
"backtick failed with exit code 200 "error: backtick failed with exit code 200
| |
10 | echo '{{`exit 200`}}' 10 | echo '{{`exit 200`}}'
| ^^^^^^^^^^ | ^^^^^^^^^^
@ -396,7 +397,7 @@ fn shebang_backtick_failure() {
echo {{`exit 123`}}", echo {{`exit 123`}}",
123, 123,
"", "",
"backtick failed with exit code 123 "error: backtick failed with exit code 123
| |
4 | echo {{`exit 123`}} 4 | echo {{`exit 123`}}
| ^^^^^^^^^^ | ^^^^^^^^^^
@ -413,7 +414,7 @@ fn command_backtick_failure() {
echo {{`exit 123`}}", echo {{`exit 123`}}",
123, 123,
"hello\n", "hello\n",
"echo hello\nbacktick failed with exit code 123 "echo hello\nerror: backtick failed with exit code 123
| |
3 | echo {{`exit 123`}} 3 | echo {{`exit 123`}}
| ^^^^^^^^^^ | ^^^^^^^^^^
@ -431,7 +432,7 @@ fn assignment_backtick_failure() {
a = `exit 222`", a = `exit 222`",
222, 222,
"", "",
"backtick failed with exit code 222 "error: backtick failed with exit code 222
| |
4 | a = `exit 222` 4 | a = `exit 222`
| ^^^^^^^^^^ | ^^^^^^^^^^
@ -449,7 +450,7 @@ fn unknown_override_options() {
a = `exit 222`", a = `exit 222`",
255, 255,
"", "",
"Variables `baz` and `foo` overridden on the command line but not present in justfile\n", "error: Variables `baz` and `foo` overridden on the command line but not present in justfile\n",
); );
} }
@ -463,7 +464,7 @@ fn unknown_override_args() {
a = `exit 222`", a = `exit 222`",
255, 255,
"", "",
"Variables `baz` and `foo` overridden on the command line but not present in justfile\n", "error: Variables `baz` and `foo` overridden on the command line but not present in justfile\n",
); );
} }
@ -477,7 +478,7 @@ fn unknown_override_arg() {
a = `exit 222`", a = `exit 222`",
255, 255,
"", "",
"Variable `foo` overridden on the command line but not present in justfile\n", "error: Variable `foo` overridden on the command line but not present in justfile\n",
); );
} }
@ -786,10 +787,9 @@ foo A B:
", ",
255, 255,
"", "",
"Recipe `foo` got 3 arguments but only takes 2\n" "error: Recipe `foo` got 3 arguments but only takes 2\n",
); );
} }
#[test] #[test]
fn argument_mismatch_fewer() { fn argument_mismatch_fewer() {
integration_test( integration_test(
@ -800,7 +800,7 @@ foo A B:
", ",
255, 255,
"", "",
"Recipe `foo` got 1 argument but takes 2\n" "error: Recipe `foo` got 1 argument but takes 2\n"
); );
} }
@ -811,7 +811,7 @@ fn unknown_recipe() {
"hello:", "hello:",
255, 255,
"", "",
"Justfile does not contain recipe `foo`\n", "error: Justfile does not contain recipe `foo`\n",
); );
} }
@ -822,6 +822,32 @@ fn unknown_recipes() {
"hello:", "hello:",
255, 255,
"", "",
"Justfile does not contain recipes `foo` or `bar`\n", "error: Justfile does not contain recipes `foo` or `bar`\n",
); );
} }
#[test]
fn colors_with_context() {
integration_test(
&["--color", "always"],
"b = a\na = `exit 100`\nbar:\n echo '{{`exit 200`}}'",
100,
"",
"\u{1b}[1;31merror:\u{1b}[0m \u{1b}[1mbacktick failed with exit code 100\n\u{1b}[0m |\n2 | a = `exit 100`\n | \u{1b}[1;31m^^^^^^^^^^\u{1b}[0m\n",
);
}
#[test]
fn colors_no_context() {
let text ="
recipe:
@exit 100";
integration_test(
&["--color=always"],
text,
100,
"",
"\u{1b}[1;31merror:\u{1b}[0m \u{1b}[1mRecipe `recipe` failed with exit code 100\u{1b}[0m\n",
);
}

View File

@ -13,6 +13,7 @@ extern crate lazy_static;
extern crate regex; extern crate regex;
extern crate tempdir; extern crate tempdir;
extern crate itertools; extern crate itertools;
extern crate ansi_term;
use std::io::prelude::*; use std::io::prelude::*;
@ -136,17 +137,23 @@ fn error_from_signal(recipe: &str, exit_status: process::ExitStatus) -> RunError
} }
#[cfg(unix)] #[cfg(unix)]
fn backtick_error_from_signal(exit_status: process::ExitStatus) -> RunError<'static> { fn backtick_error_from_signal<'a>(
token: &Token<'a>,
exit_status: process::ExitStatus
) -> RunError<'a> {
use std::os::unix::process::ExitStatusExt; use std::os::unix::process::ExitStatusExt;
match exit_status.signal() { match exit_status.signal() {
Some(signal) => RunError::BacktickSignal{signal: signal}, Some(signal) => RunError::BacktickSignal{token: token.clone(), signal: signal},
None => RunError::BacktickUnknownFailure, None => RunError::BacktickUnknownFailure{token: token.clone()},
} }
} }
#[cfg(windows)] #[cfg(windows)]
fn backtick_error_from_signal(exit_status: process::ExitStatus) -> RunError<'static> { fn backtick_error_from_signal<'a>(
RunError::BacktickUnknownFailure token: &Token<'a>,
exit_status: process::ExitStatus
) -> RunError<'a> {
RunError::BacktickUnknownFailure{token: token.clone()}
} }
fn export_env<'a>( fn export_env<'a>(
@ -197,10 +204,10 @@ fn run_backtick<'a>(
}); });
} }
} else { } else {
return Err(backtick_error_from_signal(output.status)); return Err(backtick_error_from_signal(token, output.status));
} }
match std::str::from_utf8(&output.stdout) { match std::str::from_utf8(&output.stdout) {
Err(error) => Err(RunError::BacktickUtf8Error{utf8_error: error}), Err(error) => Err(RunError::BacktickUtf8Error{token: token.clone(), utf8_error: error}),
Ok(utf8) => { Ok(utf8) => {
Ok(if utf8.ends_with('\n') { Ok(if utf8.ends_with('\n') {
&utf8[0..utf8.len()-1] &utf8[0..utf8.len()-1]
@ -212,7 +219,7 @@ fn run_backtick<'a>(
} }
} }
} }
Err(error) => Err(RunError::BacktickIoError{io_error: error}), Err(error) => Err(RunError::BacktickIoError{token: token.clone(), io_error: error}),
} }
} }
@ -289,7 +296,8 @@ impl<'a> Recipe<'a> {
// make the script executable // make the script executable
let current_mode = perms.mode(); let current_mode = perms.mode();
perms.set_mode(current_mode | 0o100); perms.set_mode(current_mode | 0o100);
try!(fs::set_permissions(&path, perms).map_err(|error| RunError::TmpdirIoError{recipe: self.name, io_error: error})); try!(fs::set_permissions(&path, perms)
.map_err(|error| RunError::TmpdirIoError{recipe: self.name, io_error: error}));
// run it! // run it!
let mut command = process::Command::new(path); let mut command = process::Command::new(path);
@ -373,7 +381,8 @@ impl<'a> Display for Recipe<'a> {
} }
match *piece { match *piece {
Fragment::Text{ref text} => try!(write!(f, "{}", text.lexeme)), Fragment::Text{ref text} => try!(write!(f, "{}", text.lexeme)),
Fragment::Expression{ref expression, ..} => try!(write!(f, "{}{}{}", "{{", expression, "}}")), Fragment::Expression{ref expression, ..} =>
try!(write!(f, "{}{}{}", "{{", expression, "}}")),
} }
} }
if i + 1 < self.lines.len() { if i + 1 < self.lines.len() {
@ -765,27 +774,87 @@ fn conjoin<T: Display>(
Ok(()) Ok(())
} }
fn write_error_context(
f: &mut fmt::Formatter,
text: &str,
index: usize,
line: usize,
column: usize,
width: Option<usize>,
) -> Result<(), fmt::Error> {
let line_number = line + 1;
let red = maybe_red(f.alternate());
match text.lines().nth(line) {
Some(line) => {
let line_number_width = line_number.to_string().len();
try!(write!(f, "{0:1$} |\n", "", line_number_width));
try!(write!(f, "{} | {}\n", line_number, line));
try!(write!(f, "{0:1$} |", "", line_number_width));
try!(write!(f, " {0:1$}{2}{3:^<4$}{5}", "", column,
red.prefix(), "", width.unwrap_or(1), red.suffix()));
},
None => if index != text.len() {
try!(write!(f, "internal error: Error has invalid line number: {}", line_number))
},
}
Ok(())
}
fn write_token_error_context(f: &mut fmt::Formatter, token: &Token) -> Result<(), fmt::Error> {
write_error_context(
f,
token.text,
token.index,
token.line,
token.column + token.prefix.len(),
Some(token.lexeme.len())
)
}
fn maybe_red(colors: bool) -> ansi_term::Style {
if colors {
ansi_term::Style::new().fg(ansi_term::Color::Red).bold()
} else {
ansi_term::Style::default()
}
}
fn maybe_bold(colors: bool) -> ansi_term::Style {
if colors {
ansi_term::Style::new().bold()
} else {
ansi_term::Style::default()
}
}
impl<'a> Display for Error<'a> { impl<'a> Display for Error<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
try!(write!(f, "error: ")); let red = maybe_red(f.alternate());
let bold = maybe_bold(f.alternate());
try!(write!(f, "{} {}", red.paint("error:"), bold.prefix()));
match self.kind { match self.kind {
ErrorKind::CircularRecipeDependency{recipe, ref circle} => { ErrorKind::CircularRecipeDependency{recipe, ref circle} => {
if circle.len() == 2 { if circle.len() == 2 {
try!(write!(f, "recipe `{}` depends on itself", recipe)); try!(write!(f, "recipe `{}` depends on itself", recipe));
} else { } else {
try!(writeln!(f, "recipe `{}` has circular dependency `{}`", recipe, circle.join(" -> "))); try!(writeln!(f, "recipe `{}` has circular dependency `{}`",
recipe, circle.join(" -> ")));
} }
} }
ErrorKind::CircularVariableDependency{variable, ref circle} => { ErrorKind::CircularVariableDependency{variable, ref circle} => {
if circle.len() == 2 { if circle.len() == 2 {
try!(writeln!(f, "variable `{}` depends on its own value: `{}`", variable, circle.join(" -> "))); try!(writeln!(f, "variable `{}` depends on its own value: `{}`",
variable, circle.join(" -> ")));
} else { } else {
try!(writeln!(f, "variable `{}` depends on its own value: `{}`", variable, circle.join(" -> "))); try!(writeln!(f, "variable `{}` depends on its own value: `{}`",
variable, circle.join(" -> ")));
} }
} }
ErrorKind::InvalidEscapeSequence{character} => { ErrorKind::InvalidEscapeSequence{character} => {
try!(writeln!(f, "`\\{}` is not a valid escape sequence", character.escape_default().collect::<String>())); try!(writeln!(f, "`\\{}` is not a valid escape sequence",
character.escape_default().collect::<String>()));
} }
ErrorKind::DuplicateParameter{recipe, parameter} => { ErrorKind::DuplicateParameter{recipe, parameter} => {
try!(writeln!(f, "recipe `{}` has duplicate parameter `{}`", recipe, parameter)); try!(writeln!(f, "recipe `{}` has duplicate parameter `{}`", recipe, parameter));
@ -804,14 +873,16 @@ impl<'a> Display for Error<'a> {
recipe, first, self.line)); recipe, first, self.line));
} }
ErrorKind::DependencyHasParameters{recipe, dependency} => { ErrorKind::DependencyHasParameters{recipe, dependency} => {
try!(writeln!(f, "recipe `{}` depends on `{}` which requires arguments. dependencies may not require arguments", recipe, dependency)); try!(writeln!(f, "recipe `{}` depends on `{}` which requires arguments. \
dependencies may not require arguments", recipe, dependency));
} }
ErrorKind::ParameterShadowsVariable{parameter} => { ErrorKind::ParameterShadowsVariable{parameter} => {
try!(writeln!(f, "parameter `{}` shadows variable of the same name", parameter)); try!(writeln!(f, "parameter `{}` shadows variable of the same name", parameter));
} }
ErrorKind::MixedLeadingWhitespace{whitespace} => { ErrorKind::MixedLeadingWhitespace{whitespace} => {
try!(writeln!(f, try!(writeln!(f,
"found a mix of tabs and spaces in leading whitespace: `{}`\n leading whitespace may consist of tabs or spaces, but not both", "found a mix of tabs and spaces in leading whitespace: `{}`\n\
leading whitespace may consist of tabs or spaces, but not both",
show_whitespace(whitespace) show_whitespace(whitespace)
)); ));
} }
@ -840,23 +911,15 @@ impl<'a> Display for Error<'a> {
try!(writeln!(f, "unterminated string")); try!(writeln!(f, "unterminated string"));
} }
ErrorKind::InternalError{ref message} => { ErrorKind::InternalError{ref message} => {
try!(writeln!(f, "internal error, this may indicate a bug in just: {}\n consider filing an issue: https://github.com/casey/just/issues/new", message)); try!(writeln!(f, "internal error, this may indicate a bug in just: {}\n\
consider filing an issue: https://github.com/casey/just/issues/new",
message));
} }
} }
match self.text.lines().nth(self.line) { try!(write!(f, "{}", bold.suffix()));
Some(line) => {
let displayed_line = self.line + 1; try!(write_error_context(f, self.text, self.index, self.line, self.column, self.width));
let line_number_width = displayed_line.to_string().len();
try!(write!(f, "{0:1$} |\n", "", line_number_width));
try!(write!(f, "{} | {}\n", displayed_line, line));
try!(write!(f, "{0:1$} |", "", line_number_width));
try!(write!(f, " {0:1$}{2:^<3$}", "", self.column, "", self.width.unwrap_or(1)));
},
None => if self.index != self.text.len() {
try!(write!(f, "internal error: Error has invalid line number: {}", self.line + 1))
},
};
Ok(()) Ok(())
} }
@ -1018,15 +1081,18 @@ enum RunError<'a> {
UnknownFailure{recipe: &'a str}, UnknownFailure{recipe: &'a str},
UnknownRecipes{recipes: Vec<&'a str>}, UnknownRecipes{recipes: Vec<&'a str>},
UnknownOverrides{overrides: Vec<&'a str>}, UnknownOverrides{overrides: Vec<&'a str>},
BacktickCode{code: i32, token: Token<'a>}, BacktickCode{token: Token<'a>, code: i32},
BacktickIoError{io_error: io::Error}, BacktickIoError{token: Token<'a>, io_error: io::Error},
BacktickSignal{signal: i32}, BacktickSignal{token: Token<'a>, signal: i32},
BacktickUtf8Error{utf8_error: std::str::Utf8Error}, BacktickUtf8Error{token: Token<'a>, utf8_error: std::str::Utf8Error},
BacktickUnknownFailure, BacktickUnknownFailure{token: Token<'a>},
} }
impl<'a> Display for RunError<'a> { impl<'a> Display for RunError<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
let red = maybe_red(f.alternate());
try!(write!(f, "{} ", red.paint("error:")));
match *self { match *self {
RunError::UnknownRecipes{ref recipes} => { RunError::UnknownRecipes{ref recipes} => {
try!(write!(f, "Justfile does not contain recipe{} {}", try!(write!(f, "Justfile does not contain recipe{} {}",
@ -1039,7 +1105,8 @@ impl<'a> Display for RunError<'a> {
And(&overrides.iter().map(Tick).collect::<Vec<_>>()))) And(&overrides.iter().map(Tick).collect::<Vec<_>>())))
}, },
RunError::NonLeadingRecipeWithParameters{recipe} => { RunError::NonLeadingRecipeWithParameters{recipe} => {
try!(write!(f, "Recipe `{}` takes arguments and so must be the first and only recipe specified on the command line", recipe)); try!(write!(f, "Recipe `{}` takes arguments and so must be the first and only recipe \
specified on the command line", recipe));
}, },
RunError::ArgumentCountMismatch{recipe, found, expected} => { RunError::ArgumentCountMismatch{recipe, found, expected} => {
try!(write!(f, "Recipe `{}` got {} argument{} but {}takes {}", try!(write!(f, "Recipe `{}` got {} argument{} but {}takes {}",
@ -1047,58 +1114,61 @@ impl<'a> Display for RunError<'a> {
if expected < found { "only " } else { "" }, expected)); if expected < found { "only " } else { "" }, expected));
}, },
RunError::Code{recipe, code} => { RunError::Code{recipe, code} => {
try!(write!(f, "Recipe \"{}\" failed with exit code {}", recipe, code)); try!(write!(f, "Recipe `{}` failed with exit code {}", recipe, code));
}, },
RunError::Signal{recipe, signal} => { RunError::Signal{recipe, signal} => {
try!(write!(f, "Recipe \"{}\" wast terminated by signal {}", recipe, signal)); try!(write!(f, "Recipe `{}` wast terminated by signal {}", recipe, signal));
} }
RunError::UnknownFailure{recipe} => { RunError::UnknownFailure{recipe} => {
try!(write!(f, "Recipe \"{}\" failed for an unknown reason", recipe)); try!(write!(f, "Recipe `{}` failed for an unknown reason", recipe));
}, },
RunError::IoError{recipe, ref io_error} => { RunError::IoError{recipe, ref io_error} => {
try!(match io_error.kind() { try!(match io_error.kind() {
io::ErrorKind::NotFound => write!(f, "Recipe \"{}\" could not be run because just could not find `sh` the command:\n{}", recipe, io_error), io::ErrorKind::NotFound => write!(f,
io::ErrorKind::PermissionDenied => write!(f, "Recipe \"{}\" could not be run because just could not run `sh`:\n{}", recipe, io_error), "Recipe `{}` could not be run because just could not find `sh` the command:\n{}",
_ => write!(f, "Recipe \"{}\" could not be run because of an IO error while launching `sh`:\n{}", recipe, io_error), recipe, io_error),
io::ErrorKind::PermissionDenied => write!(
f, "Recipe `{}` could not be run because just could not run `sh`:\n{}",
recipe, io_error),
_ => write!(f, "Recipe `{}` could not be run because of an IO error while \
launching `sh`:\n{}", recipe, io_error),
}); });
}, },
RunError::TmpdirIoError{recipe, ref io_error} => RunError::TmpdirIoError{recipe, ref io_error} =>
try!(write!(f, "Recipe \"{}\" could not be run because of an IO error while trying to create a temporary directory or write a file to that directory`:\n{}", recipe, io_error)), try!(write!(f, "Recipe `{}` could not be run because of an IO error while trying \
to create a temporary directory or write a file to that directory`:\n{}",
recipe, io_error)),
RunError::BacktickCode{code, ref token} => { RunError::BacktickCode{code, ref token} => {
try!(write!(f, "backtick failed with exit code {}\n", code)); try!(write!(f, "backtick failed with exit code {}\n", code));
match token.text.lines().nth(token.line) { try!(write_token_error_context(f, token));
Some(line) => {
let displayed_line = token.line + 1;
let line_number_width = displayed_line.to_string().len();
try!(write!(f, "{0:1$} |\n", "", line_number_width));
try!(write!(f, "{} | {}\n", displayed_line, line));
try!(write!(f, "{0:1$} |", "", line_number_width));
try!(write!(f, " {0:1$}{2:^<3$}", "",
token.column + token.prefix.len(), "", token.lexeme.len()));
},
None => if token.index != token.text.len() {
try!(write!(f, "internal error: Error has invalid line number: {}", token.line + 1))
},
} }
} RunError::BacktickSignal{ref token, signal} => {
RunError::BacktickSignal{signal} => {
try!(write!(f, "backtick was terminated by signal {}", signal)); try!(write!(f, "backtick was terminated by signal {}", signal));
try!(write_token_error_context(f, token));
} }
RunError::BacktickUnknownFailure => { RunError::BacktickUnknownFailure{ref token} => {
try!(write!(f, "backtick failed for an uknown reason")); try!(write!(f, "backtick failed for an uknown reason"));
try!(write_token_error_context(f, token));
} }
RunError::BacktickIoError{ref io_error} => { RunError::BacktickIoError{ref token, ref io_error} => {
try!(match io_error.kind() { try!(match io_error.kind() {
io::ErrorKind::NotFound => write!(f, "backtick could not be run because just could not find `sh` the command:\n{}", io_error), io::ErrorKind::NotFound => write!(
io::ErrorKind::PermissionDenied => write!(f, "backtick could not be run because just could not run `sh`:\n{}", io_error), f, "backtick could not be run because just could not find `sh` the command:\n{}",
_ => write!(f, "backtick could not be run because of an IO error while launching `sh`:\n{}", io_error), io_error),
io::ErrorKind::PermissionDenied => write!(
f, "backtick could not be run because just could not run `sh`:\n{}", io_error),
_ => write!(f, "backtick could not be run because of an IO \
error while launching `sh`:\n{}", io_error),
}); });
try!(write_token_error_context(f, token));
} }
RunError::BacktickUtf8Error{ref utf8_error} => { RunError::BacktickUtf8Error{ref token, ref utf8_error} => {
try!(write!(f, "backtick succeeded but stdout was not utf8: {}", utf8_error)); try!(write!(f, "backtick succeeded but stdout was not utf8: {}", utf8_error));
try!(write_token_error_context(f, token));
} }
RunError::InternalError{ref message} => { RunError::InternalError{ref message} => {
try!(write!(f, "internal error, this may indicate a bug in just: {}\n consider filing an issue: https://github.com/casey/just/issues/new", message)); try!(write!(f, "internal error, this may indicate a bug in just: {}
consider filing an issue: https://github.com/casey/just/issues/new", message));
} }
} }
@ -1303,7 +1373,8 @@ fn tokenize(text: &str) -> Result<Vec<Token>, Error> {
} }
let (prefix, lexeme, kind) = let (prefix, lexeme, kind) =
if let (0, &State::Indent(indent), Some(captures)) = (column, state.last().unwrap(), LINE.captures(rest)) { if let (0, &State::Indent(indent), Some(captures)) =
(column, state.last().unwrap(), LINE.captures(rest)) {
let line = captures.at(0).unwrap(); let line = captures.at(0).unwrap();
if !line.starts_with(indent) { if !line.starts_with(indent) {
return error!(ErrorKind::InternalError{message: "unexpected indent".to_string()}); return error!(ErrorKind::InternalError{message: "unexpected indent".to_string()});