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]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
|
18
README.adoc
18
README.adoc
@ -198,6 +198,24 @@ $ just --summary
|
|||||||
build test deploy lint
|
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
|
=== Documentation Comments
|
||||||
|
|
||||||
Comments immediately preceding a recipe will appear in `just --list`:
|
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"]
|
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::collections::{BTreeMap as Map, BTreeSet as Set};
|
||||||
pub use std::fmt::Display;
|
pub use std::fmt::Display;
|
||||||
pub use std::io::prelude::*;
|
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::path::{Path, PathBuf};
|
||||||
pub use std::process::Command;
|
pub use std::process::Command;
|
||||||
pub use std::sync::{Mutex, MutexGuard};
|
pub use std::sync::{Mutex, MutexGuard};
|
||||||
@ -13,6 +13,8 @@ pub use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
|||||||
pub use regex::Regex;
|
pub use regex::Regex;
|
||||||
pub use tempdir::TempDir;
|
pub use tempdir::TempDir;
|
||||||
|
|
||||||
|
pub use alias::Alias;
|
||||||
|
pub use alias_resolver::AliasResolver;
|
||||||
pub use assignment_evaluator::AssignmentEvaluator;
|
pub use assignment_evaluator::AssignmentEvaluator;
|
||||||
pub use assignment_resolver::AssignmentResolver;
|
pub use assignment_resolver::AssignmentResolver;
|
||||||
pub use command_ext::CommandExt;
|
pub use command_ext::CommandExt;
|
||||||
|
@ -16,6 +16,10 @@ pub struct CompilationError<'a> {
|
|||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum CompilationErrorKind<'a> {
|
pub enum CompilationErrorKind<'a> {
|
||||||
|
AliasShadowsRecipe {
|
||||||
|
alias: &'a str,
|
||||||
|
recipe_line: usize,
|
||||||
|
},
|
||||||
CircularRecipeDependency {
|
CircularRecipeDependency {
|
||||||
recipe: &'a str,
|
recipe: &'a str,
|
||||||
circle: Vec<&'a str>,
|
circle: Vec<&'a str>,
|
||||||
@ -28,6 +32,10 @@ pub enum CompilationErrorKind<'a> {
|
|||||||
recipe: &'a str,
|
recipe: &'a str,
|
||||||
dependency: &'a str,
|
dependency: &'a str,
|
||||||
},
|
},
|
||||||
|
DuplicateAlias {
|
||||||
|
alias: &'a str,
|
||||||
|
first: usize,
|
||||||
|
},
|
||||||
DuplicateDependency {
|
DuplicateDependency {
|
||||||
recipe: &'a str,
|
recipe: &'a str,
|
||||||
dependency: &'a str,
|
dependency: &'a str,
|
||||||
@ -78,6 +86,10 @@ pub enum CompilationErrorKind<'a> {
|
|||||||
expected: Vec<TokenKind>,
|
expected: Vec<TokenKind>,
|
||||||
found: TokenKind,
|
found: TokenKind,
|
||||||
},
|
},
|
||||||
|
UnknownAliasTarget {
|
||||||
|
alias: &'a str,
|
||||||
|
target: &'a str,
|
||||||
|
},
|
||||||
UnknownDependency {
|
UnknownDependency {
|
||||||
recipe: &'a str,
|
recipe: &'a str,
|
||||||
unknown: &'a str,
|
unknown: &'a str,
|
||||||
@ -99,6 +111,15 @@ impl<'a> Display for CompilationError<'a> {
|
|||||||
write!(f, "{} {}", error.paint("error:"), message.prefix())?;
|
write!(f, "{} {}", error.paint("error:"), message.prefix())?;
|
||||||
|
|
||||||
match self.kind {
|
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 } => {
|
CircularRecipeDependency { recipe, ref circle } => {
|
||||||
if circle.len() == 2 {
|
if circle.len() == 2 {
|
||||||
writeln!(f, "Recipe `{}` depends on itself", recipe)?;
|
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)?;
|
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 } => {
|
DuplicateDependency { recipe, dependency } => {
|
||||||
writeln!(
|
writeln!(
|
||||||
f,
|
f,
|
||||||
@ -228,6 +258,9 @@ impl<'a> Display for CompilationError<'a> {
|
|||||||
show_whitespace(found)
|
show_whitespace(found)
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
UnknownAliasTarget { alias, target } => {
|
||||||
|
writeln!(f, "Alias `{}` has an unknown target `{}`", alias, target)?;
|
||||||
|
}
|
||||||
UnknownDependency { recipe, unknown } => {
|
UnknownDependency { recipe, unknown } => {
|
||||||
writeln!(
|
writeln!(
|
||||||
f,
|
f,
|
||||||
|
@ -29,7 +29,7 @@ impl<'a> CookedString<'a> {
|
|||||||
other => {
|
other => {
|
||||||
return Err(
|
return Err(
|
||||||
token.error(CompilationErrorKind::InvalidEscapeSequence { character: other }),
|
token.error(CompilationErrorKind::InvalidEscapeSequence { character: other }),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
escape = false;
|
escape = false;
|
||||||
|
@ -6,6 +6,7 @@ pub struct Justfile<'a> {
|
|||||||
pub recipes: Map<&'a str, Recipe<'a>>,
|
pub recipes: Map<&'a str, Recipe<'a>>,
|
||||||
pub assignments: Map<&'a str, Expression<'a>>,
|
pub assignments: Map<&'a str, Expression<'a>>,
|
||||||
pub exports: Set<&'a str>,
|
pub exports: Set<&'a str>,
|
||||||
|
pub aliases: Map<&'a str, Alias<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Justfile<'a> where {
|
impl<'a> Justfile<'a> where {
|
||||||
@ -90,13 +91,22 @@ impl<'a> Justfile<'a> where {
|
|||||||
let mut rest = arguments;
|
let mut rest = arguments;
|
||||||
|
|
||||||
while let Some((argument, mut tail)) = rest.split_first() {
|
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() {
|
if recipe.parameters.is_empty() {
|
||||||
grouped.push((recipe, &tail[0..0]));
|
grouped.push((recipe, &tail[0..0]));
|
||||||
} else {
|
} else {
|
||||||
let argument_range = recipe.argument_range();
|
let argument_range = recipe.argument_range();
|
||||||
let argument_count = cmp::min(tail.len(), recipe.max_arguments());
|
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 {
|
return Err(RuntimeError::ArgumentCountMismatch {
|
||||||
recipe: recipe.name,
|
recipe: recipe.name,
|
||||||
parameters: recipe.parameters.iter().collect(),
|
parameters: recipe.parameters.iter().collect(),
|
||||||
@ -161,7 +171,7 @@ impl<'a> Justfile<'a> where {
|
|||||||
|
|
||||||
impl<'a> Display for Justfile<'a> {
|
impl<'a> Display for Justfile<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
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 {
|
for (name, expression) in &self.assignments {
|
||||||
if self.exports.contains(name) {
|
if self.exports.contains(name) {
|
||||||
write!(f, "export ")?;
|
write!(f, "export ")?;
|
||||||
@ -172,6 +182,13 @@ impl<'a> Display for Justfile<'a> {
|
|||||||
write!(f, "\n\n")?;
|
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() {
|
for recipe in self.recipes.values() {
|
||||||
write!(f, "{}", recipe)?;
|
write!(f, "{}", recipe)?;
|
||||||
items -= 1;
|
items -= 1;
|
||||||
|
@ -321,7 +321,7 @@ impl<'a> Lexer<'a> {
|
|||||||
if escape || content_end >= self.rest.len() {
|
if escape || content_end >= self.rest.len() {
|
||||||
return Err(self.error(UnterminatedString));
|
return Err(self.error(UnterminatedString));
|
||||||
}
|
}
|
||||||
(prefix, &self.rest[start..content_end + 1], StringToken)
|
(prefix, &self.rest[start..=content_end], StringToken)
|
||||||
} else {
|
} else {
|
||||||
return Err(self.error(UnknownStartOfToken));
|
return Err(self.error(UnknownStartOfToken));
|
||||||
};
|
};
|
||||||
|
@ -27,6 +27,8 @@ pub mod fuzzing;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod die;
|
mod die;
|
||||||
|
|
||||||
|
mod alias;
|
||||||
|
mod alias_resolver;
|
||||||
mod assignment_evaluator;
|
mod assignment_evaluator;
|
||||||
mod assignment_resolver;
|
mod assignment_resolver;
|
||||||
mod color;
|
mod color;
|
||||||
|
153
src/parser.rs
153
src/parser.rs
@ -11,6 +11,8 @@ pub struct Parser<'a> {
|
|||||||
assignments: Map<&'a str, Expression<'a>>,
|
assignments: Map<&'a str, Expression<'a>>,
|
||||||
assignment_tokens: Map<&'a str, Token<'a>>,
|
assignment_tokens: Map<&'a str, Token<'a>>,
|
||||||
exports: Set<&'a str>,
|
exports: Set<&'a str>,
|
||||||
|
aliases: Map<&'a str, Alias<'a>>,
|
||||||
|
alias_tokens: Map<&'a str, Token<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Parser<'a> {
|
impl<'a> Parser<'a> {
|
||||||
@ -27,6 +29,8 @@ impl<'a> Parser<'a> {
|
|||||||
assignments: empty(),
|
assignments: empty(),
|
||||||
assignment_tokens: empty(),
|
assignment_tokens: empty(),
|
||||||
exports: empty(),
|
exports: empty(),
|
||||||
|
aliases: empty(),
|
||||||
|
alias_tokens: empty(),
|
||||||
text,
|
text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -342,6 +346,41 @@ impl<'a> Parser<'a> {
|
|||||||
Ok(())
|
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>> {
|
pub fn justfile(mut self) -> CompilationResult<'a, Justfile<'a>> {
|
||||||
let mut doc = None;
|
let mut doc = None;
|
||||||
loop {
|
loop {
|
||||||
@ -380,6 +419,16 @@ impl<'a> Parser<'a> {
|
|||||||
self.recipe(&token, doc, false)?;
|
self.recipe(&token, doc, false)?;
|
||||||
doc = None;
|
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) {
|
} else if self.accepted(Equals) {
|
||||||
self.assignment(token, false)?;
|
self.assignment(token, false)?;
|
||||||
doc = None;
|
doc = None;
|
||||||
@ -400,7 +449,7 @@ impl<'a> Parser<'a> {
|
|||||||
kind: Internal {
|
kind: Internal {
|
||||||
message: "unexpected end of token stream".to_string(),
|
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)?;
|
AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?;
|
||||||
|
|
||||||
Ok(Justfile {
|
Ok(Justfile {
|
||||||
recipes: self.recipes,
|
recipes: self.recipes,
|
||||||
assignments: self.assignments,
|
assignments: self.assignments,
|
||||||
exports: self.exports,
|
exports: self.exports,
|
||||||
|
aliases: self.aliases,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -532,6 +584,45 @@ export a = "hello"
|
|||||||
r#"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! {
|
summary_test! {
|
||||||
parse_complex,
|
parse_complex,
|
||||||
"
|
"
|
||||||
@ -680,6 +771,66 @@ a:
|
|||||||
{{env_var_or_default("foo" + "bar", "baz")}} {{env_var(env_var("baz"))}}"#,
|
{{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! {
|
compilation_error_test! {
|
||||||
name: missing_colon,
|
name: missing_colon,
|
||||||
input: "a b c\nd e f",
|
input: "a b c\nd e f",
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
pub trait RangeExt<T> {
|
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>
|
impl<T> RangeExt<T> for Range<T>
|
||||||
where
|
where
|
||||||
T: PartialOrd + Copy,
|
T: PartialOrd + Copy,
|
||||||
{
|
{
|
||||||
fn range_contains(&self, i: T) -> bool {
|
fn range_contains(&self, i: &T) -> bool {
|
||||||
i >= self.start && i < self.end
|
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]
|
#[test]
|
||||||
fn range() {
|
fn range() {
|
||||||
assert!((0..1).range_contains(0));
|
assert!((0..1).range_contains(&0));
|
||||||
assert!((10..20).range_contains(15));
|
assert!((10..20).range_contains(&15));
|
||||||
assert!(!(0..0).range_contains(0));
|
assert!(!(0..0).range_contains(&0));
|
||||||
assert!(!(1..10).range_contains(0));
|
assert!(!(1..10).range_contains(&0));
|
||||||
assert!(!(1..10).range_contains(10));
|
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> {
|
impl<'a> Recipe<'a> {
|
||||||
pub fn argument_range(&self) -> Range<usize> {
|
pub fn argument_range(&self) -> RangeInclusive<usize> {
|
||||||
self.min_arguments()..self.max_arguments() + 1
|
self.min_arguments()..=self.max_arguments()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn min_arguments(&self) -> usize {
|
pub fn min_arguments(&self) -> usize {
|
||||||
@ -94,7 +94,7 @@ impl<'a> Recipe<'a> {
|
|||||||
None => {
|
None => {
|
||||||
return Err(RuntimeError::Internal {
|
return Err(RuntimeError::Internal {
|
||||||
message: "missing parameter without default".to_string(),
|
message: "missing parameter without default".to_string(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if parameter.variadic {
|
} else if parameter.variadic {
|
||||||
@ -226,7 +226,7 @@ impl<'a> Recipe<'a> {
|
|||||||
command: interpreter.to_string(),
|
command: interpreter.to_string(),
|
||||||
argument: argument.map(String::from),
|
argument: argument.map(String::from),
|
||||||
io_error,
|
io_error,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -305,7 +305,7 @@ impl<'a> Recipe<'a> {
|
|||||||
return Err(RuntimeError::IoError {
|
return Err(RuntimeError::IoError {
|
||||||
recipe: self.name,
|
recipe: self.name,
|
||||||
io_error,
|
io_error,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
|
|||||||
return Err(dependency_token.error(UnknownDependency {
|
return Err(dependency_token.error(UnknownDependency {
|
||||||
recipe: recipe.name,
|
recipe: recipe.name,
|
||||||
unknown: dependency_token.lexeme,
|
unknown: dependency_token.lexeme,
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
src/run.rs
32
src/run.rs
@ -356,6 +356,17 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if matches.is_present("LIST") {
|
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();
|
let mut line_widths: Map<&str, usize> = Map::new();
|
||||||
|
|
||||||
for (name, recipe) in &justfile.recipes {
|
for (name, recipe) in &justfile.recipes {
|
||||||
@ -363,6 +374,7 @@ pub fn run() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for name in iter::once(name).chain(recipe_aliases.get(name).unwrap_or(&Vec::new())) {
|
||||||
let mut line_width = UnicodeWidthStr::width(*name);
|
let mut line_width = UnicodeWidthStr::width(*name);
|
||||||
|
|
||||||
for parameter in &recipe.parameters {
|
for parameter in &recipe.parameters {
|
||||||
@ -373,15 +385,21 @@ pub fn run() {
|
|||||||
line_widths.insert(name, line_width);
|
line_widths.insert(name, line_width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let max_line_width = cmp::min(line_widths.values().cloned().max().unwrap_or(0), 30);
|
let max_line_width = cmp::min(line_widths.values().cloned().max().unwrap_or(0), 30);
|
||||||
|
|
||||||
let doc_color = color.stdout().doc();
|
let doc_color = color.stdout().doc();
|
||||||
println!("Available recipes:");
|
println!("Available recipes:");
|
||||||
|
|
||||||
for (name, recipe) in &justfile.recipes {
|
for (name, recipe) in &justfile.recipes {
|
||||||
if recipe.private {
|
if recipe.private {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
print!(" {}", name);
|
||||||
for parameter in &recipe.parameters {
|
for parameter in &recipe.parameters {
|
||||||
if color.stdout().active() {
|
if color.stdout().active() {
|
||||||
@ -390,7 +408,11 @@ pub fn run() {
|
|||||||
print!(" {}", parameter);
|
print!(" {}", parameter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(doc) = recipe.doc {
|
|
||||||
|
// 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!(
|
print!(
|
||||||
" {:padding$}{} {}",
|
" {:padding$}{} {}",
|
||||||
"",
|
"",
|
||||||
@ -399,9 +421,17 @@ pub fn run() {
|
|||||||
padding =
|
padding =
|
||||||
max_line_width.saturating_sub(line_widths.get(name).cloned().unwrap_or(max_line_width))
|
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);
|
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! {
|
integration_test! {
|
||||||
name: default,
|
name: default,
|
||||||
justfile: "default:\n echo hello\nother: \n echo bar",
|
justfile: "default:\n echo hello\nother: \n echo bar",
|
||||||
|
Loading…
Reference in New Issue
Block a user