Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,13 +486,14 @@ pub(crate) struct Subcommand {
)]
pub(crate) summary: bool,
#[arg(
add = ArgValueCompleter::new(Completer::complete_recipe),
add = ArgValueCompleter::new(Completer::complete_recipe_or_module),
conflicts_with = "arguments",
help = "Print usage information for recipe at <RECIPE_PATH>",
help = "Print usage information for recipe at <PATH> or all recipes in module at <PATH>, \
defaulting to all recipes in the root justfile",
help_heading = Self::HEADING,
long,
num_args = 1..,
value_name = "RECIPE_PATH",
num_args = 0..,
value_name = "PATH",
)]
pub(crate) usage: Option<Vec<String>>,
#[arg(
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1101,7 +1101,7 @@ impl ColorDisplay for Error<'_> {
f,
"{}",
Usage {
long: false,
mode: usage::Mode::Short,
path: recipe.recipe_path(),
recipe,
}
Expand Down
109 changes: 68 additions & 41 deletions src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -622,27 +622,8 @@ impl Subcommand {
}

fn list<'src>(config: &Config, root: &Justfile<'src>, path: &Modulepath) -> RunResult<'src> {
let mut module = root;

for name in &path.components {
if let Some(submodule) = module.modules.get(name) {
module = submodule;
} else if let Some(alias) = module.module_aliases.get(name) {
module = root.submodule(&alias.target).unwrap();
} else if module.absent_modules.contains(name) {
return Err(Error::ModuleAbsent {
module: module.module_path.join(name),
});
} else {
return Err(Error::UnknownSubmodule {
path: path.to_string(),
suggestion: module.suggest_submodule(name),
});
}
}

let module = Self::resolve_module(root, path, &path.components)?;
Self::list_module(config, 0, &config.groups, module)?;

Ok(())
}

Expand Down Expand Up @@ -1042,38 +1023,66 @@ impl Subcommand {
}
}

fn usage<'src>(config: &Config, module: &Justfile<'src>, path: &Modulepath) -> RunResult<'src> {
let (alias, recipe) = Self::resolve_path(module, path, "usage")?;
fn usage<'src>(config: &Config, root: &Justfile<'src>, path: &Modulepath) -> RunResult<'src> {
if let Some(module) = root.submodule(path) {
let recipes = module.public_recipes(config);

if let Some(alias) = alias {
println!("{alias}");
}
if recipes.is_empty() {
if config.verbosity.loud() {
eprintln!("module contains no recipes");
}
} else {
println!("{}", config.color.stdout().heading().paint("Usage:"));
for (i, recipe) in recipes.into_iter().enumerate() {
if i > 0 {
println!();
}

let path = Modulepath {
spaced: true,
..path.join(recipe.name())
};

println!(
"{}",
Usage {
mode: usage::Mode::Module,
path: &path,
recipe,
}
.color_display(config.color.stdout()),
);
}
}
} else {
let (alias, recipe) = Self::resolve_path(root, path, "usage")?;

println!(
"{}",
Usage {
long: true,
path,
recipe,
if let Some(alias) = alias {
println!("{alias}");
}
.color_display(config.color.stdout()),
);

println!(
"{}",
Usage {
mode: usage::Mode::Recipe,
path,
recipe
}
.color_display(config.color.stdout()),
);
}

Ok(())
}

fn resolve_path<'src, 'run>(
fn resolve_module<'src, 'run>(
root: &'run Justfile<'src>,
path: &Modulepath,
subcommand: &'static str,
) -> RunResult<'src, (Option<&'run RecipeAlias<'src>>, &'run Recipe<'src>)> {
components: &[String],
) -> RunResult<'src, &'run Justfile<'src>> {
let mut module = root;

let Some((name, ancestors)) = path.components.split_last() else {
return Err(Error::RecipeRequired { subcommand });
};

for name in ancestors {
for name in components {
if let Some(submodule) = module.modules.get(name) {
module = submodule;
} else if let Some(alias) = module.module_aliases.get(name) {
Expand All @@ -1090,6 +1099,20 @@ impl Subcommand {
}
}

Ok(module)
}

fn resolve_path<'src, 'run>(
root: &'run Justfile<'src>,
path: &Modulepath,
subcommand: &'static str,
) -> RunResult<'src, (Option<&'run RecipeAlias<'src>>, &'run Recipe<'src>)> {
let Some((name, ancestors)) = path.components.split_last() else {
return Err(Error::RecipeRequired { subcommand });
};

let module = Self::resolve_module(root, path, ancestors)?;

if let Some(alias) = module.recipe_alias(name) {
Ok((Some(alias), &alias.target))
} else if let Some(recipe) = module.recipe(name) {
Expand All @@ -1104,6 +1127,10 @@ impl Subcommand {
alias: path.clone(),
modules: disabled.modules.clone(),
})
} else if module.absent_modules.contains(name) {
Err(Error::ModuleAbsent {
module: module.module_path.join(name),
})
} else {
Err(Error::UnknownRecipe {
recipe: name.to_owned(),
Expand Down
71 changes: 51 additions & 20 deletions src/usage.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,48 @@
use super::*;

pub(crate) struct Usage<'a, D> {
pub(crate) long: bool,
#[derive(PartialEq)]
pub(crate) enum Mode {
Module,
Recipe,
Short,
}

pub(crate) struct Usage<'a> {
pub(crate) mode: Mode,
pub(crate) path: &'a Modulepath,
pub(crate) recipe: &'a Recipe<'a, D>,
pub(crate) recipe: &'a Recipe<'a>,
}

impl<D> ColorDisplay for Usage<'_, D> {
impl ColorDisplay for Usage<'_> {
fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
let indentation = match self.mode {
Mode::Module => " ",
Mode::Recipe | Mode::Short => "",
};

match self.mode {
Mode::Module => {
if let Some(doc) = self.recipe.doc() {
for line in doc.lines() {
writeln!(
f,
"{indentation}{}",
color.doc().paint(&format!("# {line}"))
)?;
}
}
}
Mode::Recipe => {
write!(f, "{} ", color.heading().paint("Usage:"))?;
}
Mode::Short => {
write!(f, "{}\n ", color.heading().paint("usage:"))?;
}
}

write!(
f,
"{}{}{} {}",
color
.heading()
.paint(if self.long { "Usage:" } else { "usage:" }),
if self.long { " " } else { "\n " },
"{indentation}{} {}",
color.argument().paint("just"),
color.argument().paint(&self.path.to_string()),
)?;
Expand Down Expand Up @@ -45,14 +73,17 @@ impl<D> ColorDisplay for Usage<'_, D> {
)?;
}

if !self.long {
if self.mode == Mode::Short {
return Ok(());
}

if arguments {
writeln!(f)?;
writeln!(f)?;
writeln!(f, "{}", color.heading().paint("Arguments:"))?;

if self.mode == Mode::Recipe {
writeln!(f)?;
writeln!(f, "{}", color.heading().paint("Arguments:"))?;
}

for (i, parameter) in self
.recipe
Expand All @@ -65,11 +96,9 @@ impl<D> ColorDisplay for Usage<'_, D> {
writeln!(f)?;
}

write!(f, " ")?;

write!(
f,
"{}",
"{indentation} {}",
UsageParameter {
parameter,
long: true,
Expand All @@ -81,8 +110,12 @@ impl<D> ColorDisplay for Usage<'_, D> {

if options {
writeln!(f)?;
writeln!(f)?;
writeln!(f, "{}", color.heading().paint("Options:"))?;

if self.mode == Mode::Recipe {
writeln!(f)?;
writeln!(f, "{}", color.heading().paint("Options:"))?;
}

for (i, parameter) in self
.recipe
.parameters
Expand All @@ -94,11 +127,9 @@ impl<D> ColorDisplay for Usage<'_, D> {
writeln!(f)?;
}

write!(f, " ")?;

write!(
f,
"{}",
"{indentation} {}",
UsageParameter {
parameter,
long: true,
Expand Down
2 changes: 1 addition & 1 deletion tests/completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ fn usage_recipes() {
.shell(false)
.env("JUST_COMPLETE", "fish")
.args(complete_args(&["--usage", ""]))
.stdout("bar\nfoo\n")
.stdout_regex("bar\nfoo\nbar\nfoo\n.\njustfile\n--.*")
.success();
}

Expand Down
18 changes: 18 additions & 0 deletions tests/modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,15 @@ fn show_recipe_in_absent_optional_module_is_error() {
.failure();
}

#[test]
fn show_absent_optional_module_is_error() {
Test::new()
.justfile("mod? foo")
.args(["--show", "foo"])
.stderr("error: optional module `foo` is absent\n")
.failure();
}

#[test]
fn list_absent_optional_module_is_error() {
Test::new()
Expand All @@ -1033,6 +1042,15 @@ fn list_absent_optional_module_is_error() {
.failure();
}

#[test]
fn usage_absent_optional_module_is_error() {
Test::new()
.justfile("mod? foo")
.args(["--usage", "foo"])
.stderr("error: optional module `foo` is absent\n")
.failure();
}

#[test]
fn evaluate_absent_optional_module_is_error() {
Test::new()
Expand Down
Loading
Loading