Skip to content

Commit

Permalink
Ignore file name case while searching for justfile (#436)
Browse files Browse the repository at this point in the history
  • Loading branch information
shevtsiv authored and casey committed Jun 2, 2019
1 parent 24311b7 commit 7f06bc6
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 101 deletions.
8 changes: 5 additions & 3 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ image:https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg[say thanks,link=htt

(非官方中文文档,link:https://github.com/chinanf-boy/just-zh[这里],快看过来!)

Commands are stored in a file called `justfile` or `Justfile` with syntax inspired by `make`:
Commands are stored in a file called `justfile` with syntax inspired by `make`:

```make
build:
Expand Down Expand Up @@ -126,7 +126,9 @@ another-recipe:
@echo 'This is another recipe.'
```
When you invoke `just` it looks for a `justfile` in the current directory and upwards, so you can invoke it from any subdirectory of your project.
When you invoke `just` it looks for file `justfile` in the current directory and upwards, so you can invoke it from any subdirectory of your project.
The search for a `justfile` is case insensitive, so any case, like `Justfile`, `JUSTFILE`, or `JuStFiLe`, will work.
Running `just` with no arguments runs the first recipe in the `justfile`:
Expand Down Expand Up @@ -744,7 +746,7 @@ if exists("did_load_filetypes")
endif

augroup filetypedetect
au BufNewFile,BufRead Justfile,justfile setf make
au BufNewFile,BufRead justfile setf make
augroup END
```

Expand Down
50 changes: 18 additions & 32 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,27 @@ pub(crate) use log::warn;
pub(crate) use tempdir::TempDir;
pub(crate) use unicode_width::UnicodeWidthChar;

// Modules
pub(crate) use crate::search;

// Functions
pub(crate) use crate::{
alias::Alias,
alias_resolver::AliasResolver,
assignment_evaluator::AssignmentEvaluator,
assignment_resolver::AssignmentResolver,
color::Color,
compilation_error::CompilationError,
compilation_error_kind::CompilationErrorKind,
configuration::Configuration,
expression::Expression,
fragment::Fragment,
function::Function,
function_context::FunctionContext,
functions::Functions,
interrupt_guard::InterruptGuard,
interrupt_handler::InterruptHandler,
justfile::Justfile,
lexer::Lexer,
load_dotenv::load_dotenv,
misc::{default, empty},
parameter::Parameter,
parser::Parser,
position::Position,
recipe::Recipe,
recipe_context::RecipeContext,
recipe_resolver::RecipeResolver,
runtime_error::RuntimeError,
shebang::Shebang,
state::State,
string_literal::StringLiteral,
token::Token,
token_kind::TokenKind,
use_color::UseColor,
variables::Variables,
verbosity::Verbosity,
};

// Structs and enums
pub(crate) use crate::{
alias::Alias, alias_resolver::AliasResolver, assignment_evaluator::AssignmentEvaluator,
assignment_resolver::AssignmentResolver, color::Color, compilation_error::CompilationError,
compilation_error_kind::CompilationErrorKind, configuration::Configuration,
expression::Expression, fragment::Fragment, function::Function,
function_context::FunctionContext, functions::Functions, interrupt_guard::InterruptGuard,
interrupt_handler::InterruptHandler, justfile::Justfile, lexer::Lexer, parameter::Parameter,
parser::Parser, position::Position, recipe::Recipe, recipe_context::RecipeContext,
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, search_error::SearchError,
shebang::Shebang, state::State, string_literal::StringLiteral, token::Token,
token_kind::TokenKind, use_color::UseColor, variables::Variables, verbosity::Verbosity,
};

pub type CompilationResult<'a, T> = Result<T, CompilationError<'a>>;
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ mod recipe_context;
mod recipe_resolver;
mod run;
mod runtime_error;
mod search;
mod search_error;
mod shebang;
mod state;
mod string_literal;
Expand Down
52 changes: 19 additions & 33 deletions src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,44 +273,30 @@ pub fn run() {
);
}
} else {
let name;
'outer: loop {
for candidate in &["justfile", "Justfile"] {
match fs::metadata(candidate) {
Ok(metadata) => {
if metadata.is_file() {
name = *candidate;
break 'outer;
}
}
Err(error) => {
if error.kind() != io::ErrorKind::NotFound {
die!("Error fetching justfile metadata: {}", error)
}
}
let current_dir = match env::current_dir() {
Ok(current_dir) => current_dir,
Err(io_error) => die!("Error getting current dir: {}", io_error),
};
match search::justfile(&current_dir) {
Ok(name) => {
if matches.is_present("EDIT") {
edit(name);
}
}
text = fs::read_to_string(&name)
.unwrap_or_else(|error| die!("Error reading justfile: {}", error));

match env::current_dir() {
Ok(pathbuf) => {
if pathbuf.as_os_str() == "/" {
die!("No justfile found.");
}
}
Err(error) => die!("Error getting current dir: {}", error),
}
let parent = name.parent().unwrap();

if let Err(error) = env::set_current_dir("..") {
die!("Error changing directory: {}", error);
if let Err(error) = env::set_current_dir(&parent) {
die!(
"Error changing directory to {}: {}",
parent.display(),
error
);
}
}
Err(search_error) => die!("{}", search_error),
}

if matches.is_present("EDIT") {
edit(name);
}

text =
fs::read_to_string(name).unwrap_or_else(|error| die!("Error reading justfile: {}", error));
}

let justfile = Parser::parse(&text).unwrap_or_else(|error| {
Expand Down
163 changes: 163 additions & 0 deletions src/search.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
use crate::common::*;
use std::fs;
use std::path::{Path, PathBuf};

const FILENAME: &str = "justfile";

pub fn justfile(directory: &Path) -> Result<PathBuf, SearchError> {
let mut candidates = Vec::new();
let dir = fs::read_dir(directory).map_err(|io_error| SearchError::Io {
io_error,
directory: directory.to_owned(),
})?;
for entry in dir {
let entry = entry.map_err(|io_error| SearchError::Io {
io_error,
directory: directory.to_owned(),
})?;
if let Some(name) = entry.file_name().to_str() {
if name.eq_ignore_ascii_case(FILENAME) {
candidates.push(entry.path());
}
}
}
if candidates.len() == 1 {
Ok(candidates.pop().unwrap())
} else if candidates.len() > 1 {
Err(SearchError::MultipleCandidates { candidates })
} else if let Some(parent_dir) = directory.parent() {
justfile(parent_dir)
} else {
Err(SearchError::NotFound)
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempdir::TempDir;

#[test]
fn not_found() {
let tmp = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
match search::justfile(tmp.path()) {
Err(SearchError::NotFound) => {
assert!(true);
}
_ => panic!("No justfile found error was expected"),
}
}

#[test]
fn multiple_candidates() {
let tmp = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
let mut path = tmp.path().to_path_buf();
path.push(FILENAME);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
path.push(FILENAME.to_uppercase());
if let Ok(_) = fs::File::open(path.as_path()) {
// We are in case-insensitive file system
return;
}
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
match search::justfile(path.as_path()) {
Err(SearchError::MultipleCandidates { .. }) => {
assert!(true);
}
_ => panic!("Multiple candidates error was expected"),
}
}

#[test]
fn found() {
let tmp = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
let mut path = tmp.path().to_path_buf();
path.push(FILENAME);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
match search::justfile(path.as_path()) {
Ok(_path) => {
assert!(true);
}
_ => panic!("No errors were expected"),
}
}

#[test]
fn found_spongebob_case() {
let tmp = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
let mut path = tmp.path().to_path_buf();
let spongebob_case = FILENAME
.chars()
.enumerate()
.map(|(i, c)| {
if i % 2 == 0 {
c.to_ascii_uppercase()
} else {
c
}
})
.collect::<String>();
path.push(spongebob_case);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
match search::justfile(path.as_path()) {
Ok(_path) => {
assert!(true);
}
_ => panic!("No errors were expected"),
}
}

#[test]
fn found_from_inner_dir() {
let tmp = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
let mut path = tmp.path().to_path_buf();
path.push(FILENAME);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
path.push("a");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
path.push("b");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
match search::justfile(path.as_path()) {
Ok(_path) => {
assert!(true);
}
_ => panic!("No errors were expected"),
}
}

#[test]
fn found_and_stopped_at_first_justfile() {
let tmp = TempDir::new("just-test-justfile-search")
.expect("test justfile search: failed to create temporary directory");
let mut path = tmp.path().to_path_buf();
path.push(FILENAME);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
path.push("a");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
path.push(FILENAME);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
path.push("b");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
match search::justfile(path.as_path()) {
Ok(found_path) => {
path.pop();
path.push(FILENAME);
assert_eq!(found_path, path);
}
_ => panic!("No errors were expected"),
}
}
}
62 changes: 62 additions & 0 deletions src/search_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use std::{fmt, io, path::PathBuf};

use crate::misc::And;

pub enum SearchError {
MultipleCandidates {
candidates: Vec<PathBuf>,
},
Io {
directory: PathBuf,
io_error: io::Error,
},
NotFound,
}

impl fmt::Display for SearchError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SearchError::Io {
directory,
io_error,
} => write!(
f,
"I/O error reading directory `{}`: {}",
directory.display(),
io_error
),
SearchError::MultipleCandidates { candidates } => write!(
f,
"Multiple candidate justfiles found in `{}`: {}",
candidates[0].parent().unwrap().display(),
And(
&candidates
.iter()
.map(|candidate| format!("`{}`", candidate.file_name().unwrap().to_string_lossy()))
.collect::<Vec<String>>()
),
),
SearchError::NotFound => write!(f, "No justfile found"),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn multiple_candidates_formatting() {
let error = SearchError::MultipleCandidates {
candidates: vec![
PathBuf::from("/foo/justfile"),
PathBuf::from("/foo/JUSTFILE"),
],
};

assert_eq!(
error.to_string(),
"Multiple candidate justfiles found in `/foo`: `justfile` and `JUSTFILE`"
)
}
}
Loading

0 comments on commit 7f06bc6

Please sign in to comment.