export variables done

This commit is contained in:
Casey Rodarmor 2016-10-30 16:15:18 -07:00
parent f925520101
commit 69f8e07a30
6 changed files with 217 additions and 52 deletions

16
Cargo.lock generated
View File

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

View File

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

6
notes
View File

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

View File

@ -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",
);
}

View File

@ -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<String, RunError<'a>> {
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<String, RunError<'a>> {
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<Vec<Token>, Error> {
fn parse(text: &str) -> Result<Justfile, Error> {
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<Justfile, Error> {
struct Parser<'a> {
text: &'a str,
tokens: std::iter::Peekable<std::vec::IntoIter<Token<'a>>>
tokens: itertools::PutBack<std::vec::IntoIter<Token<'a>>>
}
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<Token<'a>> {
@ -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,
})
}
}

View File

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