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 |
|
| Name | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `[confirm]`<sup>1.17.0</sup> | Require confirmation prior to executing recipe. |
|
| `[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. |
|
| `[linux]`<sup>1.8.0</sup> | Enable recipe on Linux. |
|
||||||
| `[macos]`<sup>1.8.0</sup> | Enable recipe on MacOS. |
|
| `[macos]`<sup>1.8.0</sup> | Enable recipe on MacOS. |
|
||||||
| `[no-cd]`<sup>1.9.0</sup> | Don't change directory before executing recipe. |
|
| `[no-cd]`<sup>1.9.0</sup> | Don't change directory before executing recipe. |
|
||||||
@ -1709,6 +1710,72 @@ delete-everything:
|
|||||||
rm -rf *
|
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
|
### Command Evaluation Using Backticks
|
||||||
|
|
||||||
Backticks can be used to store the result of commands:
|
Backticks can be used to store the result of commands:
|
||||||
|
@ -30,7 +30,7 @@ _just() {
|
|||||||
|
|
||||||
case "${cmd}" in
|
case "${cmd}" in
|
||||||
"$1")
|
"$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
|
if [[ ${cur} == -* ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
@ -66,6 +66,7 @@ set edit:completion:arg-completer[just] = {|@words|
|
|||||||
cand --init 'Initialize new justfile in project root'
|
cand --init 'Initialize new justfile in project root'
|
||||||
cand -l 'List available recipes and their arguments'
|
cand -l 'List available recipes and their arguments'
|
||||||
cand --list '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 --man 'Print man page'
|
||||||
cand --summary 'List names of available recipes'
|
cand --summary 'List names of available recipes'
|
||||||
cand --variables 'List names of variables'
|
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 fmt -d 'Format and overwrite justfile'
|
||||||
complete -c just -l init -d 'Initialize new justfile in project root'
|
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 -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 man -d 'Print man page'
|
||||||
complete -c just -l summary -d 'List names of available recipes'
|
complete -c just -l summary -d 'List names of available recipes'
|
||||||
complete -c just -l variables -d 'List names of variables'
|
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('--init', 'init', [CompletionResultType]::ParameterName, 'Initialize new justfile in project root')
|
||||||
[CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List available recipes and their arguments')
|
[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('--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('--man', 'man', [CompletionResultType]::ParameterName, 'Print man page')
|
||||||
[CompletionResult]::new('--summary', 'summary', [CompletionResultType]::ParameterName, 'List names of available recipes')
|
[CompletionResult]::new('--summary', 'summary', [CompletionResultType]::ParameterName, 'List names of available recipes')
|
||||||
[CompletionResult]::new('--variables', 'variables', [CompletionResultType]::ParameterName, 'List names of variables')
|
[CompletionResult]::new('--variables', 'variables', [CompletionResultType]::ParameterName, 'List names of variables')
|
||||||
|
@ -64,6 +64,7 @@ _just() {
|
|||||||
'--init[Initialize new justfile in project root]' \
|
'--init[Initialize new justfile in project root]' \
|
||||||
'-l[List available recipes and their arguments]' \
|
'-l[List available recipes and their arguments]' \
|
||||||
'--list[List available recipes and their arguments]' \
|
'--list[List available recipes and their arguments]' \
|
||||||
|
'--groups[List recipe groups]' \
|
||||||
'--man[Print man page]' \
|
'--man[Print man page]' \
|
||||||
'--summary[List names of available recipes]' \
|
'--summary[List names of available recipes]' \
|
||||||
'--variables[List names of variables]' \
|
'--variables[List names of variables]' \
|
||||||
|
@ -1,45 +1,105 @@
|
|||||||
use super::*;
|
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")]
|
#[strum(serialize_all = "kebab-case")]
|
||||||
#[serde(rename_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> {
|
pub(crate) enum Attribute<'src> {
|
||||||
Confirm(Option<StringLiteral<'src>>),
|
Confirm(Option<StringLiteral<'src>>),
|
||||||
|
Group(StringLiteral<'src>),
|
||||||
Linux,
|
Linux,
|
||||||
Macos,
|
Macos,
|
||||||
NoCd,
|
NoCd,
|
||||||
NoExitMessage,
|
NoExitMessage,
|
||||||
Private,
|
|
||||||
NoQuiet,
|
NoQuiet,
|
||||||
|
Private,
|
||||||
Unix,
|
Unix,
|
||||||
Windows,
|
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> {
|
impl<'src> Attribute<'src> {
|
||||||
pub(crate) fn from_name(name: Name) -> Option<Self> {
|
pub(crate) fn new(
|
||||||
name.lexeme().parse().ok()
|
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 {
|
pub(crate) fn name(&self) -> &'static str {
|
||||||
self.into()
|
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> {
|
fn argument(&self) -> Option<&StringLiteral> {
|
||||||
if let Self::Confirm(prompt) = self {
|
match self {
|
||||||
prompt.as_ref()
|
Self::Confirm(prompt) => prompt.as_ref(),
|
||||||
} else {
|
Self::Group(name) => Some(name),
|
||||||
None
|
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(),
|
self.token.line.ordinal(),
|
||||||
recipe_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 `#!`"),
|
BacktickShebang => write!(f, "Backticks may not start with `#!`"),
|
||||||
CircularRecipeDependency { recipe, ref circle } => {
|
CircularRecipeDependency { recipe, ref circle } => {
|
||||||
if circle.len() == 2 {
|
if circle.len() == 2 {
|
||||||
@ -212,13 +233,6 @@ impl Display for CompileError<'_> {
|
|||||||
"Non-default parameter `{parameter}` follows default parameter"
|
"Non-default parameter `{parameter}` follows default parameter"
|
||||||
),
|
),
|
||||||
UndefinedVariable { variable } => write!(f, "Variable `{variable}` not defined"),
|
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}`"),
|
UnexpectedCharacter { expected } => write!(f, "Expected character `{expected}`"),
|
||||||
UnexpectedClosingDelimiter { close } => {
|
UnexpectedClosingDelimiter { close } => {
|
||||||
write!(f, "Unexpected closing delimiter `{}`", close.close())
|
write!(f, "Unexpected closing delimiter `{}`", close.close())
|
||||||
|
@ -10,6 +10,12 @@ pub(crate) enum CompileErrorKind<'src> {
|
|||||||
alias: &'src str,
|
alias: &'src str,
|
||||||
recipe_line: usize,
|
recipe_line: usize,
|
||||||
},
|
},
|
||||||
|
AttributeArgumentCountMismatch {
|
||||||
|
attribute: &'src str,
|
||||||
|
found: usize,
|
||||||
|
min: usize,
|
||||||
|
max: usize,
|
||||||
|
},
|
||||||
BacktickShebang,
|
BacktickShebang,
|
||||||
CircularRecipeDependency {
|
CircularRecipeDependency {
|
||||||
recipe: &'src str,
|
recipe: &'src str,
|
||||||
@ -88,9 +94,6 @@ pub(crate) enum CompileErrorKind<'src> {
|
|||||||
UndefinedVariable {
|
UndefinedVariable {
|
||||||
variable: &'src str,
|
variable: &'src str,
|
||||||
},
|
},
|
||||||
UnexpectedAttributeArgument {
|
|
||||||
attribute: Attribute<'src>,
|
|
||||||
},
|
|
||||||
UnexpectedCharacter {
|
UnexpectedCharacter {
|
||||||
expected: char,
|
expected: char,
|
||||||
},
|
},
|
||||||
|
@ -54,6 +54,7 @@ mod cmd {
|
|||||||
pub(crate) const EDIT: &str = "EDIT";
|
pub(crate) const EDIT: &str = "EDIT";
|
||||||
pub(crate) const EVALUATE: &str = "EVALUATE";
|
pub(crate) const EVALUATE: &str = "EVALUATE";
|
||||||
pub(crate) const FORMAT: &str = "FORMAT";
|
pub(crate) const FORMAT: &str = "FORMAT";
|
||||||
|
pub(crate) const GROUPS: &str = "GROUPS";
|
||||||
pub(crate) const INIT: &str = "INIT";
|
pub(crate) const INIT: &str = "INIT";
|
||||||
pub(crate) const LIST: &str = "LIST";
|
pub(crate) const LIST: &str = "LIST";
|
||||||
pub(crate) const MAN: &str = "MAN";
|
pub(crate) const MAN: &str = "MAN";
|
||||||
@ -417,6 +418,12 @@ impl Config {
|
|||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.help("List available recipes and their arguments"),
|
.help("List available recipes and their arguments"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(cmd::GROUPS)
|
||||||
|
.long("groups")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.help("List recipe groups")
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(cmd::MAN)
|
Arg::new(cmd::MAN)
|
||||||
.long("man")
|
.long("man")
|
||||||
@ -649,6 +656,8 @@ impl Config {
|
|||||||
Subcommand::Init
|
Subcommand::Init
|
||||||
} else if matches.get_flag(cmd::LIST) {
|
} else if matches.get_flag(cmd::LIST) {
|
||||||
Subcommand::List
|
Subcommand::List
|
||||||
|
} else if matches.get_flag(cmd::GROUPS) {
|
||||||
|
Subcommand::Groups
|
||||||
} else if matches.get_flag(cmd::MAN) {
|
} else if matches.get_flag(cmd::MAN) {
|
||||||
Subcommand::Man
|
Subcommand::Man
|
||||||
} else if let Some(name) = matches.get_one::<String>(cmd::SHOW).map(Into::into) {
|
} else if let Some(name) = matches.get_one::<String>(cmd::SHOW).map(Into::into) {
|
||||||
|
@ -488,6 +488,16 @@ impl<'src> Justfile<'src> {
|
|||||||
|
|
||||||
recipes
|
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> {
|
impl<'src> ColorDisplay for Justfile<'src> {
|
||||||
|
@ -71,7 +71,7 @@ pub(crate) use {
|
|||||||
Serialize, Serializer,
|
Serialize, Serializer,
|
||||||
},
|
},
|
||||||
snafu::{ResultExt, Snafu},
|
snafu::{ResultExt, Snafu},
|
||||||
strum::{Display, EnumString, IntoStaticStr},
|
strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr},
|
||||||
typed_arena::Arena,
|
typed_arena::Arena,
|
||||||
unicode_width::{UnicodeWidthChar, UnicodeWidthStr},
|
unicode_width::{UnicodeWidthChar, UnicodeWidthStr},
|
||||||
},
|
},
|
||||||
|
@ -976,11 +976,17 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||||||
while self.accepted(BracketL)? {
|
while self.accepted(BracketL)? {
|
||||||
loop {
|
loop {
|
||||||
let name = self.parse_name()?;
|
let name = self.parse_name()?;
|
||||||
let attribute = Attribute::from_name(name).ok_or_else(|| {
|
|
||||||
name.error(CompileErrorKind::UnknownAttribute {
|
let argument = if self.accepted(ParenL)? {
|
||||||
attribute: name.lexeme(),
|
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) {
|
if let Some(line) = attributes.get(&attribute) {
|
||||||
return Err(name.error(CompileErrorKind::DuplicateAttribute {
|
return Err(name.error(CompileErrorKind::DuplicateAttribute {
|
||||||
attribute: name.lexeme(),
|
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);
|
attributes.insert(attribute, name.line);
|
||||||
|
|
||||||
if !self.accepted(Comma)? {
|
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> {
|
impl<'src, D: Display> ColorDisplay for Recipe<'src, D> {
|
||||||
|
@ -29,6 +29,7 @@ pub(crate) enum Subcommand {
|
|||||||
variable: Option<String>,
|
variable: Option<String>,
|
||||||
},
|
},
|
||||||
Format,
|
Format,
|
||||||
|
Groups,
|
||||||
Init,
|
Init,
|
||||||
List,
|
List,
|
||||||
Man,
|
Man,
|
||||||
@ -86,6 +87,7 @@ impl Subcommand {
|
|||||||
}
|
}
|
||||||
Dump => Self::dump(config, ast, justfile)?,
|
Dump => Self::dump(config, ast, justfile)?,
|
||||||
Format => Self::format(config, &search, src, ast)?,
|
Format => Self::format(config, &search, src, ast)?,
|
||||||
|
Groups => Self::groups(config, justfile),
|
||||||
List => Self::list(config, 0, justfile),
|
List => Self::list(config, 0, justfile),
|
||||||
Show { ref name } => Self::show(config, name, justfile)?,
|
Show { ref name } => Self::show(config, name, justfile)?,
|
||||||
Summary => Self::summary(config, justfile),
|
Summary => Self::summary(config, justfile),
|
||||||
@ -96,6 +98,13 @@ impl Subcommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn groups(config: &Config, justfile: &Justfile) {
|
||||||
|
println!("Recipe groups:");
|
||||||
|
for group in justfile.public_groups() {
|
||||||
|
println!("{}{group}", config.list_prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn run<'src>(
|
fn run<'src>(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
loader: &'src Loader,
|
loader: &'src Loader,
|
||||||
@ -469,90 +478,125 @@ impl Subcommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn list(config: &Config, level: usize, justfile: &Justfile) {
|
fn list(config: &Config, level: usize, justfile: &Justfile) {
|
||||||
const MAX_WIDTH: usize = 50;
|
let aliases = if config.no_aliases {
|
||||||
|
BTreeMap::new()
|
||||||
|
} else {
|
||||||
|
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 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(aliases.get(name).unwrap_or(&Vec::new())) {
|
||||||
|
signature_widths.insert(
|
||||||
|
name,
|
||||||
|
UnicodeWidthStr::width(
|
||||||
|
RecipeSignature { name, recipe }
|
||||||
|
.color_display(Color::never())
|
||||||
|
.to_string()
|
||||||
|
.as_str(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signature_widths
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_signature_width = signature_widths
|
||||||
|
.values()
|
||||||
|
.copied()
|
||||||
|
.filter(|width| *width <= 50)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
if level == 0 {
|
if level == 0 {
|
||||||
print!("{}", config.list_heading);
|
print!("{}", config.list_heading);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct a target to alias map.
|
let groups = {
|
||||||
let mut recipe_aliases = BTreeMap::<&str, Vec<&str>>::new();
|
let mut groups = BTreeMap::<Option<String>, Vec<&Recipe>>::new();
|
||||||
if !config.no_aliases {
|
for recipe in justfile.public_recipes(config.unsorted) {
|
||||||
for alias in justfile.aliases.values() {
|
let recipe_groups = recipe.groups();
|
||||||
if alias.is_private() {
|
if recipe_groups.is_empty() {
|
||||||
continue;
|
groups.entry(None).or_default().push(recipe);
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
} else {
|
} else {
|
||||||
recipe_aliases.insert(alias.target.name.lexeme(), vec![alias.name.lexeme()]);
|
for group in recipe_groups {
|
||||||
|
groups.entry(Some(group)).or_default().push(recipe);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
groups
|
||||||
|
};
|
||||||
let mut line_widths = BTreeMap::<&str, usize>::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(
|
|
||||||
name,
|
|
||||||
UnicodeWidthStr::width(
|
|
||||||
RecipeSignature { name, recipe }
|
|
||||||
.color_display(Color::never())
|
|
||||||
.to_string()
|
|
||||||
.as_str(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let max_line_width = line_widths
|
|
||||||
.values()
|
|
||||||
.filter(|line_width| **line_width <= MAX_WIDTH)
|
|
||||||
.copied()
|
|
||||||
.max()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
for recipe in justfile.public_recipes(config.unsorted) {
|
|
||||||
let name = recipe.name();
|
|
||||||
|
|
||||||
for (i, name) in iter::once(&name)
|
|
||||||
.chain(recipe_aliases.get(name).unwrap_or(&Vec::new()))
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
print!(
|
|
||||||
"{}{}",
|
|
||||||
config.list_prefix.repeat(level + 1),
|
|
||||||
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))),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(doc) = doc {
|
|
||||||
print!(
|
|
||||||
" {:padding$}{} {}",
|
|
||||||
"",
|
|
||||||
config.color.stdout().doc().paint("#"),
|
|
||||||
config.color.stdout().doc().paint(&doc),
|
|
||||||
padding = max_line_width.saturating_sub(line_widths[name]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for (i, (group, recipes)) in groups.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
println!();
|
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!(
|
||||||
|
"{}{}",
|
||||||
|
config.list_prefix.repeat(level + 1),
|
||||||
|
RecipeSignature { name, recipe }.color_display(config.color.stdout())
|
||||||
|
);
|
||||||
|
|
||||||
|
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$}{} {}",
|
||||||
|
"",
|
||||||
|
config.color.stdout().doc().paint("#"),
|
||||||
|
config.color.stdout().doc().paint(&doc),
|
||||||
|
padding = max_signature_width.saturating_sub(signature_widths[name]) + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (name, module) in &justfile.modules {
|
for (i, (name, module)) in justfile.modules.iter().enumerate() {
|
||||||
|
if i + groups.len() > 0 {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
println!("{}{name}:", config.list_prefix.repeat(level + 1));
|
println!("{}{name}:", config.list_prefix.repeat(level + 1));
|
||||||
Self::list(config, level + 1, module);
|
Self::list(config, level + 1, module);
|
||||||
}
|
}
|
||||||
|
@ -119,7 +119,7 @@ fn unexpected_attribute_argument() {
|
|||||||
)
|
)
|
||||||
.stderr(
|
.stderr(
|
||||||
"
|
"
|
||||||
error: Attribute `private` specified with argument but takes no arguments
|
error: Attribute `private` got 1 argument but takes 0 arguments
|
||||||
——▶ justfile:1:2
|
——▶ justfile:1:2
|
||||||
│
|
│
|
||||||
1 │ [private('foo')]
|
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;
|
mod functions;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
mod global;
|
mod global;
|
||||||
|
mod groups;
|
||||||
mod ignore_comments;
|
mod ignore_comments;
|
||||||
mod imports;
|
mod imports;
|
||||||
mod init;
|
mod init;
|
||||||
|
@ -22,6 +22,34 @@ fn list_displays_recipes_in_submodules() {
|
|||||||
.run();
|
.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]
|
#[test]
|
||||||
fn module_recipe_list_alignment_ignores_private_recipes() {
|
fn module_recipe_list_alignment_ignores_private_recipes() {
|
||||||
Test::new()
|
Test::new()
|
||||||
|
Loading…
Reference in New Issue
Block a user