Ignore file name case while searching for justfile (#436)
This commit is contained in:
parent
24311b7c0b
commit
7f06bc68d4
@ -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:
|
||||
@ -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`:
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
|
@ -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>>;
|
||||
|
@ -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;
|
||||
|
54
src/run.rs
54
src/run.rs
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match env::current_dir() {
|
||||
Ok(pathbuf) => {
|
||||
if pathbuf.as_os_str() == "/" {
|
||||
die!("No justfile found.");
|
||||
}
|
||||
}
|
||||
Err(error) => die!("Error getting current dir: {}", error),
|
||||
}
|
||||
|
||||
if let Err(error) = env::set_current_dir("..") {
|
||||
die!("Error changing directory: {}", 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(¤t_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));
|
||||
|
||||
text =
|
||||
fs::read_to_string(name).unwrap_or_else(|error| die!("Error reading justfile: {}", error));
|
||||
let parent = name.parent().unwrap();
|
||||
|
||||
if let Err(error) = env::set_current_dir(&parent) {
|
||||
die!(
|
||||
"Error changing directory to {}: {}",
|
||||
parent.display(),
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(search_error) => die!("{}", search_error),
|
||||
}
|
||||
}
|
||||
|
||||
let justfile = Parser::parse(&text).unwrap_or_else(|error| {
|
||||
|
163
src/search.rs
Normal file
163
src/search.rs
Normal file
@ -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
src/search_error.rs
Normal file
62
src/search_error.rs
Normal file
@ -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`"
|
||||
)
|
||||
}
|
||||
}
|
@ -62,38 +62,6 @@ fn test_capitalized_justfile_search() {
|
||||
search_test(path, &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capitalization_priority() {
|
||||
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("justfile");
|
||||
fs::write(&path, "default:\n\techo ok").unwrap();
|
||||
path.pop();
|
||||
path.push("Justfile");
|
||||
fs::write(&path, "default:\n\techo fail").unwrap();
|
||||
path.pop();
|
||||
|
||||
// if we see "default\n\techo fail" in `justfile` then we're running
|
||||
// in a case insensitive filesystem, so just bail
|
||||
path.push("justfile");
|
||||
if fs::read_to_string(&path).unwrap() == "default:\n\techo fail" {
|
||||
return;
|
||||
}
|
||||
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");
|
||||
path.push("c");
|
||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
||||
path.push("d");
|
||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
||||
|
||||
search_test(path, &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upwards_path_argument() {
|
||||
let tmp = TempDir::new("just-test-justfile-search")
|
||||
|
@ -5,7 +5,7 @@ use tempdir::TempDir;
|
||||
/// Test that just runs with the correct working directory when invoked with
|
||||
/// `--justfile` but not `--working-directory`
|
||||
#[test]
|
||||
fn justfile_without_working_directory() -> Result<(), Box<Error>> {
|
||||
fn justfile_without_working_directory() -> Result<(), Box<dyn Error>> {
|
||||
let tmp = TempDir::new("just-integration")?;
|
||||
let justfile = tmp.path().join("justfile");
|
||||
let data = tmp.path().join("data");
|
||||
@ -30,3 +30,35 @@ fn justfile_without_working_directory() -> Result<(), Box<Error>> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that just invokes commands from the directory in which the justfile is found
|
||||
#[test]
|
||||
fn change_working_directory_to_justfile_parent() -> Result<(), Box<dyn Error>> {
|
||||
let tmp = TempDir::new("just-integration")?;
|
||||
|
||||
let justfile = tmp.path().join("justfile");
|
||||
fs::write(
|
||||
&justfile,
|
||||
"foo = `cat data`\ndefault:\n echo {{foo}}\n cat data",
|
||||
)?;
|
||||
|
||||
let data = tmp.path().join("data");
|
||||
fs::write(&data, "found it")?;
|
||||
|
||||
let subdir = tmp.path().join("subdir");
|
||||
fs::create_dir(&subdir)?;
|
||||
|
||||
let output = Command::new(executable_path("just"))
|
||||
.current_dir(subdir)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
panic!("just invocation failed: {}", output.status)
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
assert_eq!(stdout, "found it\nfound it");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user