Add --fmt subcommand (#837)

This commit is contained in:
Oleksii Dorozhkin 2021-06-08 11:01:27 +03:00 committed by GitHub
parent 39e76488b5
commit 8677492d56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1133 additions and 64 deletions

View File

@ -20,7 +20,7 @@ _just() {
case "${cmd}" in
just)
opts=" -q -u -v -e -l -h -V -f -d -c -s --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --verbose --choose --dump --edit --evaluate --init --list --summary --variables --help --version --chooser --color --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show <ARGUMENTS>... "
opts=" -q -u -v -e -l -h -V -f -d -c -s --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --verbose --choose --dump --edit --evaluate --fmt --init --list --summary --variables --help --version --chooser --color --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show <ARGUMENTS>... "
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0

View File

@ -47,6 +47,7 @@ edit:completion:arg-completer[just] = [@words]{
cand -e 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
cand --edit 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
cand --evaluate 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.'
cand --fmt 'Format and overwrite justfile'
cand --init 'Initialize new justfile in project root'
cand -l 'List available recipes and their arguments'
cand --list 'List available recipes and their arguments'

View File

@ -34,6 +34,7 @@ complete -c just -n "__fish_use_subcommand" -l choose -d 'Select one or more rec
complete -c just -n "__fish_use_subcommand" -l dump -d 'Print entire justfile'
complete -c just -n "__fish_use_subcommand" -s e -l edit -d 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
complete -c just -n "__fish_use_subcommand" -l evaluate -d 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable\'s value.'
complete -c just -n "__fish_use_subcommand" -l fmt -d 'Format and overwrite justfile'
complete -c just -n "__fish_use_subcommand" -l init -d 'Initialize new justfile in project root'
complete -c just -n "__fish_use_subcommand" -s l -l list -d 'List available recipes and their arguments'
complete -c just -n "__fish_use_subcommand" -l summary -d 'List names of available recipes'

View File

@ -52,6 +52,7 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
[CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`')
[CompletionResult]::new('--edit', 'edit', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`')
[CompletionResult]::new('--evaluate', 'evaluate', [CompletionResultType]::ParameterName, 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.')
[CompletionResult]::new('--fmt', 'fmt', [CompletionResultType]::ParameterName, 'Format and overwrite justfile')
[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')

View File

@ -48,6 +48,7 @@ _just() {
'-e[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \
'--edit[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \
'--evaluate[Evaluate and print all variables. If a variable name is given as an argument, only print that variable'\''s value.]' \
'--fmt[Format and overwrite justfile]' \
'--init[Initialize new justfile in project root]' \
'-l[List available recipes and their arguments]' \
'--list[List available recipes and their arguments]' \

View File

@ -1,7 +1,7 @@
use crate::common::*;
/// An alias, e.g. `name := target`
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct Alias<'src, T = Rc<Recipe<'src>>> {
pub(crate) name: Name<'src>,
pub(crate) target: T,

View File

@ -29,6 +29,7 @@ impl<'src> Analyzer<'src> {
self.analyze_assignment(&assignment)?;
self.assignments.insert(assignment);
},
Item::Comment(_) => (),
Item::Recipe(recipe) => {
self.analyze_recipe(&recipe)?;
self.recipes.insert(recipe);

View File

@ -2,3 +2,12 @@ use crate::common::*;
/// An assignment, e.g `foo := bar`
pub(crate) type Assignment<'src> = Binding<'src, Expression<'src>>;
impl<'src> Display for Assignment<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
if self.export {
write!(f, "export ")?;
}
write!(f, "{} := {}", self.name, self.value)
}
}

View File

@ -1,7 +1,7 @@
use crate::common::*;
/// A binding of `name` to `value`
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Binding<'src, V = String> {
/// Export binding as an environment variable to child processes
pub(crate) export: bool,

View File

@ -5,9 +5,10 @@ pub(crate) use std::{
env,
ffi::{OsStr, OsString},
fmt::{self, Debug, Display, Formatter},
fs,
fs::{self, File},
io::{self, Cursor, Write},
iter::{self, FromIterator},
mem,
ops::{Index, Range, RangeInclusive},
path::{Path, PathBuf},
process::{self, Command, Stdio},
@ -45,21 +46,21 @@ pub(crate) use crate::{
alias::Alias, analyzer::Analyzer, assignment::Assignment,
assignment_resolver::AssignmentResolver, binding::Binding, color::Color,
compilation_error::CompilationError, compilation_error_kind::CompilationErrorKind,
compiler::Compiler, config::Config, config_error::ConfigError, count::Count,
delimiter::Delimiter, dependency::Dependency, enclosure::Enclosure, evaluator::Evaluator,
expression::Expression, fragment::Fragment, function::Function,
function_context::FunctionContext, interrupt_guard::InterruptGuard,
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyword::Keyword,
lexer::Lexer, line::Line, list::List, load_error::LoadError, module::Module, name::Name,
output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind, parser::Parser,
platform::Platform, position::Position, positional::Positional, recipe::Recipe,
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand,
suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind,
unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe,
use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning,
config::Config, config_error::ConfigError, count::Count, delimiter::Delimiter,
dependency::Dependency, enclosure::Enclosure, evaluator::Evaluator, expression::Expression,
fragment::Fragment, function::Function, function_context::FunctionContext,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
justfile::Justfile, keyword::Keyword, lexer::Lexer, line::Line, list::List,
load_error::LoadError, module::Module, name::Name, output_error::OutputError,
parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform,
position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext,
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search,
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, string_kind::StringKind,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
verbosity::Verbosity, warning::Warning,
};
// type aliases

View File

@ -38,6 +38,7 @@ mod cmd {
pub(crate) const DUMP: &str = "DUMP";
pub(crate) const EDIT: &str = "EDIT";
pub(crate) const EVALUATE: &str = "EVALUATE";
pub(crate) const FORMAT: &str = "FORMAT";
pub(crate) const INIT: &str = "INIT";
pub(crate) const LIST: &str = "LIST";
pub(crate) const SHOW: &str = "SHOW";
@ -52,6 +53,7 @@ mod cmd {
DUMP,
EDIT,
EVALUATE,
FORMAT,
INIT,
LIST,
SHOW,
@ -63,6 +65,7 @@ mod cmd {
COMPLETIONS,
DUMP,
EDIT,
FORMAT,
INIT,
LIST,
SHOW,
@ -266,6 +269,11 @@ impl Config {
"Evaluate and print all variables. If a variable name is given as an argument, only print \
that variable's value.",
))
.arg(
Arg::with_name(cmd::FORMAT)
.long("fmt")
.help("Format and overwrite justfile"),
)
.arg(
Arg::with_name(cmd::INIT)
.long("init")
@ -442,6 +450,8 @@ impl Config {
Subcommand::Summary
} else if matches.is_present(cmd::DUMP) {
Subcommand::Dump
} else if matches.is_present(cmd::FORMAT) {
Subcommand::Format
} else if matches.is_present(cmd::INIT) {
Subcommand::Init
} else if matches.is_present(cmd::LIST) {
@ -539,7 +549,9 @@ impl Config {
})
.eprint(self.color)?;
let justfile = Compiler::compile(&src).eprint(self.color)?;
let tokens = Lexer::lex(&src).eprint(self.color)?;
let ast = Parser::parse(&tokens).eprint(self.color)?;
let justfile = Analyzer::analyze(ast.clone()).eprint(self.color)?;
if self.verbosity.loud() {
for warning in &justfile.warnings {
@ -555,8 +567,9 @@ impl Config {
Choose { overrides, chooser } =>
self.choose(justfile, &search, overrides, chooser.as_deref())?,
Command { overrides, .. } => self.run(justfile, &search, overrides, &[])?,
Dump => Self::dump(justfile),
Dump => Self::dump(ast)?,
Evaluate { overrides, .. } => self.run(justfile, &search, overrides, &[])?,
Format => self.format(ast, &search)?,
List => self.list(justfile),
Run {
arguments,
@ -676,8 +689,9 @@ impl Config {
self.run(justfile, search, overrides, &recipes)
}
fn dump(justfile: Justfile) {
println!("{}", justfile);
fn dump(ast: Module) -> Result<(), i32> {
print!("{}", ast);
Ok(())
}
pub(crate) fn edit(&self, search: &Search) -> Result<(), i32> {
@ -713,6 +727,24 @@ impl Config {
}
}
fn format(&self, ast: Module, search: &Search) -> Result<(), i32> {
if let Err(error) = File::open(&search.justfile).and_then(|mut file| write!(file, "{}", ast)) {
if self.verbosity.loud() {
eprintln!(
"Failed to write justfile to `{}`: {}",
search.justfile.display(),
error
);
}
Err(EXIT_FAILURE)
} else {
if self.verbosity.loud() {
eprintln!("Wrote justfile to `{}`", search.justfile.display());
}
Ok(())
}
}
pub(crate) fn init(&self) -> Result<(), i32> {
let search =
Search::init(&self.search_config, &self.invocation_directory).eprint(self.color)?;
@ -920,6 +952,7 @@ FLAGS:
--evaluate Evaluate and print all variables. If a variable name is given as an \
argument, only print
that variable's value.
--fmt Format and overwrite justfile
--highlight Highlight echoed recipe lines in bold
--init Initialize new justfile in project root
-l, --list List available recipes and their arguments
@ -1315,6 +1348,11 @@ ARGS:
args: ["--list", "--dump"],
}
error! {
name: subcommand_conflict_fmt,
args: ["--list", "--fmt"],
}
error! {
name: subcommand_conflict_init,
args: ["--list", "--init"],
@ -1661,6 +1699,16 @@ ARGS:
},
}
error! {
name: fmt_arguments,
args: ["--fmt", "bar"],
error: ConfigError::SubcommandArguments { subcommand, arguments },
check: {
assert_eq!(subcommand, cmd::FORMAT);
assert_eq!(arguments, &["bar"]);
},
}
error! {
name: init_arguments,
args: ["--init", "bar"],

View File

@ -6,7 +6,7 @@ use crate::common::*;
/// parenthetical groups).
///
/// The parser parses both values and expressions into `Expression`s.
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Clone)]
pub(crate) enum Expression<'src> {
/// `contents`
Backtick {
@ -45,7 +45,7 @@ impl<'src> Expression<'src> {
impl<'src> Display for Expression<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
match self {
Expression::Backtick { contents, .. } => write!(f, "`{}`", contents),
Expression::Backtick { token, .. } => write!(f, "{}", token.lexeme()),
Expression::Concatination { lhs, rhs } => write!(f, "{} + {}", lhs, rhs),
Expression::Conditional {
lhs,
@ -55,7 +55,7 @@ impl<'src> Display for Expression<'src> {
inverted,
} => write!(
f,
"if {} {} {} {{ {} }} else {{ {} }} ",
"if {} {} {} {{ {} }} else {{ {} }}",
lhs,
if *inverted { "!=" } else { "==" },
rhs,

View File

@ -1,7 +1,7 @@
use crate::common::*;
/// A line fragment consisting either of…
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Clone)]
pub(crate) enum Fragment<'src> {
/// …raw text…
Text { token: Token<'src> },

View File

@ -1,10 +1,23 @@
use crate::common::*;
/// A single top-level item
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) enum Item<'src> {
Alias(Alias<'src, Name<'src>>),
Assignment(Assignment<'src>),
Comment(&'src str),
Recipe(UnresolvedRecipe<'src>),
Set(Set<'src>),
}
impl<'src> Display for Item<'src> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Item::Alias(alias) => write!(f, "{}", alias),
Item::Assignment(assignment) => write!(f, "{}", assignment),
Item::Comment(comment) => write!(f, "{}", comment),
Item::Recipe(recipe) => write!(f, "{}", recipe),
Item::Set(set) => write!(f, "{}", set),
}
}
}

View File

@ -802,7 +802,7 @@ goodbye := \"y\"
hello a b c: x y z
#! blah
#blarg
{{foo + bar}}abc{{goodbye + \"x\"}}xyz
{{ foo + bar }}abc{{ goodbye + \"x\" }}xyz
1
2
3
@ -828,7 +828,7 @@ install:
install:
#!/bin/sh
if [[ -f {{practicum}} ]]; then
if [[ -f {{ practicum }} ]]; then
\treturn
fi",
}
@ -869,7 +869,7 @@ c := a + b + a + b",
r#"a:
echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
r#"a:
echo {{`echo hello` + "blarg"}} {{`echo bob`}}"#,
echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
}
test! {
@ -895,7 +895,7 @@ c := a + b + a + b",
"a b c:
{{b}} {{c}}",
"a b c:
{{b}} {{c}}",
{{ b }} {{ c }}",
}
test! {
@ -908,7 +908,7 @@ a:
"x := arch()
a:
{{os()}} {{os_family()}}",
{{ os() }} {{ os_family() }}",
}
test! {
@ -921,7 +921,7 @@ a:
r#"x := env_var('foo')
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")) }}"#,
}
test! {

View File

@ -1,7 +1,7 @@
use crate::common::*;
/// A single line in a recipe body, consisting of any number of `Fragment`s.
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Line<'src> {
pub(crate) fragments: Vec<Fragment<'src>>,
}

View File

@ -7,10 +7,30 @@ use crate::common::*;
/// Not all successful parses result in valid justfiles, so additional
/// consistency checks and name resolution are performed by the `Analyzer`,
/// which produces a `Justfile` from a `Module`.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) struct Module<'src> {
/// Items in the justfile
pub(crate) items: Vec<Item<'src>>,
/// Non-fatal warnings encountered during parsing
pub(crate) warnings: Vec<Warning>,
}
impl<'src> Display for Module<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
let mut iter = self.items.iter().peekable();
while let Some(item) = iter.next() {
writeln!(f, "{}", item)?;
if let Some(next_item) = iter.peek() {
if matches!(item, Item::Recipe(_))
|| mem::discriminant(item) != mem::discriminant(next_item)
{
writeln!(f)?;
}
}
}
Ok(())
}
}

View File

@ -20,6 +20,7 @@ impl<'src> Node<'src> for Item<'src> {
match self {
Item::Alias(alias) => alias.tree(),
Item::Assignment(assignment) => assignment.tree(),
Item::Comment(comment) => comment.tree(),
Item::Recipe(recipe) => recipe.tree(),
Item::Set(set) => set.tree(),
}
@ -210,3 +211,9 @@ impl<'src> Node<'src> for Warning {
unreachable!()
}
}
impl<'src> Node<'src> for str {
fn tree(&self) -> Tree<'src> {
Tree::atom("comment").push(["\"", self, "\""].concat())
}
}

View File

@ -1,7 +1,7 @@
use crate::common::*;
/// A single function parameter
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Clone)]
pub(crate) struct Parameter<'src> {
/// The parameter name
pub(crate) name: Name<'src>,
@ -16,6 +16,9 @@ pub(crate) struct Parameter<'src> {
impl<'src> Display for Parameter<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
let color = Color::fmt(f);
if self.export {
write!(f, "$")?;
}
if let Some(prefix) = self.kind.prefix() {
write!(f, "{}", color.annotation().paint(prefix))?;
}

View File

@ -296,17 +296,34 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
/// Parse a justfile, consumes self
fn parse_justfile(mut self) -> CompilationResult<'src, Module<'src>> {
fn pop_doc_comment<'src>(
items: &mut Vec<Item<'src>>,
eol_since_last_comment: bool,
) -> Option<&'src str> {
if !eol_since_last_comment {
if let Some(Item::Comment(contents)) = items.last() {
let doc = Some(contents[1..].trim_start());
items.pop();
return doc;
}
}
None
}
let mut items = Vec::new();
let mut doc = None;
let mut eol_since_last_comment = false;
loop {
let next = self.next()?;
if let Some(comment) = self.accept(Comment)? {
doc = Some(comment.lexeme()[1..].trim());
items.push(Item::Comment(comment.lexeme().trim_end()));
self.expect_eol()?;
eol_since_last_comment = false;
} else if self.accepted(Eol)? {
eol_since_last_comment = true;
} else if self.accepted(Eof)? {
break;
} else if self.next_is(Identifier) {
@ -317,6 +334,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
} else if self.next_are(&[Identifier, Identifier, ColonEquals]) {
items.push(Item::Alias(self.parse_alias()?));
} else {
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(doc, false)?));
},
Some(Keyword::Export) =>
@ -326,6 +344,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
self.presume_keyword(Keyword::Export)?;
items.push(Item::Assignment(self.parse_assignment(true)?));
} else {
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(doc, false)?));
},
Some(Keyword::Set) =>
@ -335,6 +354,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
{
items.push(Item::Set(self.parse_set()?));
} else {
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(doc, false)?));
},
_ =>
@ -343,18 +363,16 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
} else if self.next_are(&[Identifier, ColonEquals]) {
items.push(Item::Assignment(self.parse_assignment(false)?));
} else {
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(doc, false)?));
},
}
} else if self.accepted(At)? {
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(doc, true)?));
} else {
return Err(self.unexpected_token()?);
}
if next.kind != Comment {
doc = None;
}
}
if self.next != self.tokens.len() {
@ -1097,11 +1115,17 @@ mod tests {
test! {
name: comment,
text: "# foo",
tree: (justfile),
tree: (justfile (comment "# foo")),
}
test! {
name: comment_alias,
name: comment_before_alias,
text: "# foo\nalias x := y",
tree: (justfile (comment "# foo") (alias x y)),
}
test! {
name: comment_after_alias,
text: "alias x := y # foo",
tree: (justfile (alias x y)),
}
@ -1166,7 +1190,7 @@ mod tests {
x := y
bar:
",
tree: (justfile (assignment x y) (recipe bar)),
tree: (justfile (comment "# foo") (assignment x y) (recipe bar)),
}
test! {
@ -1176,7 +1200,7 @@ mod tests {
bar:
",
tree: (justfile (recipe bar)),
tree: (justfile (comment "# foo") (recipe bar)),
}
test! {

View File

@ -23,7 +23,7 @@ fn error_from_signal(
}
/// A recipe, e.g. `foo: bar baz`
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Clone)]
pub(crate) struct Recipe<'src, D = Dependency<'src>> {
pub(crate) dependencies: Vec<D>,
pub(crate) doc: Option<&'src str>,
@ -314,7 +314,7 @@ impl<'src, D> Keyed<'src> for Recipe<'src, D> {
}
}
impl<'src> Display for Recipe<'src> {
impl<'src, D: Display> Display for Recipe<'src, D> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
if let Some(doc) = self.doc {
writeln!(f, "# {}", doc)?;
@ -344,7 +344,7 @@ impl<'src> Display for Recipe<'src> {
}
match fragment {
Fragment::Text { token } => write!(f, "{}", token.lexeme())?,
Fragment::Interpolation { expression, .. } => write!(f, "{{{{{}}}}}", expression)?,
Fragment::Interpolation { expression, .. } => write!(f, "{{{{ {} }}}}", expression)?,
}
}
if i + 1 < self.body.len() {

View File

@ -1,6 +1,6 @@
use crate::common::*;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) struct Set<'src> {
pub(crate) name: Name<'src>,
pub(crate) value: Setting<'src>,
@ -11,3 +11,9 @@ impl<'src> Keyed<'src> for Set<'src> {
self.name.lexeme()
}
}
impl<'src> Display for Set<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
write!(f, "set {} := {}", self.name, self.value)
}
}

View File

@ -1,6 +1,6 @@
use crate::common::*;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) enum Setting<'src> {
DotenvLoad(bool),
Export(bool),
@ -8,8 +8,31 @@ pub(crate) enum Setting<'src> {
Shell(Shell<'src>),
}
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Shell<'src> {
pub(crate) command: StringLiteral<'src>,
pub(crate) arguments: Vec<StringLiteral<'src>>,
}
impl<'src> Display for Setting<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
match self {
Setting::DotenvLoad(value) => write!(f, "{}", value),
Setting::Export(value) => write!(f, "{}", value),
Setting::PositionalArguments(value) => write!(f, "{}", value),
Setting::Shell(shell) => write!(f, "{}", shell),
}
}
}
impl<'src> Display for Shell<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
write!(f, "[{}", self.command)?;
for argument in &self.arguments {
write!(f, ", {}", argument)?;
}
write!(f, "]")
}
}

View File

@ -1,6 +1,6 @@
use crate::common::*;
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Clone)]
pub(crate) struct StringLiteral<'src> {
pub(crate) kind: StringKind,
pub(crate) raw: &'src str,

View File

@ -20,6 +20,7 @@ pub(crate) enum Subcommand {
overrides: BTreeMap<String, String>,
variable: Option<String>,
},
Format,
Init,
List,
Run {

View File

@ -1,5 +1,6 @@
use crate::common::*;
use crate::compiler::Compiler;
use pretty_assertions::assert_eq;
pub(crate) fn compile(text: &str) -> Justfile {

View File

@ -1,7 +1,7 @@
use crate::common::*;
#[derive(Derivative)]
#[derivative(Debug, PartialEq = "feature_allow_slow_enum")]
#[derivative(Debug, Clone, PartialEq = "feature_allow_slow_enum")]
pub(crate) enum Thunk<'src> {
Nullary {
name: Name<'src>,

View File

@ -1,7 +1,23 @@
use crate::common::*;
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Clone)]
pub(crate) struct UnresolvedDependency<'src> {
pub(crate) recipe: Name<'src>,
pub(crate) arguments: Vec<Expression<'src>>,
}
impl<'src> Display for UnresolvedDependency<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
if self.arguments.is_empty() {
write!(f, "{}", self.recipe)
} else {
write!(f, "({}", self.recipe)?;
for argument in &self.arguments {
write!(f, " {}", argument)?;
}
write!(f, ")")
}
}
}

View File

@ -1,6 +1,6 @@
use crate::common::*;
#[derive(Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Warning {
// Remove this on 2021-07-01.
#[allow(dead_code)]

View File

@ -33,7 +33,7 @@ test! {
USAGE:
just{} --color <COLOR> --shell <SHELL> --shell-arg <SHELL-ARG>... \
<--choose|--command <COMMAND>|--completions <SHELL>|--dump|--edit|\
--evaluate|--init|--list|--show <RECIPE>|--summary|--variables>
--evaluate|--fmt|--init|--list|--show <RECIPE>|--summary|--variables>
For more information try --help
", EXE_SUFFIX),

View File

@ -149,10 +149,10 @@ test! {
echo {{ a }}
",
args: ("--dump"),
stdout: format!("
a := if '' == '' {{ '' }} else {{ '' }}{}
stdout: "
a := if '' == '' { '' } else { '' }
foo:
echo {{{{a}}}}
", " ").as_str(),
echo {{ a }}
",
}

891
tests/fmt.rs Normal file
View File

@ -0,0 +1,891 @@
test! {
name: alias_good,
justfile: "
alias f := foo
foo:
echo foo
",
args: ("--dump"),
stdout: "
alias f := foo
foo:
echo foo
",
}
test! {
name: alias_fix_indent,
justfile: "
alias f:= foo
foo:
echo foo
",
args: ("--dump"),
stdout: "
alias f := foo
foo:
echo foo
",
}
test! {
name: assignment_singlequote,
justfile: "
foo := 'foo'
",
args: ("--dump"),
stdout: "
foo := 'foo'
",
}
test! {
name: assignment_doublequote,
justfile: r#"
foo := "foo"
"#,
args: ("--dump"),
stdout: r#"
foo := "foo"
"#,
}
test! {
name: assignment_indented_singlequote,
justfile: "
foo := '''
foo
'''
",
args: ("--dump"),
stdout: r"
foo := '''
foo
'''
",
}
test! {
name: assignment_indented_doublequote,
justfile: r#"
foo := """
foo
"""
"#,
args: ("--dump"),
stdout: r#"
foo := """
foo
"""
"#,
}
test! {
name: assignment_backtick,
justfile: "
foo := `foo`
",
args: ("--dump"),
stdout: "
foo := `foo`
",
}
test! {
name: assignment_indented_backtick,
justfile: "
foo := ```
foo
```
",
args: ("--dump"),
stdout: "
foo := ```
foo
```
",
}
test! {
name: assignment_name,
justfile: "
bar := 'bar'
foo := bar
",
args: ("--dump"),
stdout: "
bar := 'bar'
foo := bar
",
}
test! {
name: assignment_parenthized_expression,
justfile: "
foo := ('foo')
",
args: ("--dump"),
stdout: "
foo := ('foo')
",
}
test! {
name: assignment_export,
justfile: "
export foo := 'foo'
",
args: ("--dump"),
stdout: "
export foo := 'foo'
",
}
test! {
name: assignment_concat_values,
justfile: "
foo := 'foo' + 'bar'
",
args: ("--dump"),
stdout: "
foo := 'foo' + 'bar'
",
}
test! {
name: assignment_if_oneline,
justfile: "
foo := if 'foo' == 'foo' { 'foo' } else { 'bar' }
",
args: ("--dump"),
stdout: "
foo := if 'foo' == 'foo' { 'foo' } else { 'bar' }
",
}
test! {
name: assignment_if_multiline,
justfile: "
foo := if 'foo' != 'foo' {
'foo'
} else {
'bar'
}
",
args: ("--dump"),
stdout: "
foo := if 'foo' != 'foo' { 'foo' } else { 'bar' }
",
}
test! {
name: assignment_nullary_function,
justfile: "
foo := arch()
",
args: ("--dump"),
stdout: "
foo := arch()
",
}
test! {
name: assignment_unary_function,
justfile: "
foo := env_var('foo')
",
args: ("--dump"),
stdout: "
foo := env_var('foo')
",
}
test! {
name: assignment_binary_function,
justfile: "
foo := env_var_or_default('foo', 'bar')
",
args: ("--dump"),
stdout: "
foo := env_var_or_default('foo', 'bar')
",
}
test! {
name: recipe_ordinary,
justfile: "
foo:
echo bar
",
args: ("--dump"),
stdout: "
foo:
echo bar
",
}
test! {
name: recipe_with_docstring,
justfile: "
# bar
foo:
echo bar
",
args: ("--dump"),
stdout: "
# bar
foo:
echo bar
",
}
test! {
name: recipe_with_comments_in_body,
justfile: "
foo:
# bar
echo bar
",
args: ("--dump"),
stdout: "
foo:
# bar
echo bar
",
}
test! {
name: recipe_body_is_comment,
justfile: "
foo:
# bar
",
args: ("--dump"),
stdout: "
foo:
# bar
",
}
test! {
name: recipe_several_commands,
justfile: "
foo:
echo bar
echo baz
",
args: ("--dump"),
stdout: "
foo:
echo bar
echo baz
",
}
test! {
name: recipe_quiet,
justfile: "
@foo:
echo bar
",
args: ("--dump"),
stdout: "
@foo:
echo bar
",
}
test! {
name: recipe_quiet_command,
justfile: "
foo:
@echo bar
",
args: ("--dump"),
stdout: "
foo:
@echo bar
",
}
test! {
name: recipe_quiet_comment,
justfile: "
foo:
@# bar
",
args: ("--dump"),
stdout: "
foo:
@# bar
",
}
test! {
name: recipe_ignore_errors,
justfile: "
foo:
-echo foo
",
args: ("--dump"),
stdout: "
foo:
-echo foo
",
}
test! {
name: recipe_parameter,
justfile: "
foo BAR:
echo foo
",
args: ("--dump"),
stdout: "
foo BAR:
echo foo
",
}
test! {
name: recipe_parameter_default,
justfile: "
foo BAR='bar':
echo foo
",
args: ("--dump"),
stdout: "
foo BAR='bar':
echo foo
",
}
test! {
name: recipe_parameter_envar,
justfile: "
foo $BAR:
echo foo
",
args: ("--dump"),
stdout: "
foo $BAR:
echo foo
",
}
test! {
name: recipe_parameter_default_envar,
justfile: "
foo $BAR='foo':
echo foo
",
args: ("--dump"),
stdout: "
foo $BAR='foo':
echo foo
",
}
test! {
name: recipe_parameter_concat,
justfile: "
foo BAR=('bar' + 'baz'):
echo foo
",
args: ("--dump"),
stdout: "
foo BAR=('bar' + 'baz'):
echo foo
",
}
test! {
name: recipe_parameters,
justfile: "
foo BAR BAZ:
echo foo
",
args: ("--dump"),
stdout: "
foo BAR BAZ:
echo foo
",
}
test! {
name: recipe_parameters_envar,
justfile: "
foo $BAR $BAZ:
echo foo
",
args: ("--dump"),
stdout: "
foo $BAR $BAZ:
echo foo
",
}
test! {
name: recipe_variadic_plus,
justfile: "
foo +BAR:
echo foo
",
args: ("--dump"),
stdout: "
foo +BAR:
echo foo
",
}
test! {
name: recipe_variadic_star,
justfile: "
foo *BAR:
echo foo
",
args: ("--dump"),
stdout: "
foo *BAR:
echo foo
",
}
test! {
name: recipe_positional_variadic,
justfile: "
foo BAR *BAZ:
echo foo
",
args: ("--dump"),
stdout: "
foo BAR *BAZ:
echo foo
",
}
test! {
name: recipe_variadic_default,
justfile: "
foo +BAR='bar':
echo foo
",
args: ("--dump"),
stdout: "
foo +BAR='bar':
echo foo
",
}
test! {
name: recipe_parameter_in_body,
justfile: "
foo BAR:
echo {{ BAR }}
",
args: ("--dump"),
stdout: "
foo BAR:
echo {{ BAR }}
",
}
test! {
name: recipe_parameter_conditional,
justfile: "
foo BAR:
echo {{ if 'foo' == 'foo' { 'foo' } else { 'bar' } }}
",
args: ("--dump"),
stdout: "
foo BAR:
echo {{ if 'foo' == 'foo' { 'foo' } else { 'bar' } }}
",
}
test! {
name: recipe_escaped_braces,
justfile: "
foo BAR:
echo '{{{{BAR}}}}'
",
args: ("--dump"),
stdout: "
foo BAR:
echo '{{{{BAR}}}}'
",
}
test! {
name: recipe_assignment_in_body,
justfile: "
bar := 'bar'
foo:
echo $bar
",
args: ("--dump"),
stdout: "
bar := 'bar'
foo:
echo $bar
",
}
test! {
name: recipe_dependency,
justfile: "
bar:
echo bar
foo: bar
echo foo
",
args: ("--dump"),
stdout: "
bar:
echo bar
foo: bar
echo foo
",
}
test! {
name: recipe_dependency_param,
justfile: "
bar BAR:
echo bar
foo: (bar 'bar')
echo foo
",
args: ("--dump"),
stdout: "
bar BAR:
echo bar
foo: (bar 'bar')
echo foo
",
}
test! {
name: recipe_dependency_params,
justfile: "
bar BAR BAZ:
echo bar
foo: (bar 'bar' 'baz')
echo foo
",
args: ("--dump"),
stdout: "
bar BAR BAZ:
echo bar
foo: (bar 'bar' 'baz')
echo foo
",
}
test! {
name: recipe_dependencies,
justfile: "
bar:
echo bar
baz:
echo baz
foo: baz bar
echo foo
",
args: ("--dump"),
stdout: "
bar:
echo bar
baz:
echo baz
foo: baz bar
echo foo
",
}
test! {
name: recipe_dependencies_params,
justfile: "
bar BAR:
echo bar
baz BAZ:
echo baz
foo: (baz 'baz') (bar 'bar')
echo foo
",
args: ("--dump"),
stdout: "
bar BAR:
echo bar
baz BAZ:
echo baz
foo: (baz 'baz') (bar 'bar')
echo foo
",
}
test! {
name: set_true_explicit,
justfile: "
set export := true
",
args: ("--dump"),
stdout: "
set export := true
",
}
test! {
name: set_true_implicit,
justfile: "
set export
",
args: ("--dump"),
stdout: "
set export := true
",
}
test! {
name: set_false,
justfile: "
set export := false
",
args: ("--dump"),
stdout: "
set export := false
",
}
test! {
name: set_shell,
justfile: r#"
set shell := ['sh', "-c"]
"#,
args: ("--dump"),
stdout: r#"
set shell := ['sh', "-c"]
"#,
}
test! {
name: comment,
justfile: "
# foo
",
args: ("--dump"),
stdout: "
# foo
",
}
test! {
name: comment_multiline,
justfile: "
# foo
# bar
",
args: ("--dump"),
stdout: "
# foo
# bar
",
}
test! {
name: comment_leading,
justfile: "
# foo
foo := 'bar'
",
args: ("--dump"),
stdout: "
# foo
foo := 'bar'
",
}
test! {
name: comment_trailing,
justfile: "
foo := 'bar'
# foo
",
args: ("--dump"),
stdout: "
foo := 'bar'
# foo
",
}
test! {
name: comment_before_recipe,
justfile: "
# foo
foo:
echo foo
",
args: ("--dump"),
stdout: "
# foo
foo:
echo foo
",
}
test! {
name: comment_before_docstring_recipe,
justfile: "
# bar
# foo
foo:
echo foo
",
args: ("--dump"),
stdout: "
# bar
# foo
foo:
echo foo
",
}
test! {
name: group_recipies,
justfile: "
foo:
echo foo
bar:
echo bar
",
args: ("--dump"),
stdout: "
foo:
echo foo
bar:
echo bar
",
}
test! {
name: group_aliases,
justfile: "
alias f := foo
alias b := bar
foo:
echo foo
bar:
echo bar
",
args: ("--dump"),
stdout: "
alias f := foo
alias b := bar
foo:
echo foo
bar:
echo bar
",
}
test! {
name: group_assignments,
justfile: "
foo := 'foo'
bar := 'bar'
",
args: ("--dump"),
stdout: "
foo := 'foo'
bar := 'bar'
",
}
test! {
name: group_sets,
justfile: "
set export := true
set positional-arguments := true
",
args: ("--dump"),
stdout: "
set export := true
set positional-arguments := true
",
}
test! {
name: group_comments,
justfile: "
# foo
# bar
",
args: ("--dump"),
stdout: "
# foo
# bar
",
}
test! {
name: separate_recipes_aliases,
justfile: "
alias f := foo
foo:
echo foo
",
args: ("--dump"),
stdout: "
alias f := foo
foo:
echo foo
",
}
test! {
name: no_trailing_newline,
justfile: "
foo:
echo foo",
args: ("--dump"),
stdout: "
foo:
echo foo
",
}

View File

@ -14,6 +14,7 @@ mod error_messages;
mod evaluate;
mod examples;
mod export;
mod fmt;
mod init;
mod interrupts;
mod invocation_directory;

View File

@ -265,7 +265,7 @@ recipe:
args: ("--show", "recipe"),
stdout: r#"
recipe:
echo {{hello + "bar" + bar}}
echo {{ hello + "bar" + bar }}
"#,
}

View File

@ -75,7 +75,7 @@ impl<'a> Test<'a> {
let mut justfile_path = tmp.path().to_path_buf();
justfile_path.push("justfile");
fs::write(justfile_path, justfile).unwrap();
fs::write(&justfile_path, justfile).unwrap();
let mut dotenv_path = tmp.path().to_path_buf();
dotenv_path.push(".env");