Add recipe groups (#1842)
This commit is contained in:
parent
1654d14867
commit
ed0dc20318
69
README.md
69
README.md
@ -1610,7 +1610,8 @@ Recipes may be annotated with attributes that change their behavior.
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `[confirm]`<sup>1.17.0</sup> | Require confirmation prior to executing recipe. |
|
||||
| `[confirm("prompt")]`<sup>1.23.0</sup> | Require confirmation prior to executing recipe with a custom prompt. |
|
||||
| `[confirm('prompt')]`<sup>1.23.0</sup> | Require confirmation prior to executing recipe with a custom prompt. |
|
||||
| `[group('NAME"']`<sup>master</sup> | Put recipe in [recipe group](#recipe-groups) `NAME`.
|
||||
| `[linux]`<sup>1.8.0</sup> | Enable recipe on Linux. |
|
||||
| `[macos]`<sup>1.8.0</sup> | Enable recipe on MacOS. |
|
||||
| `[no-cd]`<sup>1.9.0</sup> | Don't change directory before executing recipe. |
|
||||
@ -1709,6 +1710,72 @@ delete-everything:
|
||||
rm -rf *
|
||||
```
|
||||
|
||||
### Recipe Groups
|
||||
|
||||
Recipes can be annotated with a group name:
|
||||
|
||||
```just
|
||||
[group('lint')]
|
||||
js-lint:
|
||||
echo 'Running JS linter…'
|
||||
|
||||
[group('rust recipes')]
|
||||
[group('lint')]
|
||||
rust-lint:
|
||||
echo 'Runninng Rust linter…'
|
||||
|
||||
[group('lint')]
|
||||
cpp-lint:
|
||||
echo 'Running C++ linter…'
|
||||
|
||||
# not in any group
|
||||
email-everyone:
|
||||
echo 'Sending mass email…'
|
||||
```
|
||||
|
||||
Recipes are listed by group:
|
||||
|
||||
```
|
||||
$ just --list
|
||||
Available recipes:
|
||||
(no group)
|
||||
email-everyone # not in any group
|
||||
|
||||
[lint]
|
||||
cpp-lint
|
||||
js-lint
|
||||
rust-lint
|
||||
|
||||
[rust recipes]
|
||||
rust-lint
|
||||
```
|
||||
|
||||
`just --list --unsorted` prints recipes in their justfile order within each group:
|
||||
|
||||
```
|
||||
$ just --list --unsorted
|
||||
Available recipes:
|
||||
(no group)
|
||||
email-everyone # not in any group
|
||||
|
||||
[lint]
|
||||
js-lint
|
||||
rust-lint
|
||||
cpp-lint
|
||||
|
||||
[rust recipes]
|
||||
rust-lint
|
||||
```
|
||||
|
||||
Groups can be listed with `--groups`:
|
||||
|
||||
```
|
||||
$ just --groups
|
||||
Recipe groups:
|
||||
lint
|
||||
rust recipes
|
||||
```
|
||||
|
||||
### Command Evaluation Using Backticks
|
||||
|
||||
Backticks can be used to store the result of commands:
|
||||
|
@ -30,7 +30,7 @@ _just() {
|
||||
|
||||
case "${cmd}" in
|
||||
"$1")
|
||||
opts="-n -f -q -u -v -d -c -e -l -s -E -g -h -V --check --chooser --color --command-color --yes --dry-run --dump-format --highlight --list-heading --list-prefix --no-aliases --no-deps --no-dotenv --no-highlight --justfile --quiet --set --shell --shell-arg --shell-command --clear-shell-args --unsorted --unstable --verbose --working-directory --changelog --choose --command --completions --dump --edit --evaluate --fmt --init --list --man --show --summary --variables --dotenv-filename --dotenv-path --global-justfile --help --version [ARGUMENTS]..."
|
||||
opts="-n -f -q -u -v -d -c -e -l -s -E -g -h -V --check --chooser --color --command-color --yes --dry-run --dump-format --highlight --list-heading --list-prefix --no-aliases --no-deps --no-dotenv --no-highlight --justfile --quiet --set --shell --shell-arg --shell-command --clear-shell-args --unsorted --unstable --verbose --working-directory --changelog --choose --command --completions --dump --edit --evaluate --fmt --init --list --groups --man --show --summary --variables --dotenv-filename --dotenv-path --global-justfile --help --version [ARGUMENTS]..."
|
||||
if [[ ${cur} == -* ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
@ -66,6 +66,7 @@ set edit:completion:arg-completer[just] = {|@words|
|
||||
cand --init 'Initialize new justfile in project root'
|
||||
cand -l 'List available recipes and their arguments'
|
||||
cand --list 'List available recipes and their arguments'
|
||||
cand --groups 'List recipe groups'
|
||||
cand --man 'Print man page'
|
||||
cand --summary 'List names of available recipes'
|
||||
cand --variables 'List names of variables'
|
||||
|
@ -73,6 +73,7 @@ complete -c just -l evaluate -d 'Evaluate and print all variables. If a variable
|
||||
complete -c just -l fmt -d 'Format and overwrite justfile'
|
||||
complete -c just -l init -d 'Initialize new justfile in project root'
|
||||
complete -c just -s l -l list -d 'List available recipes and their arguments'
|
||||
complete -c just -l groups -d 'List recipe groups'
|
||||
complete -c just -l man -d 'Print man page'
|
||||
complete -c just -l summary -d 'List names of available recipes'
|
||||
complete -c just -l variables -d 'List names of variables'
|
||||
|
@ -69,6 +69,7 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
|
||||
[CompletionResult]::new('--init', 'init', [CompletionResultType]::ParameterName, 'Initialize new justfile in project root')
|
||||
[CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List available recipes and their arguments')
|
||||
[CompletionResult]::new('--list', 'list', [CompletionResultType]::ParameterName, 'List available recipes and their arguments')
|
||||
[CompletionResult]::new('--groups', 'groups', [CompletionResultType]::ParameterName, 'List recipe groups')
|
||||
[CompletionResult]::new('--man', 'man', [CompletionResultType]::ParameterName, 'Print man page')
|
||||
[CompletionResult]::new('--summary', 'summary', [CompletionResultType]::ParameterName, 'List names of available recipes')
|
||||
[CompletionResult]::new('--variables', 'variables', [CompletionResultType]::ParameterName, 'List names of variables')
|
||||
|
@ -64,6 +64,7 @@ _just() {
|
||||
'--init[Initialize new justfile in project root]' \
|
||||
'-l[List available recipes and their arguments]' \
|
||||
'--list[List available recipes and their arguments]' \
|
||||
'--groups[List recipe groups]' \
|
||||
'--man[Print man page]' \
|
||||
'--summary[List names of available recipes]' \
|
||||
'--variables[List names of variables]' \
|
||||
|
@ -1,45 +1,105 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(EnumString, PartialEq, Debug, Clone, Serialize, Ord, PartialOrd, Eq, IntoStaticStr)]
|
||||
#[derive(
|
||||
EnumDiscriminants, PartialEq, Debug, Clone, Serialize, Ord, PartialOrd, Eq, IntoStaticStr,
|
||||
)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[strum_discriminants(name(AttributeDiscriminant))]
|
||||
#[strum_discriminants(derive(EnumString))]
|
||||
#[strum_discriminants(strum(serialize_all = "kebab-case"))]
|
||||
pub(crate) enum Attribute<'src> {
|
||||
Confirm(Option<StringLiteral<'src>>),
|
||||
Group(StringLiteral<'src>),
|
||||
Linux,
|
||||
Macos,
|
||||
NoCd,
|
||||
NoExitMessage,
|
||||
Private,
|
||||
NoQuiet,
|
||||
Private,
|
||||
Unix,
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl AttributeDiscriminant {
|
||||
fn argument_range(self) -> RangeInclusive<usize> {
|
||||
match self {
|
||||
Self::Confirm => 0..=1,
|
||||
Self::Group => 1..=1,
|
||||
Self::Linux
|
||||
| Self::Macos
|
||||
| Self::NoCd
|
||||
| Self::NoExitMessage
|
||||
| Self::NoQuiet
|
||||
| Self::Private
|
||||
| Self::Unix
|
||||
| Self::Windows => 0..=0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Attribute<'src> {
|
||||
pub(crate) fn from_name(name: Name) -> Option<Self> {
|
||||
name.lexeme().parse().ok()
|
||||
pub(crate) fn new(
|
||||
name: Name<'src>,
|
||||
argument: Option<StringLiteral<'src>>,
|
||||
) -> CompileResult<'src, Self> {
|
||||
use AttributeDiscriminant::*;
|
||||
|
||||
let discriminant = name
|
||||
.lexeme()
|
||||
.parse::<AttributeDiscriminant>()
|
||||
.ok()
|
||||
.ok_or_else(|| {
|
||||
name.error(CompileErrorKind::UnknownAttribute {
|
||||
attribute: name.lexeme(),
|
||||
})
|
||||
})?;
|
||||
|
||||
let found = argument.as_ref().iter().count();
|
||||
|
||||
let range = discriminant.argument_range();
|
||||
|
||||
if !range.contains(&found) {
|
||||
return Err(
|
||||
name.error(CompileErrorKind::AttributeArgumentCountMismatch {
|
||||
attribute: name.lexeme(),
|
||||
found,
|
||||
min: *range.start(),
|
||||
max: *range.end(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(match discriminant {
|
||||
Confirm => Self::Confirm(argument),
|
||||
Group => Self::Group(argument.unwrap()),
|
||||
Linux => Self::Linux,
|
||||
Macos => Self::Macos,
|
||||
NoCd => Self::NoCd,
|
||||
NoExitMessage => Self::NoExitMessage,
|
||||
NoQuiet => Self::NoQuiet,
|
||||
Private => Self::Private,
|
||||
Unix => Self::Unix,
|
||||
Windows => Self::Windows,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn name(&self) -> &'static str {
|
||||
self.into()
|
||||
}
|
||||
|
||||
pub(crate) fn with_argument(
|
||||
self,
|
||||
name: Name<'src>,
|
||||
argument: StringLiteral<'src>,
|
||||
) -> CompileResult<'src, Self> {
|
||||
match self {
|
||||
Self::Confirm(_) => Ok(Self::Confirm(Some(argument))),
|
||||
_ => Err(name.error(CompileErrorKind::UnexpectedAttributeArgument { attribute: self })),
|
||||
}
|
||||
}
|
||||
|
||||
fn argument(&self) -> Option<&StringLiteral> {
|
||||
if let Self::Confirm(prompt) = self {
|
||||
prompt.as_ref()
|
||||
} else {
|
||||
None
|
||||
match self {
|
||||
Self::Confirm(prompt) => prompt.as_ref(),
|
||||
Self::Group(name) => Some(name),
|
||||
Self::Linux
|
||||
| Self::Macos
|
||||
| Self::NoCd
|
||||
| Self::NoExitMessage
|
||||
| Self::NoQuiet
|
||||
| Self::Private
|
||||
| Self::Unix
|
||||
| Self::Windows => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,27 @@ impl Display for CompileError<'_> {
|
||||
self.token.line.ordinal(),
|
||||
recipe_line.ordinal(),
|
||||
),
|
||||
AttributeArgumentCountMismatch {
|
||||
attribute,
|
||||
found,
|
||||
min,
|
||||
max,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Attribute `{attribute}` got {found} {} but takes ",
|
||||
Count("argument", *found),
|
||||
)?;
|
||||
|
||||
if min == max {
|
||||
let expected = min;
|
||||
write!(f, "{expected} {}", Count("argument", *expected))
|
||||
} else if found < min {
|
||||
write!(f, "at least {min} {}", Count("argument", *min))
|
||||
} else {
|
||||
write!(f, "at most {max} {}", Count("argument", *max))
|
||||
}
|
||||
}
|
||||
BacktickShebang => write!(f, "Backticks may not start with `#!`"),
|
||||
CircularRecipeDependency { recipe, ref circle } => {
|
||||
if circle.len() == 2 {
|
||||
@ -212,13 +233,6 @@ impl Display for CompileError<'_> {
|
||||
"Non-default parameter `{parameter}` follows default parameter"
|
||||
),
|
||||
UndefinedVariable { variable } => write!(f, "Variable `{variable}` not defined"),
|
||||
UnexpectedAttributeArgument { attribute } => {
|
||||
write!(
|
||||
f,
|
||||
"Attribute `{}` specified with argument but takes no arguments",
|
||||
attribute.name(),
|
||||
)
|
||||
}
|
||||
UnexpectedCharacter { expected } => write!(f, "Expected character `{expected}`"),
|
||||
UnexpectedClosingDelimiter { close } => {
|
||||
write!(f, "Unexpected closing delimiter `{}`", close.close())
|
||||
|
@ -10,6 +10,12 @@ pub(crate) enum CompileErrorKind<'src> {
|
||||
alias: &'src str,
|
||||
recipe_line: usize,
|
||||
},
|
||||
AttributeArgumentCountMismatch {
|
||||
attribute: &'src str,
|
||||
found: usize,
|
||||
min: usize,
|
||||
max: usize,
|
||||
},
|
||||
BacktickShebang,
|
||||
CircularRecipeDependency {
|
||||
recipe: &'src str,
|
||||
@ -88,9 +94,6 @@ pub(crate) enum CompileErrorKind<'src> {
|
||||
UndefinedVariable {
|
||||
variable: &'src str,
|
||||
},
|
||||
UnexpectedAttributeArgument {
|
||||
attribute: Attribute<'src>,
|
||||
},
|
||||
UnexpectedCharacter {
|
||||
expected: char,
|
||||
},
|
||||
|
@ -54,6 +54,7 @@ mod cmd {
|
||||
pub(crate) const EDIT: &str = "EDIT";
|
||||
pub(crate) const EVALUATE: &str = "EVALUATE";
|
||||
pub(crate) const FORMAT: &str = "FORMAT";
|
||||
pub(crate) const GROUPS: &str = "GROUPS";
|
||||
pub(crate) const INIT: &str = "INIT";
|
||||
pub(crate) const LIST: &str = "LIST";
|
||||
pub(crate) const MAN: &str = "MAN";
|
||||
@ -417,6 +418,12 @@ impl Config {
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("List available recipes and their arguments"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(cmd::GROUPS)
|
||||
.long("groups")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("List recipe groups")
|
||||
)
|
||||
.arg(
|
||||
Arg::new(cmd::MAN)
|
||||
.long("man")
|
||||
@ -649,6 +656,8 @@ impl Config {
|
||||
Subcommand::Init
|
||||
} else if matches.get_flag(cmd::LIST) {
|
||||
Subcommand::List
|
||||
} else if matches.get_flag(cmd::GROUPS) {
|
||||
Subcommand::Groups
|
||||
} else if matches.get_flag(cmd::MAN) {
|
||||
Subcommand::Man
|
||||
} else if let Some(name) = matches.get_one::<String>(cmd::SHOW).map(Into::into) {
|
||||
|
@ -488,6 +488,16 @@ impl<'src> Justfile<'src> {
|
||||
|
||||
recipes
|
||||
}
|
||||
|
||||
pub(crate) fn public_groups(&self) -> BTreeSet<String> {
|
||||
self
|
||||
.recipes
|
||||
.values()
|
||||
.map(AsRef::as_ref)
|
||||
.filter(|recipe| recipe.is_public())
|
||||
.flat_map(Recipe::groups)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> ColorDisplay for Justfile<'src> {
|
||||
|
@ -71,7 +71,7 @@ pub(crate) use {
|
||||
Serialize, Serializer,
|
||||
},
|
||||
snafu::{ResultExt, Snafu},
|
||||
strum::{Display, EnumString, IntoStaticStr},
|
||||
strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr},
|
||||
typed_arena::Arena,
|
||||
unicode_width::{UnicodeWidthChar, UnicodeWidthStr},
|
||||
},
|
||||
|
@ -976,11 +976,17 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
while self.accepted(BracketL)? {
|
||||
loop {
|
||||
let name = self.parse_name()?;
|
||||
let attribute = Attribute::from_name(name).ok_or_else(|| {
|
||||
name.error(CompileErrorKind::UnknownAttribute {
|
||||
attribute: name.lexeme(),
|
||||
})
|
||||
})?;
|
||||
|
||||
let argument = if self.accepted(ParenL)? {
|
||||
let argument = self.parse_string_literal()?;
|
||||
self.expect(ParenR)?;
|
||||
Some(argument)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let attribute = Attribute::new(name, argument)?;
|
||||
|
||||
if let Some(line) = attributes.get(&attribute) {
|
||||
return Err(name.error(CompileErrorKind::DuplicateAttribute {
|
||||
attribute: name.lexeme(),
|
||||
@ -988,14 +994,6 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
}));
|
||||
}
|
||||
|
||||
let attribute = if self.accepted(ParenL)? {
|
||||
let argument = self.parse_string_literal()?;
|
||||
self.expect(ParenR)?;
|
||||
attribute.with_argument(name, argument)?
|
||||
} else {
|
||||
attribute
|
||||
};
|
||||
|
||||
attributes.insert(attribute, name.line);
|
||||
|
||||
if !self.accepted(Comma)? {
|
||||
|
@ -436,6 +436,20 @@ impl<'src, D> Recipe<'src, D> {
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn groups(&self) -> BTreeSet<String> {
|
||||
self
|
||||
.attributes
|
||||
.iter()
|
||||
.filter_map(|attribute| {
|
||||
if let Attribute::Group(group) = attribute {
|
||||
Some(group.cooked.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src, D: Display> ColorDisplay for Recipe<'src, D> {
|
||||
|
@ -29,6 +29,7 @@ pub(crate) enum Subcommand {
|
||||
variable: Option<String>,
|
||||
},
|
||||
Format,
|
||||
Groups,
|
||||
Init,
|
||||
List,
|
||||
Man,
|
||||
@ -86,6 +87,7 @@ impl Subcommand {
|
||||
}
|
||||
Dump => Self::dump(config, ast, justfile)?,
|
||||
Format => Self::format(config, &search, src, ast)?,
|
||||
Groups => Self::groups(config, justfile),
|
||||
List => Self::list(config, 0, justfile),
|
||||
Show { ref name } => Self::show(config, name, justfile)?,
|
||||
Summary => Self::summary(config, justfile),
|
||||
@ -96,6 +98,13 @@ impl Subcommand {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn groups(config: &Config, justfile: &Justfile) {
|
||||
println!("Recipe groups:");
|
||||
for group in justfile.public_groups() {
|
||||
println!("{}{group}", config.list_prefix);
|
||||
}
|
||||
}
|
||||
|
||||
fn run<'src>(
|
||||
config: &Config,
|
||||
loader: &'src Loader,
|
||||
@ -469,38 +478,33 @@ impl Subcommand {
|
||||
}
|
||||
|
||||
fn list(config: &Config, level: usize, justfile: &Justfile) {
|
||||
const MAX_WIDTH: usize = 50;
|
||||
|
||||
if level == 0 {
|
||||
print!("{}", config.list_heading);
|
||||
}
|
||||
|
||||
// Construct a target to alias map.
|
||||
let mut recipe_aliases = BTreeMap::<&str, Vec<&str>>::new();
|
||||
if !config.no_aliases {
|
||||
for alias in justfile.aliases.values() {
|
||||
if alias.is_private() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if recipe_aliases.contains_key(alias.target.name.lexeme()) {
|
||||
let aliases = recipe_aliases.get_mut(alias.target.name.lexeme()).unwrap();
|
||||
aliases.push(alias.name.lexeme());
|
||||
let aliases = if config.no_aliases {
|
||||
BTreeMap::new()
|
||||
} else {
|
||||
recipe_aliases.insert(alias.target.name.lexeme(), vec![alias.name.lexeme()]);
|
||||
}
|
||||
}
|
||||
let mut aliases = BTreeMap::<&str, Vec<&str>>::new();
|
||||
for alias in justfile
|
||||
.aliases
|
||||
.values()
|
||||
.filter(|alias| !alias.is_private())
|
||||
{
|
||||
aliases
|
||||
.entry(alias.target.name.lexeme())
|
||||
.or_default()
|
||||
.push(alias.name.lexeme());
|
||||
}
|
||||
aliases
|
||||
};
|
||||
|
||||
let mut line_widths = BTreeMap::<&str, usize>::new();
|
||||
let signature_widths = {
|
||||
let mut signature_widths: BTreeMap<&str, usize> = BTreeMap::new();
|
||||
|
||||
for (name, recipe) in &justfile.recipes {
|
||||
if !recipe.is_public() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for name in iter::once(name).chain(recipe_aliases.get(name).unwrap_or(&Vec::new())) {
|
||||
line_widths.insert(
|
||||
for name in iter::once(name).chain(aliases.get(name).unwrap_or(&Vec::new())) {
|
||||
signature_widths.insert(
|
||||
name,
|
||||
UnicodeWidthStr::width(
|
||||
RecipeSignature { name, recipe }
|
||||
@ -512,18 +516,54 @@ impl Subcommand {
|
||||
}
|
||||
}
|
||||
|
||||
let max_line_width = line_widths
|
||||
signature_widths
|
||||
};
|
||||
|
||||
let max_signature_width = signature_widths
|
||||
.values()
|
||||
.filter(|line_width| **line_width <= MAX_WIDTH)
|
||||
.copied()
|
||||
.filter(|width| *width <= 50)
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
.unwrap_or(0);
|
||||
|
||||
if level == 0 {
|
||||
print!("{}", config.list_heading);
|
||||
}
|
||||
|
||||
let groups = {
|
||||
let mut groups = BTreeMap::<Option<String>, Vec<&Recipe>>::new();
|
||||
for recipe in justfile.public_recipes(config.unsorted) {
|
||||
let name = recipe.name();
|
||||
let recipe_groups = recipe.groups();
|
||||
if recipe_groups.is_empty() {
|
||||
groups.entry(None).or_default().push(recipe);
|
||||
} else {
|
||||
for group in recipe_groups {
|
||||
groups.entry(Some(group)).or_default().push(recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
groups
|
||||
};
|
||||
|
||||
for (i, name) in iter::once(&name)
|
||||
.chain(recipe_aliases.get(name).unwrap_or(&Vec::new()))
|
||||
for (i, (group, recipes)) in groups.iter().enumerate() {
|
||||
if i > 0 {
|
||||
println!();
|
||||
}
|
||||
|
||||
let no_groups = groups.contains_key(&None) && groups.len() == 1;
|
||||
|
||||
if !no_groups {
|
||||
print!("{}", config.list_prefix.repeat(level + 1));
|
||||
if let Some(group_name) = group {
|
||||
println!("[{group_name}]");
|
||||
} else {
|
||||
println!("(no group)");
|
||||
}
|
||||
}
|
||||
|
||||
for recipe in recipes {
|
||||
for (i, name) in iter::once(&recipe.name())
|
||||
.chain(aliases.get(recipe.name()).unwrap_or(&Vec::new()))
|
||||
.enumerate()
|
||||
{
|
||||
print!(
|
||||
@ -532,27 +572,31 @@ impl Subcommand {
|
||||
RecipeSignature { name, recipe }.color_display(config.color.stdout())
|
||||
);
|
||||
|
||||
let doc = match (i, recipe.doc) {
|
||||
(0, Some(doc)) => Some(Cow::Borrowed(doc)),
|
||||
(0, None) => None,
|
||||
_ => Some(Cow::Owned(format!("alias for `{}`", recipe.name))),
|
||||
let doc = if i == 0 {
|
||||
recipe.doc.map(Cow::Borrowed)
|
||||
} else {
|
||||
Some(Cow::Owned(format!("alias for `{}`", recipe.name)))
|
||||
};
|
||||
|
||||
if let Some(doc) = doc {
|
||||
print!(
|
||||
" {:padding$}{} {}",
|
||||
"{:padding$}{} {}",
|
||||
"",
|
||||
config.color.stdout().doc().paint("#"),
|
||||
config.color.stdout().doc().paint(&doc),
|
||||
padding = max_line_width.saturating_sub(line_widths[name]),
|
||||
padding = max_signature_width.saturating_sub(signature_widths[name]) + 1,
|
||||
);
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i, (name, module)) in justfile.modules.iter().enumerate() {
|
||||
if i + groups.len() > 0 {
|
||||
println!();
|
||||
}
|
||||
|
||||
for (name, module) in &justfile.modules {
|
||||
println!("{}{name}:", config.list_prefix.repeat(level + 1));
|
||||
Self::list(config, level + 1, module);
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ fn unexpected_attribute_argument() {
|
||||
)
|
||||
.stderr(
|
||||
"
|
||||
error: Attribute `private` specified with argument but takes no arguments
|
||||
error: Attribute `private` got 1 argument but takes 0 arguments
|
||||
——▶ justfile:1:2
|
||||
│
|
||||
1 │ [private('foo')]
|
||||
|
174
tests/groups.rs
Normal file
174
tests/groups.rs
Normal file
@ -0,0 +1,174 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn list_with_groups() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[group('alpha')]
|
||||
a:
|
||||
# Doc comment
|
||||
[group('alpha')]
|
||||
[group('beta')]
|
||||
b:
|
||||
c:
|
||||
[group('multi word group')]
|
||||
d:
|
||||
[group('alpha')]
|
||||
e:
|
||||
[group('beta')]
|
||||
[group('alpha')]
|
||||
f:
|
||||
",
|
||||
)
|
||||
.arg("--list")
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
(no group)
|
||||
c
|
||||
|
||||
[alpha]
|
||||
a
|
||||
b # Doc comment
|
||||
e
|
||||
f
|
||||
|
||||
[beta]
|
||||
b # Doc comment
|
||||
f
|
||||
|
||||
[multi word group]
|
||||
d
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_with_groups_unsorted() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[group('beta')]
|
||||
[group('alpha')]
|
||||
f:
|
||||
|
||||
[group('alpha')]
|
||||
e:
|
||||
|
||||
[group('multi word group')]
|
||||
d:
|
||||
|
||||
c:
|
||||
|
||||
# Doc comment
|
||||
[group('alpha')]
|
||||
[group('beta')]
|
||||
b:
|
||||
|
||||
[group('alpha')]
|
||||
a:
|
||||
|
||||
",
|
||||
)
|
||||
.args(["--list", "--unsorted"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
(no group)
|
||||
c
|
||||
|
||||
[alpha]
|
||||
f
|
||||
e
|
||||
b # Doc comment
|
||||
a
|
||||
|
||||
[beta]
|
||||
f
|
||||
b # Doc comment
|
||||
|
||||
[multi word group]
|
||||
d
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_groups() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[group('B')]
|
||||
bar:
|
||||
|
||||
[group('A')]
|
||||
[group('B')]
|
||||
foo:
|
||||
|
||||
",
|
||||
)
|
||||
.args(["--groups"])
|
||||
.stdout(
|
||||
"
|
||||
Recipe groups:
|
||||
A
|
||||
B
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_groups_with_custom_prefix() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[group('B')]
|
||||
foo:
|
||||
|
||||
[group('A')]
|
||||
[group('B')]
|
||||
bar:
|
||||
",
|
||||
)
|
||||
.args(["--groups", "--list-prefix", "..."])
|
||||
.stdout(
|
||||
"
|
||||
Recipe groups:
|
||||
...A
|
||||
...B
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_with_groups_in_modules() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[group('FOO')]
|
||||
foo:
|
||||
|
||||
mod bar
|
||||
",
|
||||
)
|
||||
.write("bar.just", "[group('BAZ')]\nbaz:")
|
||||
.test_round_trip(false)
|
||||
.args(["--unstable", "--list"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
[FOO]
|
||||
foo
|
||||
|
||||
bar:
|
||||
[BAZ]
|
||||
baz
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
@ -61,6 +61,7 @@ mod fmt;
|
||||
mod functions;
|
||||
#[cfg(unix)]
|
||||
mod global;
|
||||
mod groups;
|
||||
mod ignore_comments;
|
||||
mod imports;
|
||||
mod init;
|
||||
|
@ -22,6 +22,34 @@ fn list_displays_recipes_in_submodules() {
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modules_are_space_separated_in_output() {
|
||||
Test::new()
|
||||
.write("foo.just", "foo:")
|
||||
.write("bar.just", "bar:")
|
||||
.justfile(
|
||||
"
|
||||
mod foo
|
||||
|
||||
mod bar
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("--list")
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
bar:
|
||||
bar
|
||||
|
||||
foo:
|
||||
foo
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_recipe_list_alignment_ignores_private_recipes() {
|
||||
Test::new()
|
||||
|
Loading…
Reference in New Issue
Block a user