diff --git a/Cargo.lock b/Cargo.lock index 517608e..bb1f368 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,6 +4,7 @@ version = "0.2.8" dependencies = [ "brev 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.16.3 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -51,11 +52,24 @@ dependencies = [ "vec_map 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "either" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "glob" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "itertools" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "either 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -184,7 +198,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "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 clap 2.16.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0ab91429d96eece6d6cf8a737105f0e255fea039fed86e2118ff8d3fd69601dd" +"checksum either 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8aa2c82b7e1abd89a8a59fd89c4a51576ea76a894edf5d5b28944dd46edfed8d" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" +"checksum itertools 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ef81b0a15a9e1808cfd3ebe6a87277d29ee88b34ac1197cc7547f1dd6d9f5424" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f" "checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d" diff --git a/Cargo.toml b/Cargo.toml index 34f2ea9..04b35bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,4 @@ clap = "^2.0.0" tempdir = "^0.3.5" lazy_static = "^0.2.1" brev = "^0.1.6" +itertools = "^0.5.5" diff --git a/notes b/notes index c3ecedf..f5a4d9b 100644 --- a/notes +++ b/notes @@ -1,11 +1,7 @@ todo ---- -- allow setting and exporting environment variables - . export a as "HELLO_BAR" - . export a - . export HELLO_BAR = a - . export CC_FLAGS = "-g" +clean up export parsing - raw strings with '' - multi line strings (not in recipe interpolations) diff --git a/src/integration.rs b/src/integration.rs index f0f3e6c..119e6da 100644 --- a/src/integration.rs +++ b/src/integration.rs @@ -5,14 +5,13 @@ use tempdir::TempDir; use super::std::process::Command; fn integration_test( - name: &str, args: &[&str], justfile: &str, expected_status: i32, expected_stdout: &str, expected_stderr: &str, ) { - let tmp = TempDir::new(name) + let tmp = TempDir::new("just-integration") .unwrap_or_else(|err| panic!("tmpdir: failed to create temporary directory: {}", err)); let mut path = tmp.path().to_path_buf(); path.push("justfile"); @@ -53,7 +52,6 @@ fn integration_test( #[test] fn default() { integration_test( - "default", &[], "default:\n echo hello\nother: \n echo bar", 0, @@ -65,7 +63,6 @@ fn default() { #[test] fn quiet() { integration_test( - "quiet", &[], "default:\n @echo hello", 0, @@ -94,7 +91,6 @@ c: b echo c @mv b c"; integration_test( - "order", &["a", "d"], text, 0, @@ -111,7 +107,6 @@ a: d: c c: b"; integration_test( - "list", &["--list"], text, 0, @@ -132,7 +127,6 @@ d: c: @echo c"; integration_test( - "select", &["d", "c"], text, 0, @@ -153,7 +147,6 @@ d: c: echo c"; integration_test( - "select", &["d", "c"], text, 0, @@ -171,7 +164,6 @@ bar = hello + hello recipe: echo {{hello + "bar" + bar}}"#; integration_test( - "show", &["--show", "recipe"], text, 0, @@ -190,7 +182,6 @@ bar = hello + hello recipe: echo {{hello + "bar" + bar}}"#; integration_test( - "debug", &["--debug"], text, 0, @@ -213,7 +204,6 @@ fn status() { recipe: @function f { return 100; }; f"; integration_test( - "status", &[], text, 100, @@ -225,7 +215,6 @@ recipe: #[test] fn error() { integration_test( - "error", &[], "bar:\nhello:\nfoo: bar baaaaaaaz hello", 255, @@ -241,7 +230,6 @@ fn error() { #[test] fn backtick_success() { integration_test( - "backtick_success", &[], "a = `printf Hello,`\nbar:\n printf '{{a + `printf ' world!'`}}'", 0, @@ -253,7 +241,6 @@ fn backtick_success() { #[test] fn backtick_trimming() { integration_test( - "backtick_trimming", &[], "a = `echo Hello,`\nbar:\n echo '{{a + `echo ' world!'`}}'", 0, @@ -265,7 +252,6 @@ fn backtick_trimming() { #[test] fn backtick_code_assignment() { integration_test( - "backtick_code_assignment", &[], "b = a\na = `function f { return 100; }; f`\nbar:\n echo '{{`function f { return 200; }; f`}}'", 100, @@ -281,7 +267,6 @@ fn backtick_code_assignment() { #[test] fn backtick_code_interpolation() { integration_test( - "backtick_code_interpolation", &[], "b = a\na = `echo hello`\nbar:\n echo '{{`function f { return 200; }; f`}}'", 200, @@ -297,7 +282,6 @@ fn backtick_code_interpolation() { #[test] fn shebang_backtick_failure() { integration_test( - "shebang_backtick_failure", &[], "foo: #!/bin/sh @@ -316,7 +300,6 @@ fn shebang_backtick_failure() { #[test] fn command_backtick_failure() { integration_test( - "command_backtick_failure", &[], "foo: echo hello @@ -334,7 +317,6 @@ fn command_backtick_failure() { #[test] fn assignment_backtick_failure() { integration_test( - "assignment_backtick_failure", &[], "foo: echo hello @@ -353,7 +335,6 @@ a = `exit 222`", #[test] fn unknown_override_options() { integration_test( - "unknown_override_options", &["--set", "foo", "bar", "a", "b", "--set", "baz", "bob", "--set", "a", "b"], "foo: echo hello @@ -368,7 +349,6 @@ a = `exit 222`", #[test] fn unknown_override_args() { integration_test( - "unknown_override_args", &["foo=bar", "baz=bob", "a=b", "a", "b"], "foo: echo hello @@ -383,7 +363,6 @@ a = `exit 222`", #[test] fn overrides_first() { integration_test( - "unknown_override_args", &["foo=bar", "a=b", "recipe", "baz=bar"], r#" foo = "foo" @@ -404,7 +383,6 @@ recipe arg: #[test] fn dry_run() { integration_test( - "dry_run", &["--dry-run", "shebang", "command"], r#" var = `echo stderr 1>&2; echo backtick` @@ -436,7 +414,6 @@ echo command interpolation #[test] fn evaluate() { integration_test( - "evaluate", &["--evaluate"], r#" foo = "a\t" @@ -457,3 +434,102 @@ foo = "a " ); } +#[test] +fn export_success() { + integration_test( + &[], + r#" +export foo = "a" +baz = "c" +export bar = "b" +export abc = foo + bar + baz + +wut: + echo $foo $bar $abc +"#, + 0, + "a b abc\n", + "echo $foo $bar $abc\n", + ); +} + + +#[test] +fn export_failure() { + integration_test( + &[], + r#" +export foo = "a" +baz = "c" +export bar = "b" +export abc = foo + bar + baz + +wut: + echo $foo $bar $baz +"#, + 127, + "", + r#"echo $foo $bar $baz +sh: baz: unbound variable +Recipe "wut" failed with exit code 127 +"#, + ); +} + +#[test] +fn export_shebang() { + integration_test( + &[], + r#" +export foo = "a" +baz = "c" +export bar = "b" +export abc = foo + bar + baz + +wut: + #!/bin/sh + echo $foo $bar $abc +"#, + 0, + "a b abc\n", + "", + ); +} + +#[test] +fn export_assignment_backtick() { + integration_test( + &[], + r#" +export exported_variable = "A" +b = `echo $exported_variable` + +recipe: + echo {{b}} +"#, + 127, + "", + "sh: exported_variable: unbound variable +backtick failed with exit code 127 + | +3 | b = `echo $exported_variable` + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +", + ); +} + +#[test] +fn export_recipe_backtick() { + integration_test( + &[], + r#" +export exported_variable = "A-IS-A" + +recipe: + echo {{`echo recipe $exported_variable`}} +"#, + 0, + "recipe A-IS-A\n", + "echo recipe A-IS-A\n", + ); +} diff --git a/src/lib.rs b/src/lib.rs index c674692..cd1b474 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ pub use app::app; extern crate lazy_static; extern crate regex; extern crate tempdir; +extern crate itertools; use std::io::prelude::*; @@ -148,14 +149,39 @@ fn backtick_error_from_signal(exit_status: process::ExitStatus) -> RunError<'sta RunError::BacktickUnknownFailure } -fn run_backtick<'a>(raw: &str, token: &Token<'a>) -> Result> { - let output = process::Command::new("sh") - .arg("-cu") - .arg(raw) - .stderr(process::Stdio::inherit()) - .output(); +fn export_env<'a>( + command: &mut process::Command, + scope: &Map<&'a str, String>, + exports: &Set<&'a str>, +) -> Result<(), RunError<'a>> { + for name in exports { + if let Some(value) = scope.get(name) { + command.env(name, value); + } else { + return Err(RunError::InternalError { + message: format!("scope does not contain exported variable `{}`", name), + }); + } + } + Ok(()) +} - match output { + +fn run_backtick<'a>( + raw: &str, + token: &Token<'a>, + scope: &Map<&'a str, String>, + exports: &Set<&'a str>, +) -> Result> { + let mut cmd = process::Command::new("sh"); + + try!(export_env(&mut cmd, scope, exports)); + + cmd.arg("-cu") + .arg(raw) + .stderr(process::Stdio::inherit()); + + match cmd.output() { Ok(output) => { if let Some(code) = output.status.code() { if code != 0 { @@ -189,6 +215,7 @@ impl<'a> Recipe<'a> { &self, arguments: &[&'a str], scope: &Map<&'a str, String>, + exports: &Set<&'a str>, dry_run: bool, ) -> Result<(), RunError<'a>> { let argument_map = arguments .iter().enumerate() @@ -197,6 +224,7 @@ impl<'a> Recipe<'a> { let mut evaluator = Evaluator { evaluated: Map::new(), scope: scope, + exports: exports, assignments: &Map::new(), overrides: &Map::new(), }; @@ -257,8 +285,11 @@ impl<'a> Recipe<'a> { try!(fs::set_permissions(&path, perms).map_err(|error| RunError::TmpdirIoError{recipe: self.name, io_error: error})); // run it! - let status = process::Command::new(path).status(); - try!(match status { + let mut command = process::Command::new(path); + + try!(export_env(&mut command, scope, exports)); + + try!(match command.status() { Ok(exit_status) => if let Some(code) = exit_status.code() { if code == 0 { Ok(()) @@ -284,11 +315,14 @@ impl<'a> Recipe<'a> { if dry_run { continue; } - let status = process::Command::new("sh") - .arg("-cu") - .arg(command) - .status(); - try!(match status { + + let mut cmd = process::Command::new("sh"); + + cmd.arg("-cu").arg(command); + + try!(export_env(&mut cmd, scope, exports)); + + try!(match cmd.status() { Ok(exit_status) => if let Some(code) = exit_status.code() { if code == 0 { Ok(()) @@ -513,6 +547,7 @@ fn evaluate_assignments<'a>( let mut evaluator = Evaluator { evaluated: Map::new(), scope: &Map::new(), + exports: &Set::new(), assignments: assignments, overrides: overrides, }; @@ -527,6 +562,7 @@ fn evaluate_assignments<'a>( struct Evaluator<'a: 'b, 'b> { evaluated: Map<&'a str, String>, scope: &'b Map<&'a str, String>, + exports: &'b Set<&'a str>, assignments: &'b Map<&'a str, Expression<'a>>, overrides: &'b Map<&'b str, &'b str>, } @@ -593,7 +629,9 @@ impl<'a, 'b> Evaluator<'a, 'b> { } } Expression::String{ref cooked, ..} => cooked.clone(), - Expression::Backtick{raw, ref token} => try!(run_backtick(raw, token)), + Expression::Backtick{raw, ref token} => { + try!(run_backtick(raw, token, &self.scope, &self.exports)) + } Expression::Concatination{ref lhs, ref rhs} => { try!(self.evaluate_expression(lhs, arguments)) + @@ -790,6 +828,7 @@ impl<'a> Display for Error<'a> { struct Justfile<'a> { recipes: Map<&'a str, Recipe<'a>>, assignments: Map<&'a str, Expression<'a>>, + exports: Set<&'a str>, } impl<'a, 'b> Justfile<'a> where 'a: 'b { @@ -890,7 +929,7 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b { try!(self.run_recipe(&self.recipes[dependency_name], &[], scope, ran, dry_run)); } } - try!(recipe.run(arguments, &scope, dry_run)); + try!(recipe.run(arguments, &scope, &self.exports, dry_run)); ran.insert(recipe.name); Ok(()) } @@ -904,6 +943,9 @@ impl<'a> Display for Justfile<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { let mut items = self.recipes.len() + self.assignments.len(); for (name, expression) in &self.assignments { + if self.exports.contains(name) { + try!(write!(f, "export ")); + } try!(write!(f, "{} = {}", name, expression)); items -= 1; if items != 0 { @@ -1343,9 +1385,9 @@ fn tokenize(text: &str) -> Result, Error> { fn parse(text: &str) -> Result { let tokens = try!(tokenize(text)); let filtered: Vec<_> = tokens.into_iter().filter(|token| token.kind != Comment).collect(); - let parser = Parser{ - text: text, - tokens: filtered.into_iter().peekable() + let parser = Parser { + text: text, + tokens: itertools::put_back(filtered), }; let justfile = try!(parser.file()); Ok(justfile) @@ -1353,12 +1395,15 @@ fn parse(text: &str) -> Result { struct Parser<'a> { text: &'a str, - tokens: std::iter::Peekable>> + tokens: itertools::PutBack>> } impl<'a> Parser<'a> { fn peek(&mut self, kind: TokenKind) -> bool { - self.tokens.peek().unwrap().kind == kind + let next = self.tokens.next().unwrap(); + let result = next.kind == kind; + self.tokens.put_back(next); + result } fn accept(&mut self, kind: TokenKind) -> Option> { @@ -1560,7 +1605,28 @@ impl<'a> Parser<'a> { Some(token) => match token.kind { Eof => break, Eol => continue, - Name => if self.accepted(Equals) { + Name => if token.lexeme == "export" { + let next = self.tokens.next().unwrap(); + if next.kind == Name && self.accepted(Equals) { + if assignments.contains_key(next.lexeme) { + return Err(token.error(ErrorKind::DuplicateVariable { + variable: next.lexeme, + })); + } + exports.insert(next.lexeme); + assignments.insert(next.lexeme, try!(self.expression(false))); + assignment_tokens.insert(next.lexeme, next); + } else { + self.tokens.put_back(next); + if let Some(recipe) = recipes.get(token.lexeme) { + return Err(token.error(ErrorKind::DuplicateRecipe { + recipe: recipe.name, + first: recipe.line_number + })); + } + recipes.insert(token.lexeme, try!(self.recipe(token.lexeme, token.line))); + } + } else if self.accepted(Equals) { if assignments.contains_key(token.lexeme) { return Err(token.error(ErrorKind::DuplicateVariable { variable: token.lexeme, @@ -1569,7 +1635,7 @@ impl<'a> Parser<'a> { assignments.insert(token.lexeme, try!(self.expression(false))); assignment_tokens.insert(token.lexeme, token); } else { - if let Some(recipe) = recipes.remove(token.lexeme) { + if let Some(recipe) = recipes.get(token.lexeme) { return Err(token.error(ErrorKind::DuplicateRecipe { recipe: recipe.name, first: recipe.line_number @@ -1627,6 +1693,7 @@ impl<'a> Parser<'a> { Ok(Justfile { recipes: recipes, assignments: assignments, + exports: exports, }) } } diff --git a/src/unit.rs b/src/unit.rs index 2e83288..0412a9e 100644 --- a/src/unit.rs +++ b/src/unit.rs @@ -246,6 +246,15 @@ fn parse_empty() { ", ""); } +#[test] +fn parse_export() { + parse_summary(r#" +export a = "hello" + + "#, r#"export a = "hello""#); +} + + #[test] fn parse_complex() { parse_summary("