Skip to content

Commit

Permalink
Implement invocation_directory function (#312)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuawarner32 authored and casey committed Jun 19, 2018
1 parent ee7302c commit cf3fde4
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 17 deletions.
18 changes: 18 additions & 0 deletions README.adoc
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions justfile
Expand Up @@ -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:
Expand Down
14 changes: 12 additions & 2 deletions src/assignment_evaluator.rs
@@ -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>,
Expand All @@ -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,
Expand All @@ -28,6 +32,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
exports: &empty(),
scope: &empty(),
assignments,
invocation_directory,
dotenv,
dry_run,
overrides,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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`");
Expand All @@ -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`");
},
Expand Down
12 changes: 12 additions & 0 deletions src/function.rs
@@ -1,13 +1,18 @@
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 )),
("os", Function::Nullary(os )),
("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();
}

Expand All @@ -29,6 +34,7 @@ impl Function {
}

pub struct FunctionContext<'a> {
pub invocation_directory: &'a Result<PathBuf, String>,
pub dotenv: &'a Map<String, String>,
}

Expand Down Expand Up @@ -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::*;

Expand Down
37 changes: 23 additions & 14 deletions src/justfile.rs
@@ -1,3 +1,5 @@
use std::path::PathBuf;

use common::*;

use edit_distance::edit_distance;
Expand Down Expand Up @@ -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, ()> {
Expand All @@ -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,
Expand Down Expand Up @@ -115,14 +119,15 @@ 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(())
}

fn run_recipe<'c>(
&'c self,
invocation_directory: &Result<PathBuf, String>,
recipe: &Recipe<'a>,
arguments: &[&'a str],
scope: &Map<&'c str, String>,
Expand All @@ -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(())
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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"]);
},
Expand All @@ -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));
Expand Down
18 changes: 18 additions & 0 deletions src/platform.rs
Expand Up @@ -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>)
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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))
}
}
3 changes: 3 additions & 0 deletions src/recipe.rs
@@ -1,5 +1,6 @@
use common::*;

use std::path::PathBuf;
use std::process::{ExitStatus, Command, Stdio};

use platform::{Platform, PlatformInterface};
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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(),
Expand Down
9 changes: 8 additions & 1 deletion src/run.rs
Expand Up @@ -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"))
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit cf3fde4

Please sign in to comment.