Implement invocation_directory function (#312)

This commit is contained in:
Joshua Warner 2018-06-19 10:04:03 -07:00 committed by Casey Rodarmor
parent ee7302c0e3
commit cf3fde442f
9 changed files with 175 additions and 17 deletions

View File

@ -289,6 +289,24 @@ This is an x86_64 machine
- `env_var_or_default(key, default)` Retrieves the environment variable with name `key`, returning `default` if it is not present. - `env_var_or_default(key, default)` Retrieves the environment variable with name `key`, returning `default` if it is not present.
==== Invocation Directory
- `invocation_directory()` - Retrieves the path of the current working directory, before `just` changed it (chdir'd) prior to executing commands.
For example, to call `rustfmt` on files just under the "current directory" (from the user/invoker's perspective), use the following rule:
```
rustfmt:
find {{invocation_directory()}} -name \*.rs -exec rustfmt {} \;
```
Alternatively, if your command needs to be run from the current directory, you could use (e.g.):
```
build:
cd {{invocation_directory()}}; ./some_script_that_needs_to_be_run_from_here
```
==== Dotenv Integration ==== Dotenv Integration
`just` will load environment variables from a file named `.env`. This file can be located in the same directory as your justfile or in a parent directory. These variables are environment variables, not `just` variables, and so must be accessed using `$VARIABLE_NAME` in recipes and backticks. `just` will load environment variables from a file named `.env`. This file can be located in the same directory as your justfile or in a parent directory. These variables are environment variables, not `just` variables, and so must be accessed using `$VARIABLE_NAME` in recipes and backticks.

View File

@ -141,6 +141,10 @@ ruby:
#!/usr/bin/env ruby #!/usr/bin/env ruby
puts "Hello from ruby!" puts "Hello from ruby!"
# Print working directory, for demonstration purposes!
pwd:
echo {{invocation_directory()}}
# Local Variables: # Local Variables:
# mode: makefile # mode: makefile
# End: # End:

View File

@ -1,9 +1,12 @@
use std::path::PathBuf;
use common::*; use common::*;
use brev; use brev;
pub struct AssignmentEvaluator<'a: 'b, 'b> { pub struct AssignmentEvaluator<'a: 'b, 'b> {
pub assignments: &'b Map<&'a str, Expression<'a>>, pub assignments: &'b Map<&'a str, Expression<'a>>,
pub invocation_directory: &'b Result<PathBuf, String>,
pub dotenv: &'b Map<String, String>, pub dotenv: &'b Map<String, String>,
pub dry_run: bool, pub dry_run: bool,
pub evaluated: Map<&'a str, String>, pub evaluated: Map<&'a str, String>,
@ -17,6 +20,7 @@ pub struct AssignmentEvaluator<'a: 'b, 'b> {
impl<'a, 'b> AssignmentEvaluator<'a, 'b> { impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
pub fn evaluate_assignments( pub fn evaluate_assignments(
assignments: &Map<&'a str, Expression<'a>>, assignments: &Map<&'a str, Expression<'a>>,
invocation_directory: &Result<PathBuf, String>,
dotenv: &'b Map<String, String>, dotenv: &'b Map<String, String>,
overrides: &Map<&str, &str>, overrides: &Map<&str, &str>,
quiet: bool, quiet: bool,
@ -28,6 +32,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
exports: &empty(), exports: &empty(),
scope: &empty(), scope: &empty(),
assignments, assignments,
invocation_directory,
dotenv, dotenv,
dry_run, dry_run,
overrides, overrides,
@ -107,6 +112,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
self.evaluate_expression(argument, arguments) self.evaluate_expression(argument, arguments)
}).collect::<Result<Vec<String>, RuntimeError>>()?; }).collect::<Result<Vec<String>, RuntimeError>>()?;
let context = FunctionContext { let context = FunctionContext {
invocation_directory: &self.invocation_directory,
dotenv: self.dotenv, dotenv: self.dotenv,
}; };
evaluate_function(token, name, &context, &call_arguments) evaluate_function(token, name, &context, &call_arguments)
@ -161,10 +167,14 @@ mod test {
use testing::parse_success; use testing::parse_success;
use Configuration; use Configuration;
fn no_cwd_err() -> Result<PathBuf, String> {
Err(String::from("no cwd in tests"))
}
#[test] #[test]
fn backtick_code() { fn backtick_code() {
match parse_success("a:\n echo {{`f() { return 100; }; f`}}") match parse_success("a:\n echo {{`f() { return 100; }; f`}}")
.run(&["a"], &Default::default()).unwrap_err() { .run(no_cwd_err(), &["a"], &Default::default()).unwrap_err() {
RuntimeError::Backtick{token, output_error: OutputError::Code(code)} => { RuntimeError::Backtick{token, output_error: OutputError::Code(code)} => {
assert_eq!(code, 100); assert_eq!(code, 100);
assert_eq!(token.lexeme, "`f() { return 100; }; f`"); assert_eq!(token.lexeme, "`f() { return 100; }; f`");
@ -187,7 +197,7 @@ recipe:
..Default::default() ..Default::default()
}; };
match parse_success(text).run(&["recipe"], &configuration).unwrap_err() { match parse_success(text).run(no_cwd_err(), &["recipe"], &configuration).unwrap_err() {
RuntimeError::Backtick{token, output_error: OutputError::Code(_)} => { RuntimeError::Backtick{token, output_error: OutputError::Code(_)} => {
assert_eq!(token.lexeme, "`echo $exported_variable`"); assert_eq!(token.lexeme, "`echo $exported_variable`");
}, },

View File

@ -1,6 +1,10 @@
use std::path::PathBuf;
use common::*; use common::*;
use target; use target;
use platform::{Platform, PlatformInterface};
lazy_static! { lazy_static! {
static ref FUNCTIONS: Map<&'static str, Function> = vec![ static ref FUNCTIONS: Map<&'static str, Function> = vec![
("arch", Function::Nullary(arch )), ("arch", Function::Nullary(arch )),
@ -8,6 +12,7 @@ lazy_static! {
("os_family", Function::Nullary(os_family )), ("os_family", Function::Nullary(os_family )),
("env_var", Function::Unary (env_var )), ("env_var", Function::Unary (env_var )),
("env_var_or_default", Function::Binary (env_var_or_default)), ("env_var_or_default", Function::Binary (env_var_or_default)),
("invocation_directory", Function::Nullary(invocation_directory)),
].into_iter().collect(); ].into_iter().collect();
} }
@ -29,6 +34,7 @@ impl Function {
} }
pub struct FunctionContext<'a> { pub struct FunctionContext<'a> {
pub invocation_directory: &'a Result<PathBuf, String>,
pub dotenv: &'a Map<String, String>, pub dotenv: &'a Map<String, String>,
} }
@ -92,6 +98,12 @@ pub fn os_family(_context: &FunctionContext) -> Result<String, String> {
Ok(target::os_family().to_string()) Ok(target::os_family().to_string())
} }
pub fn invocation_directory(context: &FunctionContext) -> Result<String, String> {
context.invocation_directory.clone()
.and_then(|s| Platform::to_shell_path(&s)
.map_err(|e| format!("Error getting shell path: {}", e)))
}
pub fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> { pub fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> {
use std::env::VarError::*; use std::env::VarError::*;

View File

@ -1,3 +1,5 @@
use std::path::PathBuf;
use common::*; use common::*;
use edit_distance::edit_distance; use edit_distance::edit_distance;
@ -42,6 +44,7 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
pub fn run( pub fn run(
&'a self, &'a self,
invocation_directory: Result<PathBuf, String>,
arguments: &[&'a str], arguments: &[&'a str],
configuration: &Configuration<'a>, configuration: &Configuration<'a>,
) -> RunResult<'a, ()> { ) -> RunResult<'a, ()> {
@ -57,6 +60,7 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
let scope = AssignmentEvaluator::evaluate_assignments( let scope = AssignmentEvaluator::evaluate_assignments(
&self.assignments, &self.assignments,
&invocation_directory,
&dotenv, &dotenv,
&configuration.overrides, &configuration.overrides,
configuration.quiet, configuration.quiet,
@ -115,7 +119,7 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
let mut ran = empty(); let mut ran = empty();
for (recipe, arguments) in grouped { for (recipe, arguments) in grouped {
self.run_recipe(recipe, arguments, &scope, &dotenv, configuration, &mut ran)? self.run_recipe(&invocation_directory, recipe, arguments, &scope, &dotenv, configuration, &mut ran)?
} }
Ok(()) Ok(())
@ -123,6 +127,7 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
fn run_recipe<'c>( fn run_recipe<'c>(
&'c self, &'c self,
invocation_directory: &Result<PathBuf, String>,
recipe: &Recipe<'a>, recipe: &Recipe<'a>,
arguments: &[&'a str], arguments: &[&'a str],
scope: &Map<&'c str, String>, scope: &Map<&'c str, String>,
@ -132,10 +137,10 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
) -> RunResult<()> { ) -> RunResult<()> {
for dependency_name in &recipe.dependencies { for dependency_name in &recipe.dependencies {
if !ran.contains(dependency_name) { if !ran.contains(dependency_name) {
self.run_recipe(&self.recipes[dependency_name], &[], scope, dotenv, configuration, ran)?; self.run_recipe(invocation_directory, &self.recipes[dependency_name], &[], scope, dotenv, configuration, ran)?;
} }
} }
recipe.run(arguments, scope, dotenv, &self.exports, configuration)?; recipe.run(invocation_directory, arguments, scope, dotenv, &self.exports, configuration)?;
ran.insert(recipe.name); ran.insert(recipe.name);
Ok(()) Ok(())
} }
@ -171,9 +176,13 @@ mod test {
use testing::parse_success; use testing::parse_success;
use RuntimeError::*; use RuntimeError::*;
fn no_cwd_err() -> Result<PathBuf, String> {
Err(String::from("no cwd in tests"))
}
#[test] #[test]
fn unknown_recipes() { fn unknown_recipes() {
match parse_success("a:\nb:\nc:").run(&["a", "x", "y", "z"], &Default::default()).unwrap_err() { match parse_success("a:\nb:\nc:").run(no_cwd_err(), &["a", "x", "y", "z"], &Default::default()).unwrap_err() {
UnknownRecipes{recipes, suggestion} => { UnknownRecipes{recipes, suggestion} => {
assert_eq!(recipes, &["x", "y", "z"]); assert_eq!(recipes, &["x", "y", "z"]);
assert_eq!(suggestion, None); assert_eq!(suggestion, None);
@ -201,7 +210,7 @@ a:
x x
"; ";
match parse_success(text).run(&["a"], &Default::default()).unwrap_err() { match parse_success(text).run(no_cwd_err(), &["a"], &Default::default()).unwrap_err() {
Code{recipe, line_number, code} => { Code{recipe, line_number, code} => {
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(code, 200); assert_eq!(code, 200);
@ -214,7 +223,7 @@ a:
#[test] #[test]
fn code_error() { fn code_error() {
match parse_success("fail:\n @exit 100") match parse_success("fail:\n @exit 100")
.run(&["fail"], &Default::default()).unwrap_err() { .run(no_cwd_err(), &["fail"], &Default::default()).unwrap_err() {
Code{recipe, line_number, code} => { Code{recipe, line_number, code} => {
assert_eq!(recipe, "fail"); assert_eq!(recipe, "fail");
assert_eq!(code, 100); assert_eq!(code, 100);
@ -230,7 +239,7 @@ a:
a return code: a return code:
@x() { {{return}} {{code + "0"}}; }; x"#; @x() { {{return}} {{code + "0"}}; }; x"#;
match parse_success(text).run(&["a", "return", "15"], &Default::default()).unwrap_err() { match parse_success(text).run(no_cwd_err(), &["a", "return", "15"], &Default::default()).unwrap_err() {
Code{recipe, line_number, code} => { Code{recipe, line_number, code} => {
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(code, 150); assert_eq!(code, 150);
@ -242,7 +251,7 @@ a return code:
#[test] #[test]
fn missing_some_arguments() { fn missing_some_arguments() {
match parse_success("a b c d:").run(&["a", "b", "c"], &Default::default()).unwrap_err() { match parse_success("a b c d:").run(no_cwd_err(), &["a", "b", "c"], &Default::default()).unwrap_err() {
ArgumentCountMismatch{recipe, found, min, max} => { ArgumentCountMismatch{recipe, found, min, max} => {
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(found, 2); assert_eq!(found, 2);
@ -255,7 +264,7 @@ a return code:
#[test] #[test]
fn missing_some_arguments_variadic() { fn missing_some_arguments_variadic() {
match parse_success("a b c +d:").run(&["a", "B", "C"], &Default::default()).unwrap_err() { match parse_success("a b c +d:").run(no_cwd_err(), &["a", "B", "C"], &Default::default()).unwrap_err() {
ArgumentCountMismatch{recipe, found, min, max} => { ArgumentCountMismatch{recipe, found, min, max} => {
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(found, 2); assert_eq!(found, 2);
@ -269,7 +278,7 @@ a return code:
#[test] #[test]
fn missing_all_arguments() { fn missing_all_arguments() {
match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}") match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}")
.run(&["a"], &Default::default()).unwrap_err() { .run(no_cwd_err(), &["a"], &Default::default()).unwrap_err() {
ArgumentCountMismatch{recipe, found, min, max} => { ArgumentCountMismatch{recipe, found, min, max} => {
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(found, 0); assert_eq!(found, 0);
@ -282,7 +291,7 @@ a return code:
#[test] #[test]
fn missing_some_defaults() { fn missing_some_defaults() {
match parse_success("a b c d='hello':").run(&["a", "b"], &Default::default()).unwrap_err() { match parse_success("a b c d='hello':").run(no_cwd_err(), &["a", "b"], &Default::default()).unwrap_err() {
ArgumentCountMismatch{recipe, found, min, max} => { ArgumentCountMismatch{recipe, found, min, max} => {
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(found, 1); assert_eq!(found, 1);
@ -295,7 +304,7 @@ a return code:
#[test] #[test]
fn missing_all_defaults() { fn missing_all_defaults() {
match parse_success("a b c='r' d='h':").run(&["a"], &Default::default()).unwrap_err() { match parse_success("a b c='r' d='h':").run(no_cwd_err(), &["a"], &Default::default()).unwrap_err() {
ArgumentCountMismatch{recipe, found, min, max} => { ArgumentCountMismatch{recipe, found, min, max} => {
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(found, 0); assert_eq!(found, 0);
@ -312,7 +321,7 @@ a return code:
configuration.overrides.insert("foo", "bar"); configuration.overrides.insert("foo", "bar");
configuration.overrides.insert("baz", "bob"); configuration.overrides.insert("baz", "bob");
match parse_success("a:\n echo {{`f() { return 100; }; f`}}") match parse_success("a:\n echo {{`f() { return 100; }; f`}}")
.run(&["a"], &configuration).unwrap_err() { .run(no_cwd_err(), &["a"], &configuration).unwrap_err() {
UnknownOverrides{overrides} => { UnknownOverrides{overrides} => {
assert_eq!(overrides, &["baz", "foo"]); assert_eq!(overrides, &["baz", "foo"]);
}, },
@ -337,7 +346,7 @@ wut:
..Default::default() ..Default::default()
}; };
match parse_success(text).run(&["wut"], &configuration).unwrap_err() { match parse_success(text).run(no_cwd_err(), &["wut"], &configuration).unwrap_err() {
Code{code: _, line_number, recipe} => { Code{code: _, line_number, recipe} => {
assert_eq!(recipe, "wut"); assert_eq!(recipe, "wut");
assert_eq!(line_number, Some(8)); assert_eq!(line_number, Some(8));

View File

@ -15,8 +15,12 @@ pub trait PlatformInterface {
/// Extract the signal from a process exit status, if it was terminated by a signal /// Extract the signal from a process exit status, if it was terminated by a signal
fn signal_from_exit_status(exit_status: process::ExitStatus) -> Option<i32>; fn signal_from_exit_status(exit_status: process::ExitStatus) -> Option<i32>;
/// Translate a path from a "native" path to a path the interpreter expects
fn to_shell_path(path: &Path) -> Result<String, String>;
} }
#[cfg(unix)] #[cfg(unix)]
impl PlatformInterface for Platform { impl PlatformInterface for Platform {
fn make_shebang_command(path: &Path, _command: &str, _argument: Option<&str>) fn make_shebang_command(path: &Path, _command: &str, _argument: Option<&str>)
@ -44,6 +48,12 @@ impl PlatformInterface for Platform {
use std::os::unix::process::ExitStatusExt; use std::os::unix::process::ExitStatusExt;
exit_status.signal() exit_status.signal()
} }
fn to_shell_path(path: &Path) -> Result<String, String> {
path.to_str().map(str::to_string)
.ok_or_else(|| String::from(
"Error getting current directory: unicode decode error"))
}
} }
#[cfg(windows)] #[cfg(windows)]
@ -75,4 +85,12 @@ impl PlatformInterface for Platform {
// from a windows process exit status, so just return None // from a windows process exit status, so just return None
None None
} }
fn to_shell_path(path: &Path) -> Result<String, String> {
// Translate path from windows style to unix style
let mut cygpath = Command::new("cygpath");
cygpath.arg("--unix");
cygpath.arg(path);
brev::output(cygpath).map_err(|e| format!("Error converting shell path: {}", e))
}
} }

View File

@ -1,5 +1,6 @@
use common::*; use common::*;
use std::path::PathBuf;
use std::process::{ExitStatus, Command, Stdio}; use std::process::{ExitStatus, Command, Stdio};
use platform::{Platform, PlatformInterface}; use platform::{Platform, PlatformInterface};
@ -50,6 +51,7 @@ impl<'a> Recipe<'a> {
pub fn run( pub fn run(
&self, &self,
invocation_directory: &Result<PathBuf, String>,
arguments: &[&'a str], arguments: &[&'a str],
scope: &Map<&'a str, String>, scope: &Map<&'a str, String>,
dotenv: &Map<String, String>, dotenv: &Map<String, String>,
@ -86,6 +88,7 @@ impl<'a> Recipe<'a> {
let mut evaluator = AssignmentEvaluator { let mut evaluator = AssignmentEvaluator {
assignments: &empty(), assignments: &empty(),
invocation_directory,
dry_run: configuration.dry_run, dry_run: configuration.dry_run,
evaluated: empty(), evaluated: empty(),
overrides: &empty(), overrides: &empty(),

View File

@ -47,6 +47,9 @@ pub fn run() {
#[cfg(windows)] #[cfg(windows)]
enable_ansi_support().ok(); enable_ansi_support().ok();
let invocation_directory = env::current_dir()
.map_err(|e| format!("Error getting current directory: {}", e));
let matches = App::new(env!("CARGO_PKG_NAME")) let matches = App::new(env!("CARGO_PKG_NAME"))
.version(concat!("v", env!("CARGO_PKG_VERSION"))) .version(concat!("v", env!("CARGO_PKG_VERSION")))
.author(env!("CARGO_PKG_AUTHORS")) .author(env!("CARGO_PKG_AUTHORS"))
@ -354,7 +357,11 @@ pub fn run() {
overrides, overrides,
}; };
if let Err(run_error) = justfile.run(&arguments, &configuration) { if let Err(run_error) = justfile.run(
invocation_directory,
&arguments,
&configuration)
{
if !configuration.quiet { if !configuration.quiet {
if color.stderr().active() { if color.stderr().active() {
eprintln!("{:#}", run_error); eprintln!("{:#}", run_error);

View File

@ -0,0 +1,77 @@
extern crate brev;
extern crate executable_path;
extern crate libc;
extern crate target;
extern crate tempdir;
use executable_path::executable_path;
use std::process;
use std::str;
use std::path::Path;
use tempdir::TempDir;
#[cfg(unix)]
fn to_shell_path(path: &Path) -> String {
use std::fs;
fs::canonicalize(path).expect("canonicalize failed")
.to_str().map(str::to_string).expect("unicode decode failed")
}
#[cfg(windows)]
fn to_shell_path(path: &Path) -> String {
// Translate path from windows style to unix style
let mut cygpath = process::Command::new("cygpath");
cygpath.arg("--unix");
cygpath.arg(path);
brev::output(cygpath).expect("converting cygwin path failed")
}
#[test]
fn test_invocation_directory() {
let tmp = TempDir::new("just-integration")
.unwrap_or_else(
|err| panic!("integration test: failed to create temporary directory: {}", err));
let mut justfile_path = tmp.path().to_path_buf();
justfile_path.push("justfile");
brev::dump(justfile_path, "default:\n @cd {{invocation_directory()}}\n @echo {{invocation_directory()}}");
let mut subdir = tmp.path().to_path_buf();
subdir.push("subdir");
brev::mkdir(&subdir);
let output = process::Command::new(&executable_path("just"))
.current_dir(&subdir)
.args(&["--shell", "sh"])
.output()
.expect("just invocation failed");
let mut failure = false;
let expected_status = 0;
let expected_stdout =
to_shell_path(&subdir) + "\n";
let expected_stderr = "";
let status = output.status.code().unwrap();
if status != expected_status {
println!("bad status: {} != {}", status, expected_status);
failure = true;
}
let stdout = str::from_utf8(&output.stdout).unwrap();
if stdout != expected_stdout {
println!("bad stdout:\ngot:\n{:?}\n\nexpected:\n{:?}", stdout, expected_stdout);
failure = true;
}
let stderr = str::from_utf8(&output.stderr).unwrap();
if stderr != expected_stderr {
println!("bad stderr:\ngot:\n{:?}\n\nexpected:\n{:?}", stderr, expected_stderr);
failure = true;
}
if failure {
panic!("test failed");
}
}