Skip to content

Commit

Permalink
Add recipe groups (#1842)
Browse files Browse the repository at this point in the history
  • Loading branch information
neunenak authored May 25, 2024
1 parent 1654d14 commit ed0dc20
Show file tree
Hide file tree
Showing 19 changed files with 534 additions and 108 deletions.
69 changes: 68 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion completions/just.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions completions/just.elvish
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions completions/just.fish
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions completions/just.powershell
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
1 change: 1 addition & 0 deletions completions/just.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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]' \
Expand Down
98 changes: 79 additions & 19 deletions src/attribute.rs
Original file line number Diff line number Diff line change
@@ -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<'src> Attribute<'src> {
pub(crate) fn from_name(name: Name) -> Option<Self> {
name.lexeme().parse().ok()
}

pub(crate) fn name(&self) -> &'static str {
self.into()
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,
}
}
}

pub(crate) fn with_argument(
self,
impl<'src> Attribute<'src> {
pub(crate) fn new(
name: Name<'src>,
argument: StringLiteral<'src>,
argument: Option<StringLiteral<'src>>,
) -> CompileResult<'src, Self> {
match self {
Self::Confirm(_) => Ok(Self::Confirm(Some(argument))),
_ => Err(name.error(CompileErrorKind::UnexpectedAttributeArgument { attribute: 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()
}

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,
}
}
}
Expand Down
28 changes: 21 additions & 7 deletions src/compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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())
Expand Down
9 changes: 6 additions & 3 deletions src/compile_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -88,9 +94,6 @@ pub(crate) enum CompileErrorKind<'src> {
UndefinedVariable {
variable: &'src str,
},
UnexpectedAttributeArgument {
attribute: Attribute<'src>,
},
UnexpectedCharacter {
expected: char,
},
Expand Down
9 changes: 9 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions src/justfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
Expand Down
Loading

0 comments on commit ed0dc20

Please sign in to comment.