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[这里],快看过来!)
|
(非官方中文文档,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
|
```make
|
||||||
build:
|
build:
|
||||||
@ -126,7 +126,9 @@ another-recipe:
|
|||||||
@echo 'This is 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`:
|
Running `just` with no arguments runs the first recipe in the `justfile`:
|
||||||
|
|
||||||
@ -744,7 +746,7 @@ if exists("did_load_filetypes")
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
augroup filetypedetect
|
augroup filetypedetect
|
||||||
au BufNewFile,BufRead Justfile,justfile setf make
|
au BufNewFile,BufRead justfile setf make
|
||||||
augroup END
|
augroup END
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -20,41 +20,27 @@ pub(crate) use log::warn;
|
|||||||
pub(crate) use tempdir::TempDir;
|
pub(crate) use tempdir::TempDir;
|
||||||
pub(crate) use unicode_width::UnicodeWidthChar;
|
pub(crate) use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
pub(crate) use crate::search;
|
||||||
|
|
||||||
|
// Functions
|
||||||
pub(crate) use crate::{
|
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,
|
load_dotenv::load_dotenv,
|
||||||
misc::{default, empty},
|
misc::{default, empty},
|
||||||
parameter::Parameter,
|
};
|
||||||
parser::Parser,
|
|
||||||
position::Position,
|
// Structs and enums
|
||||||
recipe::Recipe,
|
pub(crate) use crate::{
|
||||||
recipe_context::RecipeContext,
|
alias::Alias, alias_resolver::AliasResolver, assignment_evaluator::AssignmentEvaluator,
|
||||||
recipe_resolver::RecipeResolver,
|
assignment_resolver::AssignmentResolver, color::Color, compilation_error::CompilationError,
|
||||||
runtime_error::RuntimeError,
|
compilation_error_kind::CompilationErrorKind, configuration::Configuration,
|
||||||
shebang::Shebang,
|
expression::Expression, fragment::Fragment, function::Function,
|
||||||
state::State,
|
function_context::FunctionContext, functions::Functions, interrupt_guard::InterruptGuard,
|
||||||
string_literal::StringLiteral,
|
interrupt_handler::InterruptHandler, justfile::Justfile, lexer::Lexer, parameter::Parameter,
|
||||||
token::Token,
|
parser::Parser, position::Position, recipe::Recipe, recipe_context::RecipeContext,
|
||||||
token_kind::TokenKind,
|
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, search_error::SearchError,
|
||||||
use_color::UseColor,
|
shebang::Shebang, state::State, string_literal::StringLiteral, token::Token,
|
||||||
variables::Variables,
|
token_kind::TokenKind, use_color::UseColor, variables::Variables, verbosity::Verbosity,
|
||||||
verbosity::Verbosity,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type CompilationResult<'a, T> = Result<T, CompilationError<'a>>;
|
pub type CompilationResult<'a, T> = Result<T, CompilationError<'a>>;
|
||||||
|
@ -43,6 +43,8 @@ mod recipe_context;
|
|||||||
mod recipe_resolver;
|
mod recipe_resolver;
|
||||||
mod run;
|
mod run;
|
||||||
mod runtime_error;
|
mod runtime_error;
|
||||||
|
mod search;
|
||||||
|
mod search_error;
|
||||||
mod shebang;
|
mod shebang;
|
||||||
mod state;
|
mod state;
|
||||||
mod string_literal;
|
mod string_literal;
|
||||||
|
56
src/run.rs
56
src/run.rs
@ -273,44 +273,30 @@ pub fn run() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let name;
|
let current_dir = match env::current_dir() {
|
||||||
'outer: loop {
|
Ok(current_dir) => current_dir,
|
||||||
for candidate in &["justfile", "Justfile"] {
|
Err(io_error) => die!("Error getting current dir: {}", io_error),
|
||||||
match fs::metadata(candidate) {
|
};
|
||||||
Ok(metadata) => {
|
match search::justfile(¤t_dir) {
|
||||||
if metadata.is_file() {
|
Ok(name) => {
|
||||||
name = *candidate;
|
if matches.is_present("EDIT") {
|
||||||
break 'outer;
|
edit(name);
|
||||||
}
|
}
|
||||||
}
|
text = fs::read_to_string(&name)
|
||||||
Err(error) => {
|
.unwrap_or_else(|error| die!("Error reading justfile: {}", error));
|
||||||
if error.kind() != io::ErrorKind::NotFound {
|
|
||||||
die!("Error fetching justfile metadata: {}", 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),
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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| {
|
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, &[]);
|
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]
|
#[test]
|
||||||
fn test_upwards_path_argument() {
|
fn test_upwards_path_argument() {
|
||||||
let tmp = TempDir::new("just-test-justfile-search")
|
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
|
/// Test that just runs with the correct working directory when invoked with
|
||||||
/// `--justfile` but not `--working-directory`
|
/// `--justfile` but not `--working-directory`
|
||||||
#[test]
|
#[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 tmp = TempDir::new("just-integration")?;
|
||||||
let justfile = tmp.path().join("justfile");
|
let justfile = tmp.path().join("justfile");
|
||||||
let data = tmp.path().join("data");
|
let data = tmp.path().join("data");
|
||||||
@ -30,3 +30,35 @@ fn justfile_without_working_directory() -> Result<(), Box<Error>> {
|
|||||||
|
|
||||||
Ok(())
|
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