Add recipe aliases (#390)
Recipe aliases may be defined with `alias f = foo`, allowing recipes to be called by shorter names on the command line.
This commit is contained in:
parent
37639d68d7
commit
f64f07a0cc
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1,3 +1,5 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.6.4"
|
||||
|
18
README.adoc
18
README.adoc
@ -198,6 +198,24 @@ $ just --summary
|
||||
build test deploy lint
|
||||
```
|
||||
|
||||
=== Aliases
|
||||
|
||||
Aliases allow recipes to be invoked with alternative names:
|
||||
|
||||
```make
|
||||
alias b = build
|
||||
|
||||
build:
|
||||
echo 'Building!'
|
||||
```
|
||||
|
||||
```sh
|
||||
$ just b
|
||||
build
|
||||
echo 'Building!'
|
||||
Building!
|
||||
```
|
||||
|
||||
=== Documentation Comments
|
||||
|
||||
Comments immediately preceding a recipe will appear in `just --list`:
|
||||
|
@ -1,3 +1,3 @@
|
||||
cyclomatic-complexity-threshold = 1337
|
||||
cognitive-complexity-threshold = 1337
|
||||
|
||||
doc-valid-idents = ["FreeBSD"]
|
||||
|
13
src/alias.rs
Normal file
13
src/alias.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use common::*;
|
||||
|
||||
pub struct Alias<'a> {
|
||||
pub name: &'a str,
|
||||
pub target: &'a str,
|
||||
pub line_number: usize,
|
||||
}
|
||||
|
||||
impl<'a> Display for Alias<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "alias {} = {}", self.name, self.target)
|
||||
}
|
||||
}
|
58
src/alias_resolver.rs
Normal file
58
src/alias_resolver.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use common::*;
|
||||
use CompilationErrorKind::*;
|
||||
|
||||
pub struct AliasResolver<'a, 'b>
|
||||
where
|
||||
'a: 'b,
|
||||
{
|
||||
aliases: &'b Map<&'a str, Alias<'a>>,
|
||||
recipes: &'b Map<&'a str, Recipe<'a>>,
|
||||
alias_tokens: &'b Map<&'a str, Token<'a>>,
|
||||
}
|
||||
|
||||
impl<'a: 'b, 'b> AliasResolver<'a, 'b> {
|
||||
pub fn resolve_aliases(
|
||||
aliases: &Map<&'a str, Alias<'a>>,
|
||||
recipes: &Map<&'a str, Recipe<'a>>,
|
||||
alias_tokens: &Map<&'a str, Token<'a>>,
|
||||
) -> CompilationResult<'a, ()> {
|
||||
let resolver = AliasResolver {
|
||||
aliases,
|
||||
recipes,
|
||||
alias_tokens,
|
||||
};
|
||||
|
||||
resolver.resolve()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve(&self) -> CompilationResult<'a, ()> {
|
||||
for alias in self.aliases.values() {
|
||||
self.resolve_alias(alias)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_alias(&self, alias: &Alias<'a>) -> CompilationResult<'a, ()> {
|
||||
let token = self.alias_tokens.get(&alias.name).unwrap();
|
||||
// Make sure the alias doesn't conflict with any recipe
|
||||
if let Some(recipe) = self.recipes.get(alias.name) {
|
||||
return Err(token.error(AliasShadowsRecipe {
|
||||
alias: alias.name,
|
||||
recipe_line: recipe.line_number,
|
||||
}));
|
||||
}
|
||||
|
||||
// Make sure the target recipe exists
|
||||
if self.recipes.get(alias.target).is_none() {
|
||||
return Err(token.error(UnknownAliasTarget {
|
||||
alias: alias.name,
|
||||
target: alias.target,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ pub use std::borrow::Cow;
|
||||
pub use std::collections::{BTreeMap as Map, BTreeSet as Set};
|
||||
pub use std::fmt::Display;
|
||||
pub use std::io::prelude::*;
|
||||
pub use std::ops::Range;
|
||||
pub use std::ops::{Range, RangeInclusive};
|
||||
pub use std::path::{Path, PathBuf};
|
||||
pub use std::process::Command;
|
||||
pub use std::sync::{Mutex, MutexGuard};
|
||||
@ -13,6 +13,8 @@ pub use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
||||
pub use regex::Regex;
|
||||
pub use tempdir::TempDir;
|
||||
|
||||
pub use alias::Alias;
|
||||
pub use alias_resolver::AliasResolver;
|
||||
pub use assignment_evaluator::AssignmentEvaluator;
|
||||
pub use assignment_resolver::AssignmentResolver;
|
||||
pub use command_ext::CommandExt;
|
||||
|
@ -16,6 +16,10 @@ pub struct CompilationError<'a> {
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum CompilationErrorKind<'a> {
|
||||
AliasShadowsRecipe {
|
||||
alias: &'a str,
|
||||
recipe_line: usize,
|
||||
},
|
||||
CircularRecipeDependency {
|
||||
recipe: &'a str,
|
||||
circle: Vec<&'a str>,
|
||||
@ -28,6 +32,10 @@ pub enum CompilationErrorKind<'a> {
|
||||
recipe: &'a str,
|
||||
dependency: &'a str,
|
||||
},
|
||||
DuplicateAlias {
|
||||
alias: &'a str,
|
||||
first: usize,
|
||||
},
|
||||
DuplicateDependency {
|
||||
recipe: &'a str,
|
||||
dependency: &'a str,
|
||||
@ -78,6 +86,10 @@ pub enum CompilationErrorKind<'a> {
|
||||
expected: Vec<TokenKind>,
|
||||
found: TokenKind,
|
||||
},
|
||||
UnknownAliasTarget {
|
||||
alias: &'a str,
|
||||
target: &'a str,
|
||||
},
|
||||
UnknownDependency {
|
||||
recipe: &'a str,
|
||||
unknown: &'a str,
|
||||
@ -99,6 +111,15 @@ impl<'a> Display for CompilationError<'a> {
|
||||
write!(f, "{} {}", error.paint("error:"), message.prefix())?;
|
||||
|
||||
match self.kind {
|
||||
AliasShadowsRecipe { alias, recipe_line } => {
|
||||
writeln!(
|
||||
f,
|
||||
"Alias `{}` defined on `{}` shadows recipe defined on `{}`",
|
||||
alias,
|
||||
self.line + 1,
|
||||
recipe_line + 1,
|
||||
)?;
|
||||
}
|
||||
CircularRecipeDependency { recipe, ref circle } => {
|
||||
if circle.len() == 2 {
|
||||
writeln!(f, "Recipe `{}` depends on itself", recipe)?;
|
||||
@ -153,6 +174,15 @@ impl<'a> Display for CompilationError<'a> {
|
||||
} => {
|
||||
writeln!(f, "Expected {}, but found {}", Or(expected), found)?;
|
||||
}
|
||||
DuplicateAlias { alias, first } => {
|
||||
writeln!(
|
||||
f,
|
||||
"Alias `{}` first defined on line `{}` is redefined on line `{}`",
|
||||
alias,
|
||||
first + 1,
|
||||
self.line + 1,
|
||||
)?;
|
||||
}
|
||||
DuplicateDependency { recipe, dependency } => {
|
||||
writeln!(
|
||||
f,
|
||||
@ -228,6 +258,9 @@ impl<'a> Display for CompilationError<'a> {
|
||||
show_whitespace(found)
|
||||
)?;
|
||||
}
|
||||
UnknownAliasTarget { alias, target } => {
|
||||
writeln!(f, "Alias `{}` has an unknown target `{}`", alias, target)?;
|
||||
}
|
||||
UnknownDependency { recipe, unknown } => {
|
||||
writeln!(
|
||||
f,
|
||||
|
@ -29,7 +29,7 @@ impl<'a> CookedString<'a> {
|
||||
other => {
|
||||
return Err(
|
||||
token.error(CompilationErrorKind::InvalidEscapeSequence { character: other }),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
escape = false;
|
||||
|
@ -6,6 +6,7 @@ pub struct Justfile<'a> {
|
||||
pub recipes: Map<&'a str, Recipe<'a>>,
|
||||
pub assignments: Map<&'a str, Expression<'a>>,
|
||||
pub exports: Set<&'a str>,
|
||||
pub aliases: Map<&'a str, Alias<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Justfile<'a> where {
|
||||
@ -90,13 +91,22 @@ impl<'a> Justfile<'a> where {
|
||||
let mut rest = arguments;
|
||||
|
||||
while let Some((argument, mut tail)) = rest.split_first() {
|
||||
if let Some(recipe) = self.recipes.get(argument) {
|
||||
let get_recipe = |name| {
|
||||
if let Some(recipe) = self.recipes.get(name) {
|
||||
Some(recipe)
|
||||
} else if let Some(alias) = self.aliases.get(name) {
|
||||
self.recipes.get(alias.target)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(recipe) = get_recipe(argument) {
|
||||
if recipe.parameters.is_empty() {
|
||||
grouped.push((recipe, &tail[0..0]));
|
||||
} else {
|
||||
let argument_range = recipe.argument_range();
|
||||
let argument_count = cmp::min(tail.len(), recipe.max_arguments());
|
||||
if !argument_range.range_contains(argument_count) {
|
||||
if !argument_range.range_contains(&argument_count) {
|
||||
return Err(RuntimeError::ArgumentCountMismatch {
|
||||
recipe: recipe.name,
|
||||
parameters: recipe.parameters.iter().collect(),
|
||||
@ -161,7 +171,7 @@ impl<'a> Justfile<'a> where {
|
||||
|
||||
impl<'a> Display for Justfile<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
let mut items = self.recipes.len() + self.assignments.len();
|
||||
let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len();
|
||||
for (name, expression) in &self.assignments {
|
||||
if self.exports.contains(name) {
|
||||
write!(f, "export ")?;
|
||||
@ -172,6 +182,13 @@ impl<'a> Display for Justfile<'a> {
|
||||
write!(f, "\n\n")?;
|
||||
}
|
||||
}
|
||||
for alias in self.aliases.values() {
|
||||
write!(f, "{}",alias)?;
|
||||
items -= 1;
|
||||
if items != 0 {
|
||||
write!(f, "\n\n")?;
|
||||
}
|
||||
}
|
||||
for recipe in self.recipes.values() {
|
||||
write!(f, "{}", recipe)?;
|
||||
items -= 1;
|
||||
|
@ -321,7 +321,7 @@ impl<'a> Lexer<'a> {
|
||||
if escape || content_end >= self.rest.len() {
|
||||
return Err(self.error(UnterminatedString));
|
||||
}
|
||||
(prefix, &self.rest[start..content_end + 1], StringToken)
|
||||
(prefix, &self.rest[start..=content_end], StringToken)
|
||||
} else {
|
||||
return Err(self.error(UnknownStartOfToken));
|
||||
};
|
||||
|
@ -27,6 +27,8 @@ pub mod fuzzing;
|
||||
#[macro_use]
|
||||
mod die;
|
||||
|
||||
mod alias;
|
||||
mod alias_resolver;
|
||||
mod assignment_evaluator;
|
||||
mod assignment_resolver;
|
||||
mod color;
|
||||
|
153
src/parser.rs
153
src/parser.rs
@ -11,6 +11,8 @@ pub struct Parser<'a> {
|
||||
assignments: Map<&'a str, Expression<'a>>,
|
||||
assignment_tokens: Map<&'a str, Token<'a>>,
|
||||
exports: Set<&'a str>,
|
||||
aliases: Map<&'a str, Alias<'a>>,
|
||||
alias_tokens: Map<&'a str, Token<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
@ -27,6 +29,8 @@ impl<'a> Parser<'a> {
|
||||
assignments: empty(),
|
||||
assignment_tokens: empty(),
|
||||
exports: empty(),
|
||||
aliases: empty(),
|
||||
alias_tokens: empty(),
|
||||
text,
|
||||
}
|
||||
}
|
||||
@ -342,6 +346,41 @@ impl<'a> Parser<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn alias(&mut self, name: Token<'a>) -> CompilationResult<'a, ()> {
|
||||
// Make sure alias doesn't already exist
|
||||
if let Some(alias) = self.aliases.get(name.lexeme) {
|
||||
return Err(name.error(DuplicateAlias {
|
||||
alias: alias.name,
|
||||
first: alias.line_number,
|
||||
}));
|
||||
}
|
||||
|
||||
// Make sure the next token is of kind Name and keep it
|
||||
let target = if let Some(next) = self.accept(Name) {
|
||||
next.lexeme
|
||||
} else {
|
||||
let unexpected = self.tokens.next().unwrap();
|
||||
return Err(self.unexpected_token(&unexpected, &[Name]));
|
||||
};
|
||||
|
||||
// Make sure this is where the line or file ends without any unexpected tokens.
|
||||
if let Some(token) = self.expect_eol() {
|
||||
return Err(self.unexpected_token(&token, &[Eol, Eof]));
|
||||
}
|
||||
|
||||
self.aliases.insert(
|
||||
name.lexeme,
|
||||
Alias {
|
||||
name: name.lexeme,
|
||||
line_number: name.line,
|
||||
target,
|
||||
},
|
||||
);
|
||||
self.alias_tokens.insert(name.lexeme, name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn justfile(mut self) -> CompilationResult<'a, Justfile<'a>> {
|
||||
let mut doc = None;
|
||||
loop {
|
||||
@ -380,6 +419,16 @@ impl<'a> Parser<'a> {
|
||||
self.recipe(&token, doc, false)?;
|
||||
doc = None;
|
||||
}
|
||||
} else if token.lexeme == "alias" {
|
||||
let next = self.tokens.next().unwrap();
|
||||
if next.kind == Name && self.accepted(Equals) {
|
||||
self.alias(next)?;
|
||||
doc = None;
|
||||
} else {
|
||||
self.tokens.put_back(next);
|
||||
self.recipe(&token, doc, false)?;
|
||||
doc = None;
|
||||
}
|
||||
} else if self.accepted(Equals) {
|
||||
self.assignment(token, false)?;
|
||||
doc = None;
|
||||
@ -400,7 +449,7 @@ impl<'a> Parser<'a> {
|
||||
kind: Internal {
|
||||
message: "unexpected end of token stream".to_string(),
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -435,12 +484,15 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
AliasResolver::resolve_aliases(&self.aliases, &self.recipes, &self.alias_tokens)?;
|
||||
|
||||
AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?;
|
||||
|
||||
Ok(Justfile {
|
||||
recipes: self.recipes,
|
||||
assignments: self.assignments,
|
||||
exports: self.exports,
|
||||
aliases: self.aliases,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -532,6 +584,45 @@ export a = "hello"
|
||||
r#"export a = "hello""#,
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
parse_alias_after_target,
|
||||
r#"
|
||||
foo:
|
||||
echo a
|
||||
alias f = foo
|
||||
"#,
|
||||
r#"alias f = foo
|
||||
|
||||
foo:
|
||||
echo a"#
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
parse_alias_before_target,
|
||||
r#"
|
||||
alias f = foo
|
||||
foo:
|
||||
echo a
|
||||
"#,
|
||||
r#"alias f = foo
|
||||
|
||||
foo:
|
||||
echo a"#
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
parse_alias_with_comment,
|
||||
r#"
|
||||
alias f = foo #comment
|
||||
foo:
|
||||
echo a
|
||||
"#,
|
||||
r#"alias f = foo
|
||||
|
||||
foo:
|
||||
echo a"#
|
||||
}
|
||||
|
||||
summary_test! {
|
||||
parse_complex,
|
||||
"
|
||||
@ -680,6 +771,66 @@ a:
|
||||
{{env_var_or_default("foo" + "bar", "baz")}} {{env_var(env_var("baz"))}}"#,
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
name: duplicate_alias,
|
||||
input: "alias foo = bar\nalias foo = baz",
|
||||
index: 22,
|
||||
line: 1,
|
||||
column: 6,
|
||||
width: Some(3),
|
||||
kind: DuplicateAlias { alias: "foo", first: 0 },
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
name: alias_syntax_multiple_rhs,
|
||||
input: "alias foo = bar baz",
|
||||
index: 16,
|
||||
line: 0,
|
||||
column: 16,
|
||||
width: Some(3),
|
||||
kind: UnexpectedToken { expected: vec![Eol, Eof], found: Name },
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
name: alias_syntax_no_rhs,
|
||||
input: "alias foo = \n",
|
||||
index: 12,
|
||||
line: 0,
|
||||
column: 12,
|
||||
width: Some(1),
|
||||
kind: UnexpectedToken {expected: vec![Name], found:Eol},
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
name: unknown_alias_target,
|
||||
input: "alias foo = bar\n",
|
||||
index: 6,
|
||||
line: 0,
|
||||
column: 6,
|
||||
width: Some(3),
|
||||
kind: UnknownAliasTarget {alias: "foo", target: "bar"},
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
name: alias_shadows_recipe_before,
|
||||
input: "bar: \n echo bar\nalias foo = bar\nfoo:\n echo foo",
|
||||
index: 23,
|
||||
line: 2,
|
||||
column: 6,
|
||||
width: Some(3),
|
||||
kind: AliasShadowsRecipe {alias: "foo", recipe_line: 3},
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
name: alias_shadows_recipe_after,
|
||||
input: "foo:\n echo foo\nalias foo = bar\nbar:\n echo bar",
|
||||
index: 22,
|
||||
line: 2,
|
||||
column: 6,
|
||||
width: Some(3),
|
||||
kind: AliasShadowsRecipe { alias: "foo", recipe_line: 0 },
|
||||
}
|
||||
|
||||
compilation_error_test! {
|
||||
name: missing_colon,
|
||||
input: "a b c\nd e f",
|
||||
|
@ -1,15 +1,24 @@
|
||||
use common::*;
|
||||
|
||||
pub trait RangeExt<T> {
|
||||
fn range_contains(&self, i: T) -> bool;
|
||||
fn range_contains(&self, i: &T) -> bool;
|
||||
}
|
||||
|
||||
impl<T> RangeExt<T> for Range<T>
|
||||
where
|
||||
T: PartialOrd + Copy,
|
||||
{
|
||||
fn range_contains(&self, i: T) -> bool {
|
||||
i >= self.start && i < self.end
|
||||
fn range_contains(&self, i: &T) -> bool {
|
||||
i >= &self.start && i < &self.end
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> RangeExt<T> for RangeInclusive<T>
|
||||
where
|
||||
T: PartialOrd + Copy,
|
||||
{
|
||||
fn range_contains(&self, i: &T) -> bool {
|
||||
i >= self.start() && i <= self.end()
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,10 +28,19 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn range() {
|
||||
assert!((0..1).range_contains(0));
|
||||
assert!((10..20).range_contains(15));
|
||||
assert!(!(0..0).range_contains(0));
|
||||
assert!(!(1..10).range_contains(0));
|
||||
assert!(!(1..10).range_contains(10));
|
||||
assert!((0..1).range_contains(&0));
|
||||
assert!((10..20).range_contains(&15));
|
||||
assert!(!(0..0).range_contains(&0));
|
||||
assert!(!(1..10).range_contains(&0));
|
||||
assert!(!(1..10).range_contains(&10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_inclusive() {
|
||||
assert!((0..=10).range_contains(&0));
|
||||
assert!((0..=10).range_contains(&7));
|
||||
assert!((0..=10).range_contains(&10));
|
||||
assert!(!(0..=10).range_contains(&11));
|
||||
assert!(!(5..=10).range_contains(&4));
|
||||
}
|
||||
}
|
||||
|
@ -45,8 +45,8 @@ pub struct RecipeContext<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Recipe<'a> {
|
||||
pub fn argument_range(&self) -> Range<usize> {
|
||||
self.min_arguments()..self.max_arguments() + 1
|
||||
pub fn argument_range(&self) -> RangeInclusive<usize> {
|
||||
self.min_arguments()..=self.max_arguments()
|
||||
}
|
||||
|
||||
pub fn min_arguments(&self) -> usize {
|
||||
@ -94,7 +94,7 @@ impl<'a> Recipe<'a> {
|
||||
None => {
|
||||
return Err(RuntimeError::Internal {
|
||||
message: "missing parameter without default".to_string(),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if parameter.variadic {
|
||||
@ -226,7 +226,7 @@ impl<'a> Recipe<'a> {
|
||||
command: interpreter.to_string(),
|
||||
argument: argument.map(String::from),
|
||||
io_error,
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
} else {
|
||||
@ -305,7 +305,7 @@ impl<'a> Recipe<'a> {
|
||||
return Err(RuntimeError::IoError {
|
||||
recipe: self.name,
|
||||
io_error,
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
||||
return Err(dependency_token.error(UnknownDependency {
|
||||
recipe: recipe.name,
|
||||
unknown: dependency_token.lexeme,
|
||||
}))
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
74
src/run.rs
74
src/run.rs
@ -356,6 +356,17 @@ pub fn run() {
|
||||
}
|
||||
|
||||
if matches.is_present("LIST") {
|
||||
// Construct a target to alias map.
|
||||
let mut recipe_aliases: Map<&str, Vec<&str>> = Map::new();
|
||||
for alias in justfile.aliases.values() {
|
||||
if !recipe_aliases.contains_key(alias.target) {
|
||||
recipe_aliases.insert(alias.target, vec![alias.name]);
|
||||
} else {
|
||||
let aliases = recipe_aliases.get_mut(alias.target).unwrap();
|
||||
aliases.push(alias.name);
|
||||
}
|
||||
}
|
||||
|
||||
let mut line_widths: Map<&str, usize> = Map::new();
|
||||
|
||||
for (name, recipe) in &justfile.recipes {
|
||||
@ -363,14 +374,16 @@ pub fn run() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut line_width = UnicodeWidthStr::width(*name);
|
||||
for name in iter::once(name).chain(recipe_aliases.get(name).unwrap_or(&Vec::new())) {
|
||||
let mut line_width = UnicodeWidthStr::width(*name);
|
||||
|
||||
for parameter in &recipe.parameters {
|
||||
line_width += UnicodeWidthStr::width(format!(" {}", parameter).as_str());
|
||||
}
|
||||
for parameter in &recipe.parameters {
|
||||
line_width += UnicodeWidthStr::width(format!(" {}", parameter).as_str());
|
||||
}
|
||||
|
||||
if line_width <= 30 {
|
||||
line_widths.insert(name, line_width);
|
||||
if line_width <= 30 {
|
||||
line_widths.insert(name, line_width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -378,30 +391,47 @@ pub fn run() {
|
||||
|
||||
let doc_color = color.stdout().doc();
|
||||
println!("Available recipes:");
|
||||
|
||||
for (name, recipe) in &justfile.recipes {
|
||||
if recipe.private {
|
||||
continue;
|
||||
}
|
||||
print!(" {}", name);
|
||||
for parameter in &recipe.parameters {
|
||||
if color.stdout().active() {
|
||||
print!(" {:#}", parameter);
|
||||
} else {
|
||||
print!(" {}", parameter);
|
||||
|
||||
let alias_doc = format!("alias for `{}`", recipe.name);
|
||||
|
||||
for (i, name) in iter::once(name).chain(recipe_aliases.get(name).unwrap_or(&Vec::new())).enumerate() {
|
||||
print!(" {}", name);
|
||||
for parameter in &recipe.parameters {
|
||||
if color.stdout().active() {
|
||||
print!(" {:#}", parameter);
|
||||
} else {
|
||||
print!(" {}", parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(doc) = recipe.doc {
|
||||
print!(
|
||||
" {:padding$}{} {}",
|
||||
"",
|
||||
doc_color.paint("#"),
|
||||
doc_color.paint(doc),
|
||||
padding =
|
||||
|
||||
// Declaring this outside of the nested loops will probably be more efficient, but
|
||||
// it creates all sorts of lifetime issues with variables inside the loops.
|
||||
// If this is inlined like the docs say, it shouldn't make any difference.
|
||||
let print_doc = |doc| {
|
||||
print!(
|
||||
" {:padding$}{} {}",
|
||||
"",
|
||||
doc_color.paint("#"),
|
||||
doc_color.paint(doc),
|
||||
padding =
|
||||
max_line_width.saturating_sub(line_widths.get(name).cloned().unwrap_or(max_line_width))
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
match (i, recipe.doc) {
|
||||
(0, Some(doc)) => print_doc(doc),
|
||||
(0, None) => (),
|
||||
_ => print_doc(&alias_doc),
|
||||
}
|
||||
println!();
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
process::exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
|
@ -101,6 +101,109 @@ fn integration_test(
|
||||
}
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: alias_listing,
|
||||
justfile: "foo:\n echo foo\nalias f = foo",
|
||||
args: ("--list"),
|
||||
stdout: "Available recipes:
|
||||
foo
|
||||
f # alias for `foo`
|
||||
",
|
||||
stderr: "",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: alias_listing_multiple_aliases,
|
||||
justfile: "foo:\n echo foo\nalias f = foo\nalias fo = foo",
|
||||
args: ("--list"),
|
||||
stdout: "Available recipes:
|
||||
foo
|
||||
f # alias for `foo`
|
||||
fo # alias for `foo`
|
||||
",
|
||||
stderr: "",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: alias_listing_parameters,
|
||||
justfile: "foo PARAM='foo':\n echo {{PARAM}}\nalias f = foo",
|
||||
args: ("--list"),
|
||||
stdout: "Available recipes:
|
||||
foo PARAM='foo'
|
||||
f PARAM='foo' # alias for `foo`
|
||||
",
|
||||
stderr: "",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: alias,
|
||||
justfile: "foo:\n echo foo\nalias f = foo",
|
||||
args: ("f"),
|
||||
stdout: "foo\n",
|
||||
stderr: "echo foo\n",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: alias_with_parameters,
|
||||
justfile: "foo value='foo':\n echo {{value}}\nalias f = foo",
|
||||
args: ("f", "bar"),
|
||||
stdout: "bar\n",
|
||||
stderr: "echo bar\n",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: alias_with_dependencies,
|
||||
justfile: "foo:\n echo foo\nbar: foo\nalias b = bar",
|
||||
args: ("b"),
|
||||
stdout: "foo\n",
|
||||
stderr: "echo foo\n",
|
||||
status: EXIT_SUCCESS,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: duplicate_alias,
|
||||
justfile: "alias foo = bar\nalias foo = baz\n",
|
||||
args: (),
|
||||
stdout: "" ,
|
||||
stderr: "error: Alias `foo` first defined on line `1` is redefined on line `2`
|
||||
|
|
||||
2 | alias foo = baz
|
||||
| ^^^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: unknown_alias_target,
|
||||
justfile: "alias foo = bar\n",
|
||||
args: (),
|
||||
stdout: "",
|
||||
stderr: "error: Alias `foo` has an unknown target `bar`
|
||||
|
|
||||
1 | alias foo = bar
|
||||
| ^^^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: alias_shadows_recipe,
|
||||
justfile: "bar:\n echo bar\nalias foo = bar\nfoo:\n echo foo",
|
||||
args: (),
|
||||
stdout: "",
|
||||
stderr: "error: Alias `foo` defined on `3` shadows recipe defined on `4`
|
||||
|
|
||||
3 | alias foo = bar
|
||||
| ^^^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
integration_test! {
|
||||
name: default,
|
||||
justfile: "default:\n echo hello\nother: \n echo bar",
|
||||
|
Loading…
Reference in New Issue
Block a user