Reform positional argument parsing (#523)
This diff makes positional argument parsing much cleaner, along with adding a bunch of tests. Just's positional argument parsing is rather, complex, so hopefully this reform allows it to both be correct and stay correct. User-visible changes: - `just ..` is now accepted, with the same effect as `just ../` - `just .` is also accepted, with the same effect as `just` - It is now an error to pass arguments or overrides to subcommands that do not accept them, namely `--dump`, `--edit`, `--list`, `--show`, and `--summary`. It is also an error to pass arguments to `--evaluate`, although `--evaluate` does of course still accept overrides. (This is a breaking change, but hopefully worth it, as it will allow us to add arguments to subcommands which did not previously take them, if we so desire.) - Subcommands which do not accept arguments may now accept a single search-directory argument, so `just --list ../` and `just --dump foo/` are now accepted, with the former starting the search for the justfile to list in the parent directory, and the latter starting the search for the justfile to dump in `foo`.
This commit is contained in:
parent
aefdcea7d0
commit
177516bcbe
@ -7,6 +7,7 @@ pub(crate) struct AssignmentEvaluator<'a: 'b, 'b> {
|
|||||||
pub(crate) evaluated: BTreeMap<&'a str, (bool, String)>,
|
pub(crate) evaluated: BTreeMap<&'a str, (bool, String)>,
|
||||||
pub(crate) scope: &'b BTreeMap<&'a str, (bool, String)>,
|
pub(crate) scope: &'b BTreeMap<&'a str, (bool, String)>,
|
||||||
pub(crate) working_directory: &'b Path,
|
pub(crate) working_directory: &'b Path,
|
||||||
|
pub(crate) overrides: &'b BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||||
@ -15,10 +16,12 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
working_directory: &'b Path,
|
working_directory: &'b Path,
|
||||||
dotenv: &'b BTreeMap<String, String>,
|
dotenv: &'b BTreeMap<String, String>,
|
||||||
assignments: &BTreeMap<&'a str, Assignment<'a>>,
|
assignments: &BTreeMap<&'a str, Assignment<'a>>,
|
||||||
|
overrides: &BTreeMap<String, String>,
|
||||||
) -> RunResult<'a, BTreeMap<&'a str, (bool, String)>> {
|
) -> RunResult<'a, BTreeMap<&'a str, (bool, String)>> {
|
||||||
let mut evaluator = AssignmentEvaluator {
|
let mut evaluator = AssignmentEvaluator {
|
||||||
evaluated: empty(),
|
evaluated: empty(),
|
||||||
scope: &empty(),
|
scope: &empty(),
|
||||||
|
overrides,
|
||||||
config,
|
config,
|
||||||
assignments,
|
assignments,
|
||||||
working_directory,
|
working_directory,
|
||||||
@ -55,7 +58,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(assignment) = self.assignments.get(name) {
|
if let Some(assignment) = self.assignments.get(name) {
|
||||||
if let Some(value) = self.config.overrides.get(name) {
|
if let Some(value) = self.overrides.get(name) {
|
||||||
self
|
self
|
||||||
.evaluated
|
.evaluated
|
||||||
.insert(name, (assignment.export, value.to_string()));
|
.insert(name, (assignment.export, value.to_string()));
|
||||||
@ -159,47 +162,40 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::testing::{compile, config};
|
|
||||||
|
|
||||||
#[test]
|
run_error! {
|
||||||
fn backtick_code() {
|
name: backtick_code,
|
||||||
let justfile = compile("a:\n echo {{`f() { return 100; }; f`}}");
|
src: "
|
||||||
let config = config(&["a"]);
|
a:
|
||||||
let dir = env::current_dir().unwrap();
|
echo {{`f() { return 100; }; f`}}
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
",
|
||||||
RuntimeError::Backtick {
|
args: ["a"],
|
||||||
|
error: RuntimeError::Backtick {
|
||||||
token,
|
token,
|
||||||
output_error: OutputError::Code(code),
|
output_error: OutputError::Code(code),
|
||||||
} => {
|
},
|
||||||
|
check: {
|
||||||
assert_eq!(code, 100);
|
assert_eq!(code, 100);
|
||||||
assert_eq!(token.lexeme(), "`f() { return 100; }; f`");
|
assert_eq!(token.lexeme(), "`f() { return 100; }; f`");
|
||||||
}
|
}
|
||||||
other => panic!("expected a code run error, but got: {}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
run_error! {
|
||||||
fn export_assignment_backtick() {
|
name: export_assignment_backtick,
|
||||||
let text = r#"
|
src: r#"
|
||||||
export exported_variable = "A"
|
export exported_variable = "A"
|
||||||
b = `echo $exported_variable`
|
b = `echo $exported_variable`
|
||||||
|
|
||||||
recipe:
|
recipe:
|
||||||
echo {{b}}
|
echo {{b}}
|
||||||
"#;
|
"#,
|
||||||
|
args: ["--quiet", "recipe"],
|
||||||
let justfile = compile(text);
|
error: RuntimeError::Backtick {
|
||||||
let config = config(&["--quiet", "recipe"]);
|
|
||||||
let dir = env::current_dir().unwrap();
|
|
||||||
|
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
|
||||||
RuntimeError::Backtick {
|
|
||||||
token,
|
token,
|
||||||
output_error: OutputError::Code(_),
|
output_error: OutputError::Code(_),
|
||||||
} => {
|
},
|
||||||
|
check: {
|
||||||
assert_eq!(token.lexeme(), "`echo $exported_variable`");
|
assert_eq!(token.lexeme(), "`echo $exported_variable`");
|
||||||
}
|
}
|
||||||
other => panic!("expected a backtick code errror, but got: {}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,9 +57,10 @@ pub(crate) use crate::{
|
|||||||
function_context::FunctionContext, functions::Functions, interrupt_guard::InterruptGuard,
|
function_context::FunctionContext, functions::Functions, interrupt_guard::InterruptGuard,
|
||||||
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, lexer::Lexer, line::Line,
|
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, lexer::Lexer, line::Line,
|
||||||
list::List, load_error::LoadError, module::Module, name::Name, output_error::OutputError,
|
list::List, load_error::LoadError, module::Module, name::Name, output_error::OutputError,
|
||||||
parameter::Parameter, parser::Parser, platform::Platform, position::Position, recipe::Recipe,
|
parameter::Parameter, parser::Parser, platform::Platform, position::Position,
|
||||||
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
|
positional::Positional, recipe::Recipe, recipe_context::RecipeContext,
|
||||||
search::Search, search_config::SearchConfig, search_error::SearchError, shebang::Shebang,
|
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, search::Search,
|
||||||
|
search_config::SearchConfig, search_error::SearchError, shebang::Shebang,
|
||||||
show_whitespace::ShowWhitespace, state::State, string_literal::StringLiteral,
|
show_whitespace::ShowWhitespace, state::State, string_literal::StringLiteral,
|
||||||
subcommand::Subcommand, table::Table, token::Token, token_kind::TokenKind, use_color::UseColor,
|
subcommand::Subcommand, table::Table, token::Token, token_kind::TokenKind, use_color::UseColor,
|
||||||
variables::Variables, verbosity::Verbosity, warning::Warning,
|
variables::Variables, verbosity::Verbosity, warning::Warning,
|
||||||
|
362
src/config.rs
362
src/config.rs
@ -7,12 +7,10 @@ pub(crate) const DEFAULT_SHELL: &str = "sh";
|
|||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub(crate) struct Config {
|
pub(crate) struct Config {
|
||||||
pub(crate) arguments: Vec<String>,
|
|
||||||
pub(crate) color: Color,
|
pub(crate) color: Color,
|
||||||
pub(crate) dry_run: bool,
|
pub(crate) dry_run: bool,
|
||||||
pub(crate) highlight: bool,
|
pub(crate) highlight: bool,
|
||||||
pub(crate) invocation_directory: PathBuf,
|
pub(crate) invocation_directory: PathBuf,
|
||||||
pub(crate) overrides: BTreeMap<String, String>,
|
|
||||||
pub(crate) quiet: bool,
|
pub(crate) quiet: bool,
|
||||||
pub(crate) search_config: SearchConfig,
|
pub(crate) search_config: SearchConfig,
|
||||||
pub(crate) shell: String,
|
pub(crate) shell: String,
|
||||||
@ -27,6 +25,9 @@ mod cmd {
|
|||||||
pub(crate) const LIST: &str = "LIST";
|
pub(crate) const LIST: &str = "LIST";
|
||||||
pub(crate) const SHOW: &str = "SHOW";
|
pub(crate) const SHOW: &str = "SHOW";
|
||||||
pub(crate) const SUMMARY: &str = "SUMMARY";
|
pub(crate) const SUMMARY: &str = "SUMMARY";
|
||||||
|
|
||||||
|
pub(crate) const ALL: &[&str] = &[DUMP, EDIT, LIST, SHOW, SUMMARY, EVALUATE];
|
||||||
|
pub(crate) const ARGLESS: &[&str] = &[DUMP, EDIT, LIST, SHOW, SUMMARY];
|
||||||
}
|
}
|
||||||
|
|
||||||
mod arg {
|
mod arg {
|
||||||
@ -55,11 +56,6 @@ impl Config {
|
|||||||
.version_message("Print version information")
|
.version_message("Print version information")
|
||||||
.setting(AppSettings::ColoredHelp)
|
.setting(AppSettings::ColoredHelp)
|
||||||
.setting(AppSettings::TrailingVarArg)
|
.setting(AppSettings::TrailingVarArg)
|
||||||
.arg(
|
|
||||||
Arg::with_name(arg::ARGUMENTS)
|
|
||||||
.multiple(true)
|
|
||||||
.help("The recipe(s) to run, defaults to the first recipe in the justfile"),
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(arg::COLOR)
|
Arg::with_name(arg::COLOR)
|
||||||
.long("color")
|
.long("color")
|
||||||
@ -129,7 +125,7 @@ impl Config {
|
|||||||
.number_of_values(2)
|
.number_of_values(2)
|
||||||
.value_names(&["VARIABLE", "VALUE"])
|
.value_names(&["VARIABLE", "VALUE"])
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.help("Set <VARIABLE> to <VALUE>"),
|
.help("Override <VARIABLE> with <VALUE>"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(arg::SHELL)
|
Arg::with_name(arg::SHELL)
|
||||||
@ -166,15 +162,12 @@ impl Config {
|
|||||||
.help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set")
|
.help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set")
|
||||||
.requires(arg::JUSTFILE),
|
.requires(arg::JUSTFILE),
|
||||||
)
|
)
|
||||||
.group(ArgGroup::with_name("EARLY-EXIT").args(&[
|
.arg(
|
||||||
arg::ARGUMENTS,
|
Arg::with_name(arg::ARGUMENTS)
|
||||||
cmd::DUMP,
|
.multiple(true)
|
||||||
cmd::EDIT,
|
.help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"),
|
||||||
cmd::EVALUATE,
|
)
|
||||||
cmd::LIST,
|
.group(ArgGroup::with_name("SUBCOMMAND").args(cmd::ALL));
|
||||||
cmd::SHOW,
|
|
||||||
cmd::SUMMARY,
|
|
||||||
]));
|
|
||||||
|
|
||||||
if cfg!(feature = "help4help2man") {
|
if cfg!(feature = "help4help2man") {
|
||||||
app.version(env!("CARGO_PKG_VERSION")).about(concat!(
|
app.version(env!("CARGO_PKG_VERSION")).about(concat!(
|
||||||
@ -216,24 +209,6 @@ impl Config {
|
|||||||
.expect("`--color` had no value"),
|
.expect("`--color` had no value"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let subcommand = if matches.is_present(cmd::EDIT) {
|
|
||||||
Subcommand::Edit
|
|
||||||
} else if matches.is_present(cmd::SUMMARY) {
|
|
||||||
Subcommand::Summary
|
|
||||||
} else if matches.is_present(cmd::DUMP) {
|
|
||||||
Subcommand::Dump
|
|
||||||
} else if matches.is_present(cmd::LIST) {
|
|
||||||
Subcommand::List
|
|
||||||
} else if matches.is_present(cmd::EVALUATE) {
|
|
||||||
Subcommand::Evaluate
|
|
||||||
} else if let Some(name) = matches.value_of(cmd::SHOW) {
|
|
||||||
Subcommand::Show {
|
|
||||||
name: name.to_owned(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Subcommand::Run
|
|
||||||
};
|
|
||||||
|
|
||||||
let set_count = matches.occurrences_of(arg::SET);
|
let set_count = matches.occurrences_of(arg::SET);
|
||||||
let mut overrides = BTreeMap::new();
|
let mut overrides = BTreeMap::new();
|
||||||
if set_count > 0 {
|
if set_count > 0 {
|
||||||
@ -246,60 +221,17 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_override(arg: &&str) -> bool {
|
let positional = Positional::from_values(matches.values_of(arg::ARGUMENTS));
|
||||||
arg.chars().skip(1).any(|c| c == '=')
|
|
||||||
|
for (name, value) in positional.overrides {
|
||||||
|
overrides.insert(name.to_owned(), value.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
let raw_arguments: Vec<&str> = matches
|
|
||||||
.values_of(arg::ARGUMENTS)
|
|
||||||
.map(Iterator::collect)
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
for argument in raw_arguments.iter().cloned().take_while(is_override) {
|
|
||||||
let i = argument
|
|
||||||
.char_indices()
|
|
||||||
.skip(1)
|
|
||||||
.find(|&(_, c)| c == '=')
|
|
||||||
.unwrap()
|
|
||||||
.0;
|
|
||||||
|
|
||||||
let name = argument[..i].to_owned();
|
|
||||||
let value = argument[i + 1..].to_owned();
|
|
||||||
|
|
||||||
overrides.insert(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut search_directory = None;
|
|
||||||
|
|
||||||
let arguments = raw_arguments
|
|
||||||
.into_iter()
|
|
||||||
.skip_while(is_override)
|
|
||||||
.enumerate()
|
|
||||||
.flat_map(|(i, argument)| {
|
|
||||||
if i == 0 {
|
|
||||||
if let Some(i) = argument.rfind('/') {
|
|
||||||
let (dir, recipe) = argument.split_at(i + 1);
|
|
||||||
|
|
||||||
search_directory = Some(PathBuf::from(dir));
|
|
||||||
|
|
||||||
if recipe.is_empty() {
|
|
||||||
return None;
|
|
||||||
} else {
|
|
||||||
return Some(recipe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(argument)
|
|
||||||
})
|
|
||||||
.map(|argument| argument.to_owned())
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
|
|
||||||
let search_config = {
|
let search_config = {
|
||||||
let justfile = matches.value_of(arg::JUSTFILE).map(PathBuf::from);
|
let justfile = matches.value_of(arg::JUSTFILE).map(PathBuf::from);
|
||||||
let working_directory = matches.value_of(arg::WORKING_DIRECTORY).map(PathBuf::from);
|
let working_directory = matches.value_of(arg::WORKING_DIRECTORY).map(PathBuf::from);
|
||||||
|
|
||||||
if let Some(search_directory) = search_directory {
|
if let Some(search_directory) = positional.search_directory.map(PathBuf::from) {
|
||||||
if justfile.is_some() || working_directory.is_some() {
|
if justfile.is_some() || working_directory.is_some() {
|
||||||
return Err(ConfigError::SearchDirConflict);
|
return Err(ConfigError::SearchDirConflict);
|
||||||
}
|
}
|
||||||
@ -323,6 +255,54 @@ impl Config {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for subcommand in cmd::ARGLESS {
|
||||||
|
if matches.is_present(subcommand) {
|
||||||
|
match (!overrides.is_empty(), !positional.arguments.is_empty()) {
|
||||||
|
(false, false) => {}
|
||||||
|
(true, false) => {
|
||||||
|
return Err(ConfigError::SubcommandOverrides {
|
||||||
|
subcommand: format!("--{}", subcommand.to_lowercase()),
|
||||||
|
overrides,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
(false, true) => {
|
||||||
|
return Err(ConfigError::SubcommandArguments {
|
||||||
|
subcommand: format!("--{}", subcommand.to_lowercase()),
|
||||||
|
arguments: positional.arguments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
(true, true) => {
|
||||||
|
return Err(ConfigError::SubcommandOverridesAndArguments {
|
||||||
|
subcommand: format!("--{}", subcommand.to_lowercase()),
|
||||||
|
arguments: positional.arguments,
|
||||||
|
overrides,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subcommand = if matches.is_present(cmd::EDIT) {
|
||||||
|
Subcommand::Edit
|
||||||
|
} else if matches.is_present(cmd::SUMMARY) {
|
||||||
|
Subcommand::Summary
|
||||||
|
} else if matches.is_present(cmd::DUMP) {
|
||||||
|
Subcommand::Dump
|
||||||
|
} else if matches.is_present(cmd::LIST) {
|
||||||
|
Subcommand::List
|
||||||
|
} else if let Some(name) = matches.value_of(cmd::SHOW) {
|
||||||
|
Subcommand::Show {
|
||||||
|
name: name.to_owned(),
|
||||||
|
}
|
||||||
|
} else if matches.is_present(cmd::EVALUATE) {
|
||||||
|
Subcommand::Evaluate { overrides }
|
||||||
|
} else {
|
||||||
|
Subcommand::Run {
|
||||||
|
arguments: positional.arguments,
|
||||||
|
overrides,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
dry_run: matches.is_present(arg::DRY_RUN),
|
dry_run: matches.is_present(arg::DRY_RUN),
|
||||||
highlight: !matches.is_present(arg::NO_HIGHLIGHT),
|
highlight: !matches.is_present(arg::NO_HIGHLIGHT),
|
||||||
@ -333,8 +313,6 @@ impl Config {
|
|||||||
subcommand,
|
subcommand,
|
||||||
verbosity,
|
verbosity,
|
||||||
color,
|
color,
|
||||||
overrides,
|
|
||||||
arguments,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,9 +343,15 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.subcommand {
|
match &self.subcommand {
|
||||||
Dump => self.dump(justfile),
|
Dump => self.dump(justfile),
|
||||||
Run | Evaluate => self.run(justfile, &search.working_directory),
|
Evaluate { overrides } => {
|
||||||
|
self.run(justfile, &search.working_directory, overrides, &Vec::new())
|
||||||
|
}
|
||||||
|
Run {
|
||||||
|
arguments,
|
||||||
|
overrides,
|
||||||
|
} => self.run(justfile, &search.working_directory, overrides, arguments),
|
||||||
List => self.list(justfile),
|
List => self.list(justfile),
|
||||||
Show { ref name } => self.show(&name, justfile),
|
Show { ref name } => self.show(&name, justfile),
|
||||||
Summary => self.summary(justfile),
|
Summary => self.summary(justfile),
|
||||||
@ -497,12 +481,18 @@ impl Config {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(&self, justfile: Justfile, working_directory: &Path) -> Result<(), i32> {
|
fn run(
|
||||||
|
&self,
|
||||||
|
justfile: Justfile,
|
||||||
|
working_directory: &Path,
|
||||||
|
overrides: &BTreeMap<String, String>,
|
||||||
|
arguments: &Vec<String>,
|
||||||
|
) -> Result<(), i32> {
|
||||||
if let Err(error) = InterruptHandler::install() {
|
if let Err(error) = InterruptHandler::install() {
|
||||||
warn!("Failed to set CTRL-C handler: {}", error)
|
warn!("Failed to set CTRL-C handler: {}", error)
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = justfile.run(&self, working_directory);
|
let result = justfile.run(&self, working_directory, overrides, arguments);
|
||||||
|
|
||||||
if !self.quiet {
|
if !self.quiet {
|
||||||
result.eprint(self.color)
|
result.eprint(self.color)
|
||||||
@ -582,7 +572,7 @@ OPTIONS:
|
|||||||
Print colorful output [default: auto] [possible values: auto, always, never]
|
Print colorful output [default: auto] [possible values: auto, always, never]
|
||||||
|
|
||||||
-f, --justfile <JUSTFILE> Use <JUSTFILE> as justfile.
|
-f, --justfile <JUSTFILE> Use <JUSTFILE> as justfile.
|
||||||
--set <VARIABLE> <VALUE> Set <VARIABLE> to <VALUE>
|
--set <VARIABLE> <VALUE> Override <VARIABLE> with <VALUE>
|
||||||
--shell <SHELL> Invoke <SHELL> to run recipes [default: sh]
|
--shell <SHELL> Invoke <SHELL> to run recipes [default: sh]
|
||||||
-s, --show <RECIPE> Show information about <RECIPE>
|
-s, --show <RECIPE> Show information about <RECIPE>
|
||||||
-d, --working-directory <WORKING-DIRECTORY>
|
-d, --working-directory <WORKING-DIRECTORY>
|
||||||
@ -590,7 +580,7 @@ OPTIONS:
|
|||||||
|
|
||||||
|
|
||||||
ARGS:
|
ARGS:
|
||||||
<ARGUMENTS>... The recipe(s) to run, defaults to the first recipe in the justfile";
|
<ARGUMENTS>... Overrides and recipe(s) to run, defaulting to the first recipe in the justfile";
|
||||||
|
|
||||||
let app = Config::app().setting(AppSettings::ColorNever);
|
let app = Config::app().setting(AppSettings::ColorNever);
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
@ -604,11 +594,9 @@ ARGS:
|
|||||||
{
|
{
|
||||||
name: $name:ident,
|
name: $name:ident,
|
||||||
args: [$($arg:expr),*],
|
args: [$($arg:expr),*],
|
||||||
$(arguments: $arguments:expr,)?
|
|
||||||
$(color: $color:expr,)?
|
$(color: $color:expr,)?
|
||||||
$(dry_run: $dry_run:expr,)?
|
$(dry_run: $dry_run:expr,)?
|
||||||
$(highlight: $highlight:expr,)?
|
$(highlight: $highlight:expr,)?
|
||||||
$(overrides: $overrides:expr,)?
|
|
||||||
$(quiet: $quiet:expr,)?
|
$(quiet: $quiet:expr,)?
|
||||||
$(search_config: $search_config:expr,)?
|
$(search_config: $search_config:expr,)?
|
||||||
$(shell: $shell:expr,)?
|
$(shell: $shell:expr,)?
|
||||||
@ -623,14 +611,9 @@ ARGS:
|
|||||||
];
|
];
|
||||||
|
|
||||||
let want = Config {
|
let want = Config {
|
||||||
$(arguments: $arguments.iter().map(|argument| argument.to_string()).collect(),)?
|
|
||||||
$(color: $color,)?
|
$(color: $color,)?
|
||||||
$(dry_run: $dry_run,)?
|
$(dry_run: $dry_run,)?
|
||||||
$(highlight: $highlight,)?
|
$(highlight: $highlight,)?
|
||||||
$(
|
|
||||||
overrides: $overrides.iter().cloned()
|
|
||||||
.map(|(key, value): (&str, &str)| (key.to_owned(), value.to_owned())).collect(),
|
|
||||||
)?
|
|
||||||
$(quiet: $quiet,)?
|
$(quiet: $quiet,)?
|
||||||
$(search_config: $search_config,)?
|
$(search_config: $search_config,)?
|
||||||
$(shell: $shell.to_string(),)?
|
$(shell: $shell.to_string(),)?
|
||||||
@ -665,17 +648,50 @@ ARGS:
|
|||||||
$($arg,)*
|
$($arg,)*
|
||||||
];
|
];
|
||||||
|
|
||||||
error(arguments);
|
let app = Config::app();
|
||||||
|
|
||||||
|
app.get_matches_from_safe(arguments).expect_err("Expected clap error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
{
|
||||||
|
name: $name:ident,
|
||||||
|
args: [$($arg:expr),*],
|
||||||
|
error: $error:pat,
|
||||||
|
$(check: $check:block,)?
|
||||||
|
} => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let arguments = &[
|
||||||
|
"just",
|
||||||
|
$($arg,)*
|
||||||
|
];
|
||||||
|
|
||||||
|
let app = Config::app();
|
||||||
|
|
||||||
|
let matches = app.get_matches_from_safe(arguments).expect("Matching failes");
|
||||||
|
|
||||||
|
match Config::from_matches(&matches).expect_err("config parsing succeeded") {
|
||||||
|
$error => { $($check)? }
|
||||||
|
other => panic!("Unexpected config error: {}", other),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn error(arguments: &[&str]) {
|
macro_rules! map {
|
||||||
let app = Config::app();
|
{} => {
|
||||||
if let Ok(matches) = app.get_matches_from_safe(arguments) {
|
BTreeMap::new()
|
||||||
Config::from_matches(&matches).expect_err("config parsing unexpectedly succeeded");
|
};
|
||||||
} else {
|
{
|
||||||
return;
|
$($key:literal : $value:literal),* $(,)?
|
||||||
|
} => {
|
||||||
|
{
|
||||||
|
let mut map: BTreeMap<String, String> = BTreeMap::new();
|
||||||
|
$(
|
||||||
|
map.insert($key.to_owned(), $value.to_owned());
|
||||||
|
)*
|
||||||
|
map
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -787,31 +803,46 @@ ARGS:
|
|||||||
test! {
|
test! {
|
||||||
name: set_default,
|
name: set_default,
|
||||||
args: [],
|
args: [],
|
||||||
overrides: [],
|
subcommand: Subcommand::Run {
|
||||||
|
arguments: Vec::new(),
|
||||||
|
overrides: map!(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: set_one,
|
name: set_one,
|
||||||
args: ["--set", "foo", "bar"],
|
args: ["--set", "foo", "bar"],
|
||||||
overrides: [("foo", "bar")],
|
subcommand: Subcommand::Run {
|
||||||
|
arguments: Vec::new(),
|
||||||
|
overrides: map!{"foo": "bar"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: set_empty,
|
name: set_empty,
|
||||||
args: ["--set", "foo", ""],
|
args: ["--set", "foo", ""],
|
||||||
overrides: [("foo", "")],
|
subcommand: Subcommand::Run {
|
||||||
|
arguments: Vec::new(),
|
||||||
|
overrides: map!{"foo": ""},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: set_two,
|
name: set_two,
|
||||||
args: ["--set", "foo", "bar", "--set", "bar", "baz"],
|
args: ["--set", "foo", "bar", "--set", "bar", "baz"],
|
||||||
overrides: [("foo", "bar"), ("bar", "baz")],
|
subcommand: Subcommand::Run {
|
||||||
|
arguments: Vec::new(),
|
||||||
|
overrides: map!{"foo": "bar", "bar": "baz"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: set_override,
|
name: set_override,
|
||||||
args: ["--set", "foo", "bar", "--set", "foo", "baz"],
|
args: ["--set", "foo", "bar", "--set", "foo", "baz"],
|
||||||
overrides: [("foo", "baz")],
|
subcommand: Subcommand::Run {
|
||||||
|
arguments: Vec::new(),
|
||||||
|
overrides: map!{"foo": "baz"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
@ -864,7 +895,10 @@ ARGS:
|
|||||||
test! {
|
test! {
|
||||||
name: subcommand_default,
|
name: subcommand_default,
|
||||||
args: [],
|
args: [],
|
||||||
subcommand: Subcommand::Run,
|
subcommand: Subcommand::Run {
|
||||||
|
arguments: Vec::new(),
|
||||||
|
overrides: map!{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
@ -882,7 +916,9 @@ ARGS:
|
|||||||
test! {
|
test! {
|
||||||
name: subcommand_evaluate,
|
name: subcommand_evaluate,
|
||||||
args: ["--evaluate"],
|
args: ["--evaluate"],
|
||||||
subcommand: Subcommand::Evaluate,
|
subcommand: Subcommand::Evaluate {
|
||||||
|
overrides: map!{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
@ -923,31 +959,46 @@ ARGS:
|
|||||||
test! {
|
test! {
|
||||||
name: arguments,
|
name: arguments,
|
||||||
args: ["foo", "bar"],
|
args: ["foo", "bar"],
|
||||||
arguments: ["foo", "bar"],
|
subcommand: Subcommand::Run {
|
||||||
|
arguments: vec![String::from("foo"), String::from("bar")],
|
||||||
|
overrides: map!{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: arguments_leading_equals,
|
name: arguments_leading_equals,
|
||||||
args: ["=foo"],
|
args: ["=foo"],
|
||||||
arguments: ["=foo"],
|
subcommand: Subcommand::Run {
|
||||||
|
arguments: vec!["=foo".to_string()],
|
||||||
|
overrides: map!{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: overrides,
|
name: overrides,
|
||||||
args: ["foo=bar", "bar=baz"],
|
args: ["foo=bar", "bar=baz"],
|
||||||
overrides: [("foo", "bar"), ("bar", "baz")],
|
subcommand: Subcommand::Run {
|
||||||
|
arguments: Vec::new(),
|
||||||
|
overrides: map!{"foo": "bar", "bar": "baz"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: overrides_empty,
|
name: overrides_empty,
|
||||||
args: ["foo=", "bar="],
|
args: ["foo=", "bar="],
|
||||||
overrides: [("foo", ""), ("bar", "")],
|
subcommand: Subcommand::Run {
|
||||||
|
arguments: Vec::new(),
|
||||||
|
overrides: map!{"foo": "", "bar": ""},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: overrides_override_sets,
|
name: overrides_override_sets,
|
||||||
args: ["--set", "foo", "0", "--set", "bar", "1", "foo=bar", "bar=baz"],
|
args: ["--set", "foo", "0", "--set", "bar", "1", "foo=bar", "bar=baz"],
|
||||||
overrides: [("foo", "bar"), ("bar", "baz")],
|
subcommand: Subcommand::Run {
|
||||||
|
arguments: Vec::new(),
|
||||||
|
overrides: map!{"foo": "bar", "bar": "baz"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
@ -992,10 +1043,10 @@ ARGS:
|
|||||||
test! {
|
test! {
|
||||||
name: search_directory_parent_with_recipe,
|
name: search_directory_parent_with_recipe,
|
||||||
args: ["../build"],
|
args: ["../build"],
|
||||||
arguments: ["build"],
|
|
||||||
search_config: SearchConfig::FromSearchDirectory {
|
search_config: SearchConfig::FromSearchDirectory {
|
||||||
search_directory: PathBuf::from(".."),
|
search_directory: PathBuf::from(".."),
|
||||||
},
|
},
|
||||||
|
subcommand: Subcommand::Run { arguments: vec!["build".to_owned()], overrides: BTreeMap::new() },
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
@ -1017,19 +1068,92 @@ ARGS:
|
|||||||
test! {
|
test! {
|
||||||
name: search_directory_child_with_recipe,
|
name: search_directory_child_with_recipe,
|
||||||
args: ["foo/build"],
|
args: ["foo/build"],
|
||||||
arguments: ["build"],
|
|
||||||
search_config: SearchConfig::FromSearchDirectory {
|
search_config: SearchConfig::FromSearchDirectory {
|
||||||
search_directory: PathBuf::from("foo"),
|
search_directory: PathBuf::from("foo"),
|
||||||
},
|
},
|
||||||
|
subcommand: Subcommand::Run { arguments: vec!["build".to_owned()], overrides: BTreeMap::new() },
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
name: search_directory_conflict_justfile,
|
name: search_directory_conflict_justfile,
|
||||||
args: ["--justfile", "bar", "foo/build"],
|
args: ["--justfile", "bar", "foo/build"],
|
||||||
|
error: ConfigError::SearchDirConflict,
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
name: search_directory_conflict_working_directory,
|
name: search_directory_conflict_working_directory,
|
||||||
args: ["--justfile", "bar", "--working-directory", "baz", "foo/build"],
|
args: ["--justfile", "bar", "--working-directory", "baz", "foo/build"],
|
||||||
|
error: ConfigError::SearchDirConflict,
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: list_arguments,
|
||||||
|
args: ["--list", "bar"],
|
||||||
|
error: ConfigError::SubcommandArguments { subcommand, arguments },
|
||||||
|
check: {
|
||||||
|
assert_eq!(subcommand, "--list");
|
||||||
|
assert_eq!(arguments, &["bar"]);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: dump_arguments,
|
||||||
|
args: ["--dump", "bar"],
|
||||||
|
error: ConfigError::SubcommandArguments { subcommand, arguments },
|
||||||
|
check: {
|
||||||
|
assert_eq!(subcommand, "--dump");
|
||||||
|
assert_eq!(arguments, &["bar"]);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: edit_arguments,
|
||||||
|
args: ["--edit", "bar"],
|
||||||
|
error: ConfigError::SubcommandArguments { subcommand, arguments },
|
||||||
|
check: {
|
||||||
|
assert_eq!(subcommand, "--edit");
|
||||||
|
assert_eq!(arguments, &["bar"]);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: show_arguments,
|
||||||
|
args: ["--show", "foo", "bar"],
|
||||||
|
error: ConfigError::SubcommandArguments { subcommand, arguments },
|
||||||
|
check: {
|
||||||
|
assert_eq!(subcommand, "--show");
|
||||||
|
assert_eq!(arguments, &["bar"]);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: summary_arguments,
|
||||||
|
args: ["--summary", "bar"],
|
||||||
|
error: ConfigError::SubcommandArguments { subcommand, arguments },
|
||||||
|
check: {
|
||||||
|
assert_eq!(subcommand, "--summary");
|
||||||
|
assert_eq!(arguments, &["bar"]);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: subcommand_overrides_and_arguments,
|
||||||
|
args: ["--summary", "bar=baz", "bar"],
|
||||||
|
error: ConfigError::SubcommandOverridesAndArguments { subcommand, arguments, overrides },
|
||||||
|
check: {
|
||||||
|
assert_eq!(subcommand, "--summary");
|
||||||
|
assert_eq!(overrides, map!{"bar": "baz"});
|
||||||
|
assert_eq!(arguments, &["bar"]);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: summary_overrides,
|
||||||
|
args: ["--summary", "bar=baz"],
|
||||||
|
error: ConfigError::SubcommandOverrides { subcommand, overrides },
|
||||||
|
check: {
|
||||||
|
assert_eq!(subcommand, "--summary");
|
||||||
|
assert_eq!(overrides, map!{"bar": "baz"});
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,35 @@ pub(crate) enum ConfigError {
|
|||||||
"Path-prefixed recipes may not be used with `--working-directory` or `--justfile`."
|
"Path-prefixed recipes may not be used with `--working-directory` or `--justfile`."
|
||||||
))]
|
))]
|
||||||
SearchDirConflict,
|
SearchDirConflict,
|
||||||
|
#[snafu(display(
|
||||||
|
"`{}` used with unexpected arguments: {}",
|
||||||
|
subcommand,
|
||||||
|
List::and_ticked(arguments)
|
||||||
|
))]
|
||||||
|
SubcommandArguments {
|
||||||
|
subcommand: String,
|
||||||
|
arguments: Vec<String>,
|
||||||
|
},
|
||||||
|
#[snafu(display(
|
||||||
|
"`{}` used with unexpected overrides: {}; and arguments: {}",
|
||||||
|
subcommand,
|
||||||
|
List::and_ticked(overrides.iter().map(|(key, value)| format!("{}={}", key, value))),
|
||||||
|
List::and_ticked(arguments)))
|
||||||
|
]
|
||||||
|
SubcommandOverridesAndArguments {
|
||||||
|
subcommand: String,
|
||||||
|
overrides: BTreeMap<String, String>,
|
||||||
|
arguments: Vec<String>,
|
||||||
|
},
|
||||||
|
#[snafu(display(
|
||||||
|
"`{}` used with unexpected overrides: {}",
|
||||||
|
subcommand,
|
||||||
|
List::and_ticked(overrides.iter().map(|(key, value)| format!("{}={}", key, value))),
|
||||||
|
))]
|
||||||
|
SubcommandOverrides {
|
||||||
|
subcommand: String,
|
||||||
|
overrides: BTreeMap<String, String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigError {
|
impl ConfigError {
|
||||||
|
241
src/justfile.rs
241
src/justfile.rs
@ -46,13 +46,11 @@ impl<'a> Justfile<'a> {
|
|||||||
&'a self,
|
&'a self,
|
||||||
config: &'a Config,
|
config: &'a Config,
|
||||||
working_directory: &'a Path,
|
working_directory: &'a Path,
|
||||||
|
overrides: &'a BTreeMap<String, String>,
|
||||||
|
arguments: &'a Vec<String>,
|
||||||
) -> RunResult<'a, ()> {
|
) -> RunResult<'a, ()> {
|
||||||
let argvec: Vec<&str> = if !config.arguments.is_empty() {
|
let argvec: Vec<&str> = if !arguments.is_empty() {
|
||||||
config
|
arguments.iter().map(|argument| argument.as_str()).collect()
|
||||||
.arguments
|
|
||||||
.iter()
|
|
||||||
.map(|argument| argument.as_str())
|
|
||||||
.collect()
|
|
||||||
} else if let Some(recipe) = self.first() {
|
} else if let Some(recipe) = self.first() {
|
||||||
let min_arguments = recipe.min_arguments();
|
let min_arguments = recipe.min_arguments();
|
||||||
if min_arguments > 0 {
|
if min_arguments > 0 {
|
||||||
@ -68,8 +66,7 @@ impl<'a> Justfile<'a> {
|
|||||||
|
|
||||||
let arguments = argvec.as_slice();
|
let arguments = argvec.as_slice();
|
||||||
|
|
||||||
let unknown_overrides = config
|
let unknown_overrides = overrides
|
||||||
.overrides
|
|
||||||
.keys()
|
.keys()
|
||||||
.filter(|name| !self.assignments.contains_key(name.as_str()))
|
.filter(|name| !self.assignments.contains_key(name.as_str()))
|
||||||
.map(|name| name.as_str())
|
.map(|name| name.as_str())
|
||||||
@ -88,9 +85,10 @@ impl<'a> Justfile<'a> {
|
|||||||
working_directory,
|
working_directory,
|
||||||
&dotenv,
|
&dotenv,
|
||||||
&self.assignments,
|
&self.assignments,
|
||||||
|
overrides,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if config.subcommand == Subcommand::Evaluate {
|
if let Subcommand::Evaluate { .. } = config.subcommand {
|
||||||
let mut width = 0;
|
let mut width = 0;
|
||||||
for name in scope.keys() {
|
for name in scope.keys() {
|
||||||
width = cmp::max(name.len(), width);
|
width = cmp::max(name.len(), width);
|
||||||
@ -151,7 +149,7 @@ impl<'a> Justfile<'a> {
|
|||||||
|
|
||||||
let mut ran = empty();
|
let mut ran = empty();
|
||||||
for (recipe, arguments) in grouped {
|
for (recipe, arguments) in grouped {
|
||||||
self.run_recipe(&context, recipe, arguments, &dotenv, &mut ran)?
|
self.run_recipe(&context, recipe, arguments, &dotenv, &mut ran, overrides)?
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -178,14 +176,15 @@ impl<'a> Justfile<'a> {
|
|||||||
arguments: &[&'a str],
|
arguments: &[&'a str],
|
||||||
dotenv: &BTreeMap<String, String>,
|
dotenv: &BTreeMap<String, String>,
|
||||||
ran: &mut BTreeSet<&'a str>,
|
ran: &mut BTreeSet<&'a str>,
|
||||||
|
overrides: &BTreeMap<String, String>,
|
||||||
) -> RunResult<()> {
|
) -> RunResult<()> {
|
||||||
for dependency_name in &recipe.dependencies {
|
for dependency_name in &recipe.dependencies {
|
||||||
let lexeme = dependency_name.lexeme();
|
let lexeme = dependency_name.lexeme();
|
||||||
if !ran.contains(lexeme) {
|
if !ran.contains(lexeme) {
|
||||||
self.run_recipe(context, &self.recipes[lexeme], &[], dotenv, ran)?;
|
self.run_recipe(context, &self.recipes[lexeme], &[], dotenv, ran, overrides)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recipe.run(context, arguments, dotenv)?;
|
recipe.run(context, arguments, dotenv, overrides)?;
|
||||||
ran.insert(recipe.name());
|
ran.insert(recipe.name());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -226,29 +225,23 @@ impl<'a> Display for Justfile<'a> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::runtime_error::RuntimeError::*;
|
use testing::compile;
|
||||||
use crate::testing::{compile, config};
|
use RuntimeError::*;
|
||||||
|
|
||||||
#[test]
|
run_error! {
|
||||||
fn unknown_recipes() {
|
name: unknown_recipes,
|
||||||
let justfile = compile("a:\nb:\nc:");
|
src: "a:\nb:\nc:",
|
||||||
let config = config(&["a", "x", "y", "z"]);
|
args: ["a", "x", "y", "z"],
|
||||||
let dir = env::current_dir().unwrap();
|
error: UnknownRecipes {
|
||||||
|
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
|
||||||
UnknownRecipes {
|
|
||||||
recipes,
|
recipes,
|
||||||
suggestion,
|
suggestion,
|
||||||
} => {
|
},
|
||||||
|
check: {
|
||||||
assert_eq!(recipes, &["x", "y", "z"]);
|
assert_eq!(recipes, &["x", "y", "z"]);
|
||||||
assert_eq!(suggestion, None);
|
assert_eq!(suggestion, None);
|
||||||
}
|
}
|
||||||
other => panic!("unexpected error: {}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn run_shebang() {
|
|
||||||
// this test exists to make sure that shebang recipes
|
// this test exists to make sure that shebang recipes
|
||||||
// run correctly. although this script is still
|
// run correctly. although this script is still
|
||||||
// executed by a shell its behavior depends on the value of a
|
// executed by a shell its behavior depends on the value of a
|
||||||
@ -256,86 +249,79 @@ mod tests {
|
|||||||
// whereas in plain recipes variables are not available
|
// whereas in plain recipes variables are not available
|
||||||
// in subsequent lines and execution stops when a line
|
// in subsequent lines and execution stops when a line
|
||||||
// fails
|
// fails
|
||||||
let text = "
|
run_error! {
|
||||||
|
name: run_shebang,
|
||||||
|
src: "
|
||||||
a:
|
a:
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
code=200
|
code=200
|
||||||
x() { return $code; }
|
x() { return $code; }
|
||||||
x
|
x
|
||||||
x
|
x
|
||||||
";
|
",
|
||||||
let justfile = compile(text);
|
args: ["a"],
|
||||||
let config = config(&["a"]);
|
error: Code {
|
||||||
let dir = env::current_dir().unwrap();
|
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
|
||||||
Code {
|
|
||||||
recipe,
|
recipe,
|
||||||
line_number,
|
line_number,
|
||||||
code,
|
code,
|
||||||
} => {
|
},
|
||||||
|
check: {
|
||||||
assert_eq!(recipe, "a");
|
assert_eq!(recipe, "a");
|
||||||
assert_eq!(code, 200);
|
assert_eq!(code, 200);
|
||||||
assert_eq!(line_number, None);
|
assert_eq!(line_number, None);
|
||||||
}
|
}
|
||||||
other => panic!("unexpected error: {}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
run_error! {
|
||||||
fn code_error() {
|
name: code_error,
|
||||||
let justfile = compile("fail:\n @exit 100");
|
src: "
|
||||||
let config = config(&["fail"]);
|
fail:
|
||||||
let dir = env::current_dir().unwrap();
|
@exit 100
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
",
|
||||||
Code {
|
args: ["fail"],
|
||||||
|
error: Code {
|
||||||
recipe,
|
recipe,
|
||||||
line_number,
|
line_number,
|
||||||
code,
|
code,
|
||||||
} => {
|
},
|
||||||
|
check: {
|
||||||
assert_eq!(recipe, "fail");
|
assert_eq!(recipe, "fail");
|
||||||
assert_eq!(code, 100);
|
assert_eq!(code, 100);
|
||||||
assert_eq!(line_number, Some(2));
|
assert_eq!(line_number, Some(2));
|
||||||
}
|
}
|
||||||
other => panic!("unexpected error: {}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
run_error! {
|
||||||
fn run_args() {
|
name: run_args,
|
||||||
let text = r#"
|
src: r#"
|
||||||
a return code:
|
a return code:
|
||||||
@x() { {{return}} {{code + "0"}}; }; x"#;
|
@x() { {{return}} {{code + "0"}}; }; x
|
||||||
let justfile = compile(text);
|
"#,
|
||||||
let config = config(&["a", "return", "15"]);
|
args: ["a", "return", "15"],
|
||||||
let dir = env::current_dir().unwrap();
|
error: Code {
|
||||||
|
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
|
||||||
Code {
|
|
||||||
recipe,
|
recipe,
|
||||||
line_number,
|
line_number,
|
||||||
code,
|
code,
|
||||||
} => {
|
},
|
||||||
|
check: {
|
||||||
assert_eq!(recipe, "a");
|
assert_eq!(recipe, "a");
|
||||||
assert_eq!(code, 150);
|
assert_eq!(code, 150);
|
||||||
assert_eq!(line_number, Some(3));
|
assert_eq!(line_number, Some(2));
|
||||||
}
|
|
||||||
other => panic!("unexpected error: {}", other),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
run_error! {
|
||||||
fn missing_some_arguments() {
|
name: missing_some_arguments,
|
||||||
let justfile = compile("a b c d:");
|
src: "a b c d:",
|
||||||
let config = config(&["a", "b", "c"]);
|
args: ["a", "b", "c"],
|
||||||
let dir = env::current_dir().unwrap();
|
error: ArgumentCountMismatch {
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
|
||||||
ArgumentCountMismatch {
|
|
||||||
recipe,
|
recipe,
|
||||||
parameters,
|
parameters,
|
||||||
found,
|
found,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
} => {
|
},
|
||||||
|
check: {
|
||||||
let param_names = parameters
|
let param_names = parameters
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| p.name.lexeme())
|
.map(|p| p.name.lexeme())
|
||||||
@ -346,23 +332,20 @@ a return code:
|
|||||||
assert_eq!(min, 3);
|
assert_eq!(min, 3);
|
||||||
assert_eq!(max, 3);
|
assert_eq!(max, 3);
|
||||||
}
|
}
|
||||||
other => panic!("unexpected error: {}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
run_error! {
|
||||||
fn missing_some_arguments_variadic() {
|
name: missing_some_arguments_variadic,
|
||||||
let justfile = compile("a b c +d:");
|
src: "a b c +d:",
|
||||||
let config = config(&["a", "B", "C"]);
|
args: ["a", "B", "C"],
|
||||||
let dir = env::current_dir().unwrap();
|
error: ArgumentCountMismatch {
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
|
||||||
ArgumentCountMismatch {
|
|
||||||
recipe,
|
recipe,
|
||||||
parameters,
|
parameters,
|
||||||
found,
|
found,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
} => {
|
},
|
||||||
|
check: {
|
||||||
let param_names = parameters
|
let param_names = parameters
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| p.name.lexeme())
|
.map(|p| p.name.lexeme())
|
||||||
@ -373,24 +356,20 @@ a return code:
|
|||||||
assert_eq!(min, 3);
|
assert_eq!(min, 3);
|
||||||
assert_eq!(max, usize::MAX - 1);
|
assert_eq!(max, usize::MAX - 1);
|
||||||
}
|
}
|
||||||
other => panic!("unexpected error: {}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
run_error! {
|
||||||
fn missing_all_arguments() {
|
name: missing_all_arguments,
|
||||||
let justfile = compile("a b c d:\n echo {{b}}{{c}}{{d}}");
|
src: "a b c d:\n echo {{b}}{{c}}{{d}}",
|
||||||
let config = config(&["a"]);
|
args: ["a"],
|
||||||
let dir = env::current_dir().unwrap();
|
error: ArgumentCountMismatch {
|
||||||
|
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
|
||||||
ArgumentCountMismatch {
|
|
||||||
recipe,
|
recipe,
|
||||||
parameters,
|
parameters,
|
||||||
found,
|
found,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
} => {
|
},
|
||||||
|
check: {
|
||||||
let param_names = parameters
|
let param_names = parameters
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| p.name.lexeme())
|
.map(|p| p.name.lexeme())
|
||||||
@ -401,24 +380,20 @@ a return code:
|
|||||||
assert_eq!(min, 3);
|
assert_eq!(min, 3);
|
||||||
assert_eq!(max, 3);
|
assert_eq!(max, 3);
|
||||||
}
|
}
|
||||||
other => panic!("unexpected error: {}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
run_error! {
|
||||||
fn missing_some_defaults() {
|
name: missing_some_defaults,
|
||||||
let justfile = compile("a b c d='hello':");
|
src: "a b c d='hello':",
|
||||||
let config = config(&["a", "b"]);
|
args: ["a", "b"],
|
||||||
let dir = env::current_dir().unwrap();
|
error: ArgumentCountMismatch {
|
||||||
|
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
|
||||||
ArgumentCountMismatch {
|
|
||||||
recipe,
|
recipe,
|
||||||
parameters,
|
parameters,
|
||||||
found,
|
found,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
} => {
|
},
|
||||||
|
check: {
|
||||||
let param_names = parameters
|
let param_names = parameters
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| p.name.lexeme())
|
.map(|p| p.name.lexeme())
|
||||||
@ -429,24 +404,20 @@ a return code:
|
|||||||
assert_eq!(min, 2);
|
assert_eq!(min, 2);
|
||||||
assert_eq!(max, 3);
|
assert_eq!(max, 3);
|
||||||
}
|
}
|
||||||
other => panic!("unexpected error: {}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
run_error! {
|
||||||
fn missing_all_defaults() {
|
name: missing_all_defaults,
|
||||||
let justfile = compile("a b c='r' d='h':");
|
src: "a b c='r' d='h':",
|
||||||
let config = &config(&["a"]);
|
args: ["a"],
|
||||||
let dir = env::current_dir().unwrap();
|
error: ArgumentCountMismatch {
|
||||||
|
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
|
||||||
ArgumentCountMismatch {
|
|
||||||
recipe,
|
recipe,
|
||||||
parameters,
|
parameters,
|
||||||
found,
|
found,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
} => {
|
},
|
||||||
|
check: {
|
||||||
let param_names = parameters
|
let param_names = parameters
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| p.name.lexeme())
|
.map(|p| p.name.lexeme())
|
||||||
@ -457,27 +428,24 @@ a return code:
|
|||||||
assert_eq!(min, 1);
|
assert_eq!(min, 1);
|
||||||
assert_eq!(max, 3);
|
assert_eq!(max, 3);
|
||||||
}
|
}
|
||||||
other => panic!("unexpected error: {}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
run_error! {
|
||||||
fn unknown_overrides() {
|
name: unknown_overrides,
|
||||||
let config = config(&["foo=bar", "baz=bob", "a"]);
|
src: "
|
||||||
let justfile = compile("a:\n echo {{`f() { return 100; }; f`}}");
|
a:
|
||||||
let dir = env::current_dir().unwrap();
|
echo {{`f() { return 100; }; f`}}
|
||||||
|
",
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
args: ["foo=bar", "baz=bob", "a"],
|
||||||
UnknownOverrides { overrides } => {
|
error: UnknownOverrides { overrides },
|
||||||
|
check: {
|
||||||
assert_eq!(overrides, &["baz", "foo"]);
|
assert_eq!(overrides, &["baz", "foo"]);
|
||||||
}
|
}
|
||||||
other => panic!("unexpected error: {}", other),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
run_error! {
|
||||||
fn export_failure() {
|
name: export_failure,
|
||||||
let text = r#"
|
src: r#"
|
||||||
export foo = "a"
|
export foo = "a"
|
||||||
baz = "c"
|
baz = "c"
|
||||||
export bar = "b"
|
export bar = "b"
|
||||||
@ -485,23 +453,16 @@ export abc = foo + bar + baz
|
|||||||
|
|
||||||
wut:
|
wut:
|
||||||
echo $foo $bar $baz
|
echo $foo $bar $baz
|
||||||
"#;
|
"#,
|
||||||
|
args: ["--quiet", "wut"],
|
||||||
let config = config(&["--quiet", "wut"]);
|
error: Code {
|
||||||
|
|
||||||
let justfile = compile(text);
|
|
||||||
let dir = env::current_dir().unwrap();
|
|
||||||
|
|
||||||
match justfile.run(&config, &dir).unwrap_err() {
|
|
||||||
Code {
|
|
||||||
code: _,
|
code: _,
|
||||||
line_number,
|
line_number,
|
||||||
recipe,
|
recipe,
|
||||||
} => {
|
},
|
||||||
|
check: {
|
||||||
assert_eq!(recipe, "wut");
|
assert_eq!(recipe, "wut");
|
||||||
assert_eq!(line_number, Some(8));
|
assert_eq!(line_number, Some(7));
|
||||||
}
|
|
||||||
other => panic!("unexpected error: {}", other),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
80
src/lexer.rs
80
src/lexer.rs
@ -210,6 +210,46 @@ impl<'a> Lexer<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if `text` could be an identifier
|
||||||
|
pub(crate) fn is_identifier(text: &str) -> bool {
|
||||||
|
if !text
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.map(|c| Self::is_identifier_start(c))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for c in text.chars().skip(1) {
|
||||||
|
if !Self::is_identifier_continue(c) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if `c` can be the first character of an identifier
|
||||||
|
fn is_identifier_start(c: char) -> bool {
|
||||||
|
match c {
|
||||||
|
'a'..='z' | 'A'..='Z' | '_' => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if `c` can be a continuation character of an idenitifier
|
||||||
|
fn is_identifier_continue(c: char) -> bool {
|
||||||
|
if Self::is_identifier_start(c) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
match c {
|
||||||
|
'0'..='9' | '-' => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Consume the text and produce a series of tokens
|
/// Consume the text and produce a series of tokens
|
||||||
fn tokenize(mut self) -> CompilationResult<'a, Vec<Token<'a>>> {
|
fn tokenize(mut self) -> CompilationResult<'a, Vec<Token<'a>>> {
|
||||||
loop {
|
loop {
|
||||||
@ -362,13 +402,16 @@ impl<'a> Lexer<'a> {
|
|||||||
' ' | '\t' => self.lex_whitespace(),
|
' ' | '\t' => self.lex_whitespace(),
|
||||||
'\'' => self.lex_raw_string(),
|
'\'' => self.lex_raw_string(),
|
||||||
'"' => self.lex_cooked_string(),
|
'"' => self.lex_cooked_string(),
|
||||||
'a'..='z' | 'A'..='Z' | '_' => self.lex_identifier(),
|
|
||||||
_ => {
|
_ => {
|
||||||
|
if Self::is_identifier_start(start) {
|
||||||
|
self.lex_identifier()
|
||||||
|
} else {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
Err(self.error(UnknownStartOfToken))
|
Err(self.error(UnknownStartOfToken))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Lex token beginning with `start` in interpolation state
|
/// Lex token beginning with `start` in interpolation state
|
||||||
fn lex_interpolation(
|
fn lex_interpolation(
|
||||||
@ -515,13 +558,16 @@ impl<'a> Lexer<'a> {
|
|||||||
self.lex_double(Eol)
|
self.lex_double(Eol)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lex identifier: [a-zA-Z_][a-zA-Z0-9_]*
|
/// Lex name: [a-zA-Z_][a-zA-Z0-9_]*
|
||||||
fn lex_identifier(&mut self) -> CompilationResult<'a, ()> {
|
fn lex_identifier(&mut self) -> CompilationResult<'a, ()> {
|
||||||
while self
|
// advance over initial character
|
||||||
.next
|
self.advance()?;
|
||||||
.map(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
|
||||||
.unwrap_or(false)
|
while let Some(c) = self.next {
|
||||||
{
|
if !Self::is_identifier_continue(c) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1667,6 +1713,26 @@ mod tests {
|
|||||||
kind: UnknownStartOfToken,
|
kind: UnknownStartOfToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: invalid_name_start_dash,
|
||||||
|
input: "-foo",
|
||||||
|
offset: 0,
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
width: 1,
|
||||||
|
kind: UnknownStartOfToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: invalid_name_start_digit,
|
||||||
|
input: "0foo",
|
||||||
|
offset: 0,
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
width: 1,
|
||||||
|
kind: UnknownStartOfToken,
|
||||||
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
name: unterminated_string,
|
name: unterminated_string,
|
||||||
input: r#"a = ""#,
|
input: r#"a = ""#,
|
||||||
|
@ -62,6 +62,7 @@ mod parser;
|
|||||||
mod platform;
|
mod platform;
|
||||||
mod platform_interface;
|
mod platform_interface;
|
||||||
mod position;
|
mod position;
|
||||||
|
mod positional;
|
||||||
mod range_ext;
|
mod range_ext;
|
||||||
mod recipe;
|
mod recipe;
|
||||||
mod recipe_context;
|
mod recipe_context;
|
||||||
|
231
src/positional.rs
Normal file
231
src/positional.rs
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
/// A struct containing the parsed representation of positional
|
||||||
|
/// command-line arguments, i.e. arguments that are not flags,
|
||||||
|
/// options, or the subcommand.
|
||||||
|
///
|
||||||
|
/// The DSL of positional arguments is fairly complex and mostly
|
||||||
|
/// accidental. There are three possible components: overrides,
|
||||||
|
/// a search directory, and the rest:
|
||||||
|
///
|
||||||
|
/// - Overrides are of the form `NAME=.*`
|
||||||
|
///
|
||||||
|
/// - After overrides comes a single optional search_directory argument.
|
||||||
|
/// This is either '.', '..', or an argument that contains a `/`.
|
||||||
|
///
|
||||||
|
/// If the argument contains a `/`, everything before and including
|
||||||
|
/// the slash is the search directory, and everything after is added
|
||||||
|
/// to the rest.
|
||||||
|
///
|
||||||
|
/// - Everything else is an argument.
|
||||||
|
///
|
||||||
|
/// Overrides set the values of top-level variables in the justfile
|
||||||
|
/// being invoked and are a convenient way to override settings.
|
||||||
|
///
|
||||||
|
/// For modes that do not take other arguments, the search directory
|
||||||
|
/// argument determines where to begin searching for the justfile. This
|
||||||
|
/// allows command lines like `just -l ..` and `just ../build` to find
|
||||||
|
/// the same justfile.
|
||||||
|
///
|
||||||
|
/// For modes that do take other arguments, the search argument is simply
|
||||||
|
/// prepended to rest.
|
||||||
|
#[cfg_attr(test, derive(PartialEq, Debug))]
|
||||||
|
pub struct Positional {
|
||||||
|
/// Overrides from values of the form `[a-zA-Z_][a-zA-Z0-9_-]*=.*`
|
||||||
|
pub overrides: Vec<(String, String)>,
|
||||||
|
/// An argument equal to '.', '..', or ending with `/`
|
||||||
|
pub search_directory: Option<String>,
|
||||||
|
/// Everything else
|
||||||
|
pub arguments: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Positional {
|
||||||
|
pub fn from_values<'values>(
|
||||||
|
values: Option<impl IntoIterator<Item = &'values str>>,
|
||||||
|
) -> Positional {
|
||||||
|
let mut overrides = Vec::new();
|
||||||
|
let mut search_directory = None;
|
||||||
|
let mut arguments = Vec::new();
|
||||||
|
|
||||||
|
if let Some(values) = values {
|
||||||
|
for value in values {
|
||||||
|
if search_directory.is_none() && arguments.is_empty() {
|
||||||
|
if let Some(o) = Self::override_from_value(value) {
|
||||||
|
overrides.push(o);
|
||||||
|
} else if value == "." || value == ".." {
|
||||||
|
search_directory = Some(value.to_owned());
|
||||||
|
} else if let Some(i) = value.rfind('/') {
|
||||||
|
let (dir, tail) = value.split_at(i + 1);
|
||||||
|
|
||||||
|
search_directory = Some(dir.to_owned());
|
||||||
|
|
||||||
|
if !tail.is_empty() {
|
||||||
|
arguments.push(tail.to_owned());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arguments.push(value.to_owned());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arguments.push(value.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Positional {
|
||||||
|
overrides,
|
||||||
|
search_directory,
|
||||||
|
arguments,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an override from a value of the form `NAME=.*`.
|
||||||
|
fn override_from_value(value: &str) -> Option<(String, String)> {
|
||||||
|
if let Some(equals) = value.find('=') {
|
||||||
|
let (identifier, equals_value) = value.split_at(equals);
|
||||||
|
|
||||||
|
// exclude `=` from value
|
||||||
|
let value = &equals_value[1..];
|
||||||
|
|
||||||
|
if Lexer::is_identifier(identifier) {
|
||||||
|
Some((identifier.to_owned(), value.to_owned()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
macro_rules! test {
|
||||||
|
{
|
||||||
|
name: $name:ident,
|
||||||
|
values: $vals:expr,
|
||||||
|
overrides: $overrides:expr,
|
||||||
|
search_directory: $search_directory:expr,
|
||||||
|
arguments: $arguments:expr,
|
||||||
|
} => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
assert_eq! (
|
||||||
|
Positional::from_values(Some($vals.iter().cloned())),
|
||||||
|
Positional {
|
||||||
|
overrides: $overrides.iter().cloned().map(|(key, value): (&str, &str)| (key.to_owned(), value.to_owned())).collect(),
|
||||||
|
search_directory: $search_directory.map(|dir: &str| dir.to_owned()),
|
||||||
|
arguments: $arguments.iter().cloned().map(|arg: &str| arg.to_owned()).collect(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: no_values,
|
||||||
|
values: [],
|
||||||
|
overrides: [],
|
||||||
|
search_directory: None,
|
||||||
|
arguments: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: arguments_only,
|
||||||
|
values: ["foo", "bar"],
|
||||||
|
overrides: [],
|
||||||
|
search_directory: None,
|
||||||
|
arguments: ["foo", "bar"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: all_overrides,
|
||||||
|
values: ["foo=bar", "bar=foo"],
|
||||||
|
overrides: [("foo", "bar"), ("bar", "foo")],
|
||||||
|
search_directory: None,
|
||||||
|
arguments: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: override_not_name,
|
||||||
|
values: ["foo=bar", "bar.=foo"],
|
||||||
|
overrides: [("foo", "bar")],
|
||||||
|
search_directory: None,
|
||||||
|
arguments: ["bar.=foo"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: no_overrides,
|
||||||
|
values: ["the-dir/", "baz", "bzzd"],
|
||||||
|
overrides: [],
|
||||||
|
search_directory: Some("the-dir/"),
|
||||||
|
arguments: ["baz", "bzzd"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: no_search_directory,
|
||||||
|
values: ["foo=bar", "bar=foo", "baz", "bzzd"],
|
||||||
|
overrides: [("foo", "bar"), ("bar", "foo")],
|
||||||
|
search_directory: None,
|
||||||
|
arguments: ["baz", "bzzd"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: no_arguments,
|
||||||
|
values: ["foo=bar", "bar=foo", "the-dir/"],
|
||||||
|
overrides: [("foo", "bar"), ("bar", "foo")],
|
||||||
|
search_directory: Some("the-dir/"),
|
||||||
|
arguments: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: all_dot,
|
||||||
|
values: ["foo=bar", "bar=foo", ".", "garnor"],
|
||||||
|
overrides: [("foo", "bar"), ("bar", "foo")],
|
||||||
|
search_directory: Some("."),
|
||||||
|
arguments: ["garnor"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: all_dot_dot,
|
||||||
|
values: ["foo=bar", "bar=foo", "..", "garnor"],
|
||||||
|
overrides: [("foo", "bar"), ("bar", "foo")],
|
||||||
|
search_directory: Some(".."),
|
||||||
|
arguments: ["garnor"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: all_slash,
|
||||||
|
values: ["foo=bar", "bar=foo", "/", "garnor"],
|
||||||
|
overrides: [("foo", "bar"), ("bar", "foo")],
|
||||||
|
search_directory: Some("/"),
|
||||||
|
arguments: ["garnor"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_directory_after_argument,
|
||||||
|
values: ["foo=bar", "bar=foo", "baz", "bzzd", "bar/"],
|
||||||
|
overrides: [("foo", "bar"), ("bar", "foo")],
|
||||||
|
search_directory: None,
|
||||||
|
arguments: ["baz", "bzzd", "bar/"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: override_after_search_directory,
|
||||||
|
values: ["..", "a=b"],
|
||||||
|
overrides: [],
|
||||||
|
search_directory: Some(".."),
|
||||||
|
arguments: ["a=b"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: override_after_argument,
|
||||||
|
values: ["a", "a=b"],
|
||||||
|
overrides: [],
|
||||||
|
search_directory: None,
|
||||||
|
arguments: ["a", "a=b"],
|
||||||
|
}
|
||||||
|
}
|
@ -69,6 +69,7 @@ impl<'a> Recipe<'a> {
|
|||||||
context: &RecipeContext<'a>,
|
context: &RecipeContext<'a>,
|
||||||
arguments: &[&'a str],
|
arguments: &[&'a str],
|
||||||
dotenv: &BTreeMap<String, String>,
|
dotenv: &BTreeMap<String, String>,
|
||||||
|
overrides: &BTreeMap<String, String>,
|
||||||
) -> RunResult<'a, ()> {
|
) -> RunResult<'a, ()> {
|
||||||
let config = &context.config;
|
let config = &context.config;
|
||||||
|
|
||||||
@ -89,6 +90,7 @@ impl<'a> Recipe<'a> {
|
|||||||
evaluated: empty(),
|
evaluated: empty(),
|
||||||
working_directory: context.working_directory,
|
working_directory: context.working_directory,
|
||||||
scope: &context.scope,
|
scope: &context.scope,
|
||||||
|
overrides,
|
||||||
config,
|
config,
|
||||||
dotenv,
|
dotenv,
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,14 @@ impl Search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SearchConfig::FromSearchDirectory { search_directory } => {
|
SearchConfig::FromSearchDirectory { search_directory } => {
|
||||||
let justfile = Self::justfile(search_directory)?;
|
let search_directory =
|
||||||
|
search_directory
|
||||||
|
.canonicalize()
|
||||||
|
.context(search_error::Canonicalize {
|
||||||
|
path: search_directory,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let justfile = Self::justfile(&search_directory)?;
|
||||||
|
|
||||||
let working_directory = Self::working_directory_from_justfile(&justfile)?;
|
let working_directory = Self::working_directory_from_justfile(&justfile)?;
|
||||||
|
|
||||||
@ -58,6 +65,7 @@ impl Search {
|
|||||||
|
|
||||||
fn justfile(directory: &Path) -> SearchResult<PathBuf> {
|
fn justfile(directory: &Path) -> SearchResult<PathBuf> {
|
||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
|
|
||||||
let entries = fs::read_dir(directory).map_err(|io_error| SearchError::Io {
|
let entries = fs::read_dir(directory).map_err(|io_error| SearchError::Io {
|
||||||
io_error,
|
io_error,
|
||||||
directory: directory.to_owned(),
|
directory: directory.to_owned(),
|
||||||
|
@ -1,10 +1,19 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
pub(crate) enum Subcommand {
|
pub(crate) enum Subcommand {
|
||||||
Dump,
|
Dump,
|
||||||
Edit,
|
Edit,
|
||||||
Evaluate,
|
Evaluate {
|
||||||
Run,
|
overrides: BTreeMap<String, String>,
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
overrides: BTreeMap<String, String>,
|
||||||
|
arguments: Vec<String>,
|
||||||
|
},
|
||||||
List,
|
List,
|
||||||
Show { name: String },
|
Show {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
Summary,
|
Summary,
|
||||||
}
|
}
|
||||||
|
@ -32,12 +32,12 @@ macro_rules! analysis_error {
|
|||||||
) => {
|
) => {
|
||||||
#[test]
|
#[test]
|
||||||
fn $name() {
|
fn $name() {
|
||||||
$crate::testing::error($input, $offset, $line, $column, $width, $kind);
|
$crate::testing::analysis_error($input, $offset, $line, $column, $width, $kind);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn error(
|
pub(crate) fn analysis_error(
|
||||||
src: &str,
|
src: &str,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
line: usize,
|
line: usize,
|
||||||
@ -66,27 +66,36 @@ pub(crate) fn error(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! run_error {
|
||||||
|
{
|
||||||
|
name: $name:ident,
|
||||||
|
src: $src:expr,
|
||||||
|
args: $args:expr,
|
||||||
|
error: $error:pat,
|
||||||
|
check: $check:block $(,)?
|
||||||
|
} => {
|
||||||
#[test]
|
#[test]
|
||||||
fn readme_test() {
|
fn $name() {
|
||||||
let mut justfiles = vec![];
|
let config = &$crate::testing::config(&$args);
|
||||||
let mut current = None;
|
let current_dir = std::env::current_dir().unwrap();
|
||||||
|
|
||||||
for line in fs::read_to_string("README.adoc").unwrap().lines() {
|
if let Subcommand::Run{ overrides, arguments } = &config.subcommand {
|
||||||
if let Some(mut justfile) = current {
|
match $crate::compiler::Compiler::compile(&$crate::testing::unindent($src))
|
||||||
if line == "```" {
|
.expect("Expected successful compilation")
|
||||||
justfiles.push(justfile);
|
.run(
|
||||||
current = None;
|
config,
|
||||||
|
¤t_dir,
|
||||||
|
&overrides,
|
||||||
|
&arguments,
|
||||||
|
).expect_err("Expected runtime error") {
|
||||||
|
$error => $check
|
||||||
|
other => {
|
||||||
|
panic!("Unexpected run error: {:?}", other);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
justfile += line;
|
panic!("Unexpected subcommand: {:?}", config.subcommand);
|
||||||
justfile += "\n";
|
|
||||||
current = Some(justfile);
|
|
||||||
}
|
|
||||||
} else if line == "```make" {
|
|
||||||
current = Some(String::new());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
for justfile in justfiles {
|
|
||||||
compile(&justfile);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,16 @@ pub fn tempdir() -> tempfile::TempDir {
|
|||||||
.expect("failed to create temporary directory")
|
.expect("failed to create temporary directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assert_stdout(output: &Output, stdout: &str) {
|
pub fn assert_success(output: &Output) {
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||||
eprintln!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
eprintln!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||||
panic!(output.status);
|
panic!(output.status);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_stdout(output: &Output, stdout: &str) {
|
||||||
|
assert_success(output);
|
||||||
assert_eq!(String::from_utf8_lossy(&output.stdout), stdout);
|
assert_eq!(String::from_utf8_lossy(&output.stdout), stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
41
tests/readme.rs
Normal file
41
tests/readme.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
use std::{fs, process::Command};
|
||||||
|
|
||||||
|
use executable_path::executable_path;
|
||||||
|
use test_utilities::{assert_success, tempdir};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn readme() {
|
||||||
|
let mut justfiles = vec![];
|
||||||
|
let mut current = None;
|
||||||
|
|
||||||
|
for line in fs::read_to_string("README.adoc").unwrap().lines() {
|
||||||
|
if let Some(mut justfile) = current {
|
||||||
|
if line == "```" {
|
||||||
|
justfiles.push(justfile);
|
||||||
|
current = None;
|
||||||
|
} else {
|
||||||
|
justfile += line;
|
||||||
|
justfile += "\n";
|
||||||
|
current = Some(justfile);
|
||||||
|
}
|
||||||
|
} else if line == "```make" {
|
||||||
|
current = Some(String::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for justfile in justfiles {
|
||||||
|
let tmp = tempdir();
|
||||||
|
|
||||||
|
let path = tmp.path().join("justfile");
|
||||||
|
|
||||||
|
fs::write(&path, &justfile).unwrap();
|
||||||
|
|
||||||
|
let output = Command::new(executable_path("just"))
|
||||||
|
.current_dir(tmp.path())
|
||||||
|
.arg("--dump")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_success(&output);
|
||||||
|
}
|
||||||
|
}
|
@ -121,3 +121,27 @@ fn test_downwards_multiple_path_argument() {
|
|||||||
search_test(&path, &["./a/b/"]);
|
search_test(&path, &["./a/b/"]);
|
||||||
search_test(&path, &["./a/b/default"]);
|
search_test(&path, &["./a/b/default"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_downards() {
|
||||||
|
let tmp = tmptree! {
|
||||||
|
justfile: "default:\n\techo ok",
|
||||||
|
child: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = tmp.path();
|
||||||
|
|
||||||
|
search_test(&path, &["child/"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_upwards() {
|
||||||
|
let tmp = tmptree! {
|
||||||
|
justfile: "default:\n\techo ok",
|
||||||
|
child: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = tmp.path().join("child");
|
||||||
|
|
||||||
|
search_test(&path, &["../"]);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user