Implement invocation_directory function (#312)
This commit is contained in:
parent
ee7302c0e3
commit
cf3fde442f
18
README.adoc
18
README.adoc
@ -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.
|
||||
|
||||
==== 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
|
||||
|
||||
`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.
|
||||
|
4
justfile
4
justfile
@ -141,6 +141,10 @@ ruby:
|
||||
#!/usr/bin/env ruby
|
||||
puts "Hello from ruby!"
|
||||
|
||||
# Print working directory, for demonstration purposes!
|
||||
pwd:
|
||||
echo {{invocation_directory()}}
|
||||
|
||||
# Local Variables:
|
||||
# mode: makefile
|
||||
# End:
|
||||
|
@ -1,9 +1,12 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use common::*;
|
||||
|
||||
use brev;
|
||||
|
||||
pub struct AssignmentEvaluator<'a: 'b, 'b> {
|
||||
pub assignments: &'b Map<&'a str, Expression<'a>>,
|
||||
pub invocation_directory: &'b Result<PathBuf, String>,
|
||||
pub dotenv: &'b Map<String, String>,
|
||||
pub dry_run: bool,
|
||||
pub evaluated: Map<&'a str, String>,
|
||||
@ -17,6 +20,7 @@ pub struct AssignmentEvaluator<'a: 'b, 'b> {
|
||||
impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
pub fn evaluate_assignments(
|
||||
assignments: &Map<&'a str, Expression<'a>>,
|
||||
invocation_directory: &Result<PathBuf, String>,
|
||||
dotenv: &'b Map<String, String>,
|
||||
overrides: &Map<&str, &str>,
|
||||
quiet: bool,
|
||||
@ -28,6 +32,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
exports: &empty(),
|
||||
scope: &empty(),
|
||||
assignments,
|
||||
invocation_directory,
|
||||
dotenv,
|
||||
dry_run,
|
||||
overrides,
|
||||
@ -107,6 +112,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||
self.evaluate_expression(argument, arguments)
|
||||
}).collect::<Result<Vec<String>, RuntimeError>>()?;
|
||||
let context = FunctionContext {
|
||||
invocation_directory: &self.invocation_directory,
|
||||
dotenv: self.dotenv,
|
||||
};
|
||||
evaluate_function(token, name, &context, &call_arguments)
|
||||
@ -161,10 +167,14 @@ mod test {
|
||||
use testing::parse_success;
|
||||
use Configuration;
|
||||
|
||||
fn no_cwd_err() -> Result<PathBuf, String> {
|
||||
Err(String::from("no cwd in tests"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backtick_code() {
|
||||
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)} => {
|
||||
assert_eq!(code, 100);
|
||||
assert_eq!(token.lexeme, "`f() { return 100; }; f`");
|
||||
@ -187,7 +197,7 @@ recipe:
|
||||
..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(_)} => {
|
||||
assert_eq!(token.lexeme, "`echo $exported_variable`");
|
||||
},
|
||||
|
@ -1,6 +1,10 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use common::*;
|
||||
use target;
|
||||
|
||||
use platform::{Platform, PlatformInterface};
|
||||
|
||||
lazy_static! {
|
||||
static ref FUNCTIONS: Map<&'static str, Function> = vec![
|
||||
("arch", Function::Nullary(arch )),
|
||||
@ -8,6 +12,7 @@ lazy_static! {
|
||||
("os_family", Function::Nullary(os_family )),
|
||||
("env_var", Function::Unary (env_var )),
|
||||
("env_var_or_default", Function::Binary (env_var_or_default)),
|
||||
("invocation_directory", Function::Nullary(invocation_directory)),
|
||||
].into_iter().collect();
|
||||
}
|
||||
|
||||
@ -29,6 +34,7 @@ impl Function {
|
||||
}
|
||||
|
||||
pub struct FunctionContext<'a> {
|
||||
pub invocation_directory: &'a Result<PathBuf, 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())
|
||||
}
|
||||
|
||||
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> {
|
||||
use std::env::VarError::*;
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use common::*;
|
||||
|
||||
use edit_distance::edit_distance;
|
||||
@ -42,6 +44,7 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||
|
||||
pub fn run(
|
||||
&'a self,
|
||||
invocation_directory: Result<PathBuf, String>,
|
||||
arguments: &[&'a str],
|
||||
configuration: &Configuration<'a>,
|
||||
) -> RunResult<'a, ()> {
|
||||
@ -57,6 +60,7 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||
|
||||
let scope = AssignmentEvaluator::evaluate_assignments(
|
||||
&self.assignments,
|
||||
&invocation_directory,
|
||||
&dotenv,
|
||||
&configuration.overrides,
|
||||
configuration.quiet,
|
||||
@ -115,7 +119,7 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||
|
||||
let mut ran = empty();
|
||||
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(())
|
||||
@ -123,6 +127,7 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||
|
||||
fn run_recipe<'c>(
|
||||
&'c self,
|
||||
invocation_directory: &Result<PathBuf, String>,
|
||||
recipe: &Recipe<'a>,
|
||||
arguments: &[&'a str],
|
||||
scope: &Map<&'c str, String>,
|
||||
@ -132,10 +137,10 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
|
||||
) -> RunResult<()> {
|
||||
for dependency_name in &recipe.dependencies {
|
||||
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);
|
||||
Ok(())
|
||||
}
|
||||
@ -171,9 +176,13 @@ mod test {
|
||||
use testing::parse_success;
|
||||
use RuntimeError::*;
|
||||
|
||||
fn no_cwd_err() -> Result<PathBuf, String> {
|
||||
Err(String::from("no cwd in tests"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
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} => {
|
||||
assert_eq!(recipes, &["x", "y", "z"]);
|
||||
assert_eq!(suggestion, None);
|
||||
@ -201,7 +210,7 @@ a:
|
||||
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} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(code, 200);
|
||||
@ -214,7 +223,7 @@ a:
|
||||
#[test]
|
||||
fn code_error() {
|
||||
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} => {
|
||||
assert_eq!(recipe, "fail");
|
||||
assert_eq!(code, 100);
|
||||
@ -230,7 +239,7 @@ a:
|
||||
a return code:
|
||||
@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} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(code, 150);
|
||||
@ -242,7 +251,7 @@ a return code:
|
||||
|
||||
#[test]
|
||||
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} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(found, 2);
|
||||
@ -255,7 +264,7 @@ a return code:
|
||||
|
||||
#[test]
|
||||
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} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(found, 2);
|
||||
@ -269,7 +278,7 @@ a return code:
|
||||
#[test]
|
||||
fn missing_all_arguments() {
|
||||
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} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(found, 0);
|
||||
@ -282,7 +291,7 @@ a return code:
|
||||
|
||||
#[test]
|
||||
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} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(found, 1);
|
||||
@ -295,7 +304,7 @@ a return code:
|
||||
|
||||
#[test]
|
||||
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} => {
|
||||
assert_eq!(recipe, "a");
|
||||
assert_eq!(found, 0);
|
||||
@ -312,7 +321,7 @@ a return code:
|
||||
configuration.overrides.insert("foo", "bar");
|
||||
configuration.overrides.insert("baz", "bob");
|
||||
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} => {
|
||||
assert_eq!(overrides, &["baz", "foo"]);
|
||||
},
|
||||
@ -337,7 +346,7 @@ wut:
|
||||
..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} => {
|
||||
assert_eq!(recipe, "wut");
|
||||
assert_eq!(line_number, Some(8));
|
||||
|
@ -15,8 +15,12 @@ pub trait PlatformInterface {
|
||||
|
||||
/// 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>;
|
||||
|
||||
/// Translate a path from a "native" path to a path the interpreter expects
|
||||
fn to_shell_path(path: &Path) -> Result<String, String>;
|
||||
}
|
||||
|
||||
|
||||
#[cfg(unix)]
|
||||
impl PlatformInterface for Platform {
|
||||
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;
|
||||
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)]
|
||||
@ -75,4 +85,12 @@ impl PlatformInterface for Platform {
|
||||
// from a windows process exit status, so just return 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))
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use common::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::{ExitStatus, Command, Stdio};
|
||||
|
||||
use platform::{Platform, PlatformInterface};
|
||||
@ -50,6 +51,7 @@ impl<'a> Recipe<'a> {
|
||||
|
||||
pub fn run(
|
||||
&self,
|
||||
invocation_directory: &Result<PathBuf, String>,
|
||||
arguments: &[&'a str],
|
||||
scope: &Map<&'a str, String>,
|
||||
dotenv: &Map<String, String>,
|
||||
@ -86,6 +88,7 @@ impl<'a> Recipe<'a> {
|
||||
|
||||
let mut evaluator = AssignmentEvaluator {
|
||||
assignments: &empty(),
|
||||
invocation_directory,
|
||||
dry_run: configuration.dry_run,
|
||||
evaluated: empty(),
|
||||
overrides: &empty(),
|
||||
|
@ -47,6 +47,9 @@ pub fn run() {
|
||||
#[cfg(windows)]
|
||||
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"))
|
||||
.version(concat!("v", env!("CARGO_PKG_VERSION")))
|
||||
.author(env!("CARGO_PKG_AUTHORS"))
|
||||
@ -354,7 +357,11 @@ pub fn run() {
|
||||
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 color.stderr().active() {
|
||||
eprintln!("{:#}", run_error);
|
||||
|
77
tests/invocation_directory.rs
Normal file
77
tests/invocation_directory.rs
Normal 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");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user