Compare commits

..

10 Commits

Author SHA1 Message Date
Greg Shuflin
09867c48ca Pest parser WIP 2024-07-01 11:36:26 -07:00
Casey Rodarmor
39b2783c4b
Use default values for --list-heading and --list-prefix (#2213) 2024-06-30 21:14:39 +00:00
Greg Shuflin
208187fbb6
Use clap::ValueParser (#2211) 2024-06-30 19:16:10 +00:00
Casey Rodarmor
7683c81c08
Allow unstable features with --summary (#2210) 2024-06-29 18:12:31 -07:00
Casey Rodarmor
e0c031272d
Document module doc comments in readme (#2208) 2024-06-29 19:28:47 +00:00
Jacob Herbst
ef6a813dd1
Give modules doc comments for --list (#2199) 2024-06-28 21:13:11 -07:00
Casey Rodarmor
e07da79d40
Use -and instead of && in PowerShell completion script (#2204) 2024-06-28 07:52:16 +00:00
Casey Rodarmor
97c32e60ae
Fix readme formatting (#2203) 2024-06-27 23:03:05 +00:00
Mateusz Kurowski
929fd695d5
Link to justfiles on GitHub in readme (#2198) 2024-06-27 22:30:52 +00:00
Casey Rodarmor
23f1c1ca9f
Allow comments after mod statements (#2201) 2024-06-27 18:47:33 +00:00
23 changed files with 374 additions and 162 deletions

53
Cargo.lock generated
View File

@ -613,6 +613,8 @@ dependencies = [
"log", "log",
"num_cpus", "num_cpus",
"percent-encoding", "percent-encoding",
"pest",
"pest_derive",
"pretty_assertions", "pretty_assertions",
"rand", "rand",
"regex", "regex",
@ -737,6 +739,51 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
version = "2.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8"
dependencies = [
"memchr",
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "pest_meta"
version = "2.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -1198,6 +1245,12 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-trie"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
[[package]] [[package]]
name = "unicase" name = "unicase"
version = "2.7.0" version = "2.7.0"

View File

@ -37,6 +37,8 @@ libc = "0.2.0"
log = "0.4.4" log = "0.4.4"
num_cpus = "1.15.0" num_cpus = "1.15.0"
percent-encoding = "2.3.1" percent-encoding = "2.3.1"
pest = "2.7.10"
pest_derive = "2.7.10"
rand = "0.8.5" rand = "0.8.5"
regex = "1.10.4" regex = "1.10.4"
semver = "1.0.20" semver = "1.0.20"

View File

@ -603,8 +603,9 @@ testing… all tests passed!
Examples Examples
-------- --------
A variety of example `justfile`s can be found in the A variety of `justfile`s can be found in the
[examples directory](https://github.com/casey/just/tree/master/examples). [examples directory](https://github.com/casey/just/tree/master/examples) and on
[GitHub](https://github.com/search?q=path%3A**%2Fjustfile&type=code).
Features Features
-------- --------
@ -3227,6 +3228,20 @@ mod? foo 'bar.just'
mod? foo 'baz.just' mod? foo 'baz.just'
``` ```
Modules may be given doc comments which appear in `--list`
output<sup>master</sup>:
```mf
# foo is a great module!
mod foo
```
```sh
$ just --list
Available recipes:
foo ... # foo is a great module!
```
See the See the
[module stabilization tracking issue](https://github.com/casey/just/issues/929) [module stabilization tracking issue](https://github.com/casey/just/issues/929)
for more information. for more information.

View File

@ -9,22 +9,24 @@ pub(crate) struct Analyzer<'src> {
impl<'src> Analyzer<'src> { impl<'src> Analyzer<'src> {
pub(crate) fn analyze( pub(crate) fn analyze(
loaded: &[PathBuf],
paths: &HashMap<PathBuf, PathBuf>,
asts: &HashMap<PathBuf, Ast<'src>>, asts: &HashMap<PathBuf, Ast<'src>>,
root: &Path, doc: Option<&'src str>,
loaded: &[PathBuf],
name: Option<Name<'src>>, name: Option<Name<'src>>,
paths: &HashMap<PathBuf, PathBuf>,
root: &Path,
) -> CompileResult<'src, Justfile<'src>> { ) -> CompileResult<'src, Justfile<'src>> {
Self::default().justfile(loaded, paths, asts, root, name) Self::default().justfile(asts, doc, loaded, name, paths, root)
} }
fn justfile( fn justfile(
mut self, mut self,
loaded: &[PathBuf],
paths: &HashMap<PathBuf, PathBuf>,
asts: &HashMap<PathBuf, Ast<'src>>, asts: &HashMap<PathBuf, Ast<'src>>,
root: &Path, doc: Option<&'src str>,
loaded: &[PathBuf],
name: Option<Name<'src>>, name: Option<Name<'src>>,
paths: &HashMap<PathBuf, PathBuf>,
root: &Path,
) -> CompileResult<'src, Justfile<'src>> { ) -> CompileResult<'src, Justfile<'src>> {
let mut recipes = Vec::new(); let mut recipes = Vec::new();
@ -84,10 +86,22 @@ impl<'src> Analyzer<'src> {
stack.push(asts.get(absolute).unwrap()); stack.push(asts.get(absolute).unwrap());
} }
} }
Item::Module { absolute, name, .. } => { Item::Module {
absolute,
name,
doc,
..
} => {
if let Some(absolute) = absolute { if let Some(absolute) = absolute {
define(*name, "module", false)?; define(*name, "module", false)?;
modules.insert(Self::analyze(loaded, paths, asts, absolute, Some(*name))?); modules.insert(Self::analyze(
asts,
*doc,
loaded,
Some(*name),
paths,
absolute,
)?);
} }
} }
Item::Recipe(recipe) => { Item::Recipe(recipe) => {
@ -172,6 +186,7 @@ impl<'src> Analyzer<'src> {
Rc::clone(next) Rc::clone(next)
}), }),
}), }),
doc,
loaded: loaded.into(), loaded: loaded.into(),
modules, modules,
name, name,

View File

@ -38,6 +38,7 @@ impl Color {
} }
} }
#[cfg(test)]
pub(crate) fn always() -> Self { pub(crate) fn always() -> Self {
Self { Self {
use_color: UseColor::Always, use_color: UseColor::Always,
@ -133,6 +134,15 @@ impl Color {
} }
} }
impl From<UseColor> for Color {
fn from(use_color: UseColor) -> Self {
Self {
use_color,
..Default::default()
}
}
}
impl Default for Color { impl Default for Color {
fn default() -> Self { fn default() -> Self {
Self { Self {

26
src/command_color.rs Normal file
View File

@ -0,0 +1,26 @@
use super::*;
#[derive(Copy, Clone, ValueEnum)]
pub(crate) enum CommandColor {
Black,
Blue,
Cyan,
Green,
Purple,
Red,
Yellow,
}
impl From<CommandColor> for ansi_term::Color {
fn from(command_color: CommandColor) -> Self {
match command_color {
CommandColor::Black => Self::Black,
CommandColor::Blue => Self::Blue,
CommandColor::Cyan => Self::Cyan,
CommandColor::Green => Self::Green,
CommandColor::Purple => Self::Purple,
CommandColor::Red => Self::Red,
CommandColor::Yellow => Self::Yellow,
}
}
}

View File

@ -40,6 +40,7 @@ impl Compiler {
name, name,
optional, optional,
relative, relative,
..
} => { } => {
if !unstable { if !unstable {
return Err(Error::Unstable { return Err(Error::Unstable {
@ -107,7 +108,7 @@ impl Compiler {
asts.insert(current.path, ast.clone()); asts.insert(current.path, ast.clone());
} }
let justfile = Analyzer::analyze(&loaded, &paths, &asts, root, None)?; let justfile = Analyzer::analyze(&asts, None, &loaded, None, &paths, root)?;
Ok(Compilation { Ok(Compilation {
asts, asts,
@ -184,7 +185,7 @@ impl Compiler {
asts.insert(root.clone(), ast); asts.insert(root.clone(), ast);
let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new(); let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
paths.insert(root.clone(), root.clone()); paths.insert(root.clone(), root.clone());
Analyzer::analyze(&[], &paths, &asts, &root, None) Analyzer::analyze(&asts, None, &[], None, &paths, &root)
} }
} }

View File

@ -1,4 +1,4 @@
use {super::*, clap::ValueEnum}; use super::*;
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq)] #[derive(ValueEnum, Debug, Clone, Copy, PartialEq)]
pub(crate) enum Shell { pub(crate) enum Shell {
@ -255,7 +255,7 @@ const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
r#"function Get-JustFileRecipes([string[]]$CommandElements) { r#"function Get-JustFileRecipes([string[]]$CommandElements) {
$justFileIndex = $commandElements.IndexOf("--justfile"); $justFileIndex = $commandElements.IndexOf("--justfile");
if ($justFileIndex -ne -1 && $justFileIndex + 1 -le $commandElements.Length) { if ($justFileIndex -ne -1 -and $justFileIndex + 1 -le $commandElements.Length) {
$justFileLocation = $commandElements[$justFileIndex + 1] $justFileLocation = $commandElements[$justFileIndex + 1]
} }

View File

@ -1,7 +1,7 @@
use { use {
super::*, super::*,
clap::{ clap::{
builder::{styling::AnsiColor, FalseyValueParser, PossibleValuesParser, Styles}, builder::{styling::AnsiColor, FalseyValueParser, Styles},
parser::ValuesRef, parser::ValuesRef,
value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command, value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command,
}, },
@ -108,32 +108,6 @@ mod arg {
pub(crate) const VERBOSE: &str = "VERBOSE"; pub(crate) const VERBOSE: &str = "VERBOSE";
pub(crate) const WORKING_DIRECTORY: &str = "WORKING-DIRECTORY"; pub(crate) const WORKING_DIRECTORY: &str = "WORKING-DIRECTORY";
pub(crate) const YES: &str = "YES"; pub(crate) const YES: &str = "YES";
pub(crate) const COLOR_ALWAYS: &str = "always";
pub(crate) const COLOR_AUTO: &str = "auto";
pub(crate) const COLOR_NEVER: &str = "never";
pub(crate) const COLOR_VALUES: &[&str] = &[COLOR_AUTO, COLOR_ALWAYS, COLOR_NEVER];
pub(crate) const COMMAND_COLOR_BLACK: &str = "black";
pub(crate) const COMMAND_COLOR_BLUE: &str = "blue";
pub(crate) const COMMAND_COLOR_CYAN: &str = "cyan";
pub(crate) const COMMAND_COLOR_GREEN: &str = "green";
pub(crate) const COMMAND_COLOR_PURPLE: &str = "purple";
pub(crate) const COMMAND_COLOR_RED: &str = "red";
pub(crate) const COMMAND_COLOR_YELLOW: &str = "yellow";
pub(crate) const COMMAND_COLOR_VALUES: &[&str] = &[
COMMAND_COLOR_BLACK,
COMMAND_COLOR_BLUE,
COMMAND_COLOR_CYAN,
COMMAND_COLOR_GREEN,
COMMAND_COLOR_PURPLE,
COMMAND_COLOR_RED,
COMMAND_COLOR_YELLOW,
];
pub(crate) const DUMP_FORMAT_JSON: &str = "json";
pub(crate) const DUMP_FORMAT_JUST: &str = "just";
pub(crate) const DUMP_FORMAT_VALUES: &[&str] = &[DUMP_FORMAT_JUST, DUMP_FORMAT_JSON];
} }
impl Config { impl Config {
@ -184,8 +158,8 @@ impl Config {
.long("color") .long("color")
.env("JUST_COLOR") .env("JUST_COLOR")
.action(ArgAction::Set) .action(ArgAction::Set)
.value_parser(PossibleValuesParser::new(arg::COLOR_VALUES)) .value_parser(clap::value_parser!(UseColor))
.default_value(arg::COLOR_AUTO) .default_value("auto")
.help("Print colorful output"), .help("Print colorful output"),
) )
.arg( .arg(
@ -193,7 +167,7 @@ impl Config {
.long("command-color") .long("command-color")
.env("JUST_COMMAND_COLOR") .env("JUST_COMMAND_COLOR")
.action(ArgAction::Set) .action(ArgAction::Set)
.value_parser(PossibleValuesParser::new(arg::COMMAND_COLOR_VALUES)) .value_parser(clap::value_parser!(CommandColor))
.help("Echo recipe lines in <COMMAND-COLOR>"), .help("Echo recipe lines in <COMMAND-COLOR>"),
) )
.arg( .arg(
@ -225,8 +199,8 @@ impl Config {
.long("dump-format") .long("dump-format")
.env("JUST_DUMP_FORMAT") .env("JUST_DUMP_FORMAT")
.action(ArgAction::Set) .action(ArgAction::Set)
.value_parser(PossibleValuesParser::new(arg::DUMP_FORMAT_VALUES)) .value_parser(clap::value_parser!(DumpFormat))
.default_value(arg::DUMP_FORMAT_JUST) .default_value("just")
.value_name("FORMAT") .value_name("FORMAT")
.help("Dump justfile as <FORMAT>"), .help("Dump justfile as <FORMAT>"),
) )
@ -262,6 +236,7 @@ impl Config {
.env("JUST_LIST_HEADING") .env("JUST_LIST_HEADING")
.help("Print <TEXT> before list") .help("Print <TEXT> before list")
.value_name("TEXT") .value_name("TEXT")
.default_value("Available recipes:\n")
.action(ArgAction::Set), .action(ArgAction::Set),
) )
.arg( .arg(
@ -270,6 +245,7 @@ impl Config {
.env("JUST_LIST_PREFIX") .env("JUST_LIST_PREFIX")
.help("Print <TEXT> before each list item") .help("Print <TEXT> before each list item")
.value_name("TEXT") .value_name("TEXT")
.default_value(" ")
.action(ArgAction::Set), .action(ArgAction::Set),
) )
.arg( .arg(
@ -531,59 +507,6 @@ impl Config {
) )
} }
fn color_from_matches(matches: &ArgMatches) -> ConfigResult<Color> {
let value = matches
.get_one::<String>(arg::COLOR)
.ok_or_else(|| ConfigError::Internal {
message: "`--color` had no value".to_string(),
})?;
match value.as_str() {
arg::COLOR_AUTO => Ok(Color::auto()),
arg::COLOR_ALWAYS => Ok(Color::always()),
arg::COLOR_NEVER => Ok(Color::never()),
_ => Err(ConfigError::Internal {
message: format!("Invalid argument `{value}` to --color."),
}),
}
}
fn command_color_from_matches(matches: &ArgMatches) -> ConfigResult<Option<ansi_term::Color>> {
if let Some(value) = matches.get_one::<String>(arg::COMMAND_COLOR) {
match value.as_str() {
arg::COMMAND_COLOR_BLACK => Ok(Some(ansi_term::Color::Black)),
arg::COMMAND_COLOR_BLUE => Ok(Some(ansi_term::Color::Blue)),
arg::COMMAND_COLOR_CYAN => Ok(Some(ansi_term::Color::Cyan)),
arg::COMMAND_COLOR_GREEN => Ok(Some(ansi_term::Color::Green)),
arg::COMMAND_COLOR_PURPLE => Ok(Some(ansi_term::Color::Purple)),
arg::COMMAND_COLOR_RED => Ok(Some(ansi_term::Color::Red)),
arg::COMMAND_COLOR_YELLOW => Ok(Some(ansi_term::Color::Yellow)),
value => Err(ConfigError::Internal {
message: format!("Invalid argument `{value}` to --command-color."),
}),
}
} else {
Ok(None)
}
}
fn dump_format_from_matches(matches: &ArgMatches) -> ConfigResult<DumpFormat> {
let value =
matches
.get_one::<String>(arg::DUMP_FORMAT)
.ok_or_else(|| ConfigError::Internal {
message: "`--dump-format` had no value".to_string(),
})?;
match value.as_str() {
arg::DUMP_FORMAT_JSON => Ok(DumpFormat::Json),
arg::DUMP_FORMAT_JUST => Ok(DumpFormat::Just),
_ => Err(ConfigError::Internal {
message: format!("Invalid argument `{value}` to --dump-format."),
}),
}
}
fn parse_module_path(values: ValuesRef<String>) -> ConfigResult<ModulePath> { fn parse_module_path(values: ValuesRef<String>) -> ConfigResult<ModulePath> {
let path = values.clone().map(|s| (*s).as_str()).collect::<Vec<&str>>(); let path = values.clone().map(|s| (*s).as_str()).collect::<Vec<&str>>();
@ -744,24 +667,28 @@ impl Config {
} }
}; };
let unstable = matches.get_flag(arg::UNSTABLE) || subcommand == Subcommand::Summary;
Ok(Self { Ok(Self {
check: matches.get_flag(arg::CHECK), check: matches.get_flag(arg::CHECK),
color: Self::color_from_matches(matches)?, color: (*matches.get_one::<UseColor>(arg::COLOR).unwrap()).into(),
command_color: Self::command_color_from_matches(matches)?, command_color: matches
.get_one::<CommandColor>(arg::COMMAND_COLOR)
.copied()
.map(CommandColor::into),
dotenv_filename: matches dotenv_filename: matches
.get_one::<String>(arg::DOTENV_FILENAME) .get_one::<String>(arg::DOTENV_FILENAME)
.map(Into::into), .map(Into::into),
dotenv_path: matches.get_one::<PathBuf>(arg::DOTENV_PATH).map(Into::into), dotenv_path: matches.get_one::<PathBuf>(arg::DOTENV_PATH).map(Into::into),
dry_run: matches.get_flag(arg::DRY_RUN), dry_run: matches.get_flag(arg::DRY_RUN),
dump_format: Self::dump_format_from_matches(matches)?, dump_format: matches
.get_one::<DumpFormat>(arg::DUMP_FORMAT)
.unwrap()
.clone(),
highlight: !matches.get_flag(arg::NO_HIGHLIGHT), highlight: !matches.get_flag(arg::NO_HIGHLIGHT),
invocation_directory: env::current_dir().context(config_error::CurrentDirContext)?, invocation_directory: env::current_dir().context(config_error::CurrentDirContext)?,
list_heading: matches list_heading: matches.get_one::<String>(arg::LIST_HEADING).unwrap().into(),
.get_one::<String>(arg::LIST_HEADING) list_prefix: matches.get_one::<String>(arg::LIST_PREFIX).unwrap().into(),
.map_or_else(|| "Available recipes:\n".into(), Into::into),
list_prefix: matches
.get_one::<String>(arg::LIST_PREFIX)
.map_or_else(|| " ".into(), Into::into),
list_submodules: matches.get_flag(arg::LIST_SUBMODULES), list_submodules: matches.get_flag(arg::LIST_SUBMODULES),
load_dotenv: !matches.get_flag(arg::NO_DOTENV), load_dotenv: !matches.get_flag(arg::NO_DOTENV),
no_aliases: matches.get_flag(arg::NO_ALIASES), no_aliases: matches.get_flag(arg::NO_ALIASES),
@ -783,7 +710,7 @@ impl Config {
.unwrap() .unwrap()
.into(), .into(),
unsorted: matches.get_flag(arg::UNSORTED), unsorted: matches.get_flag(arg::UNSORTED),
unstable: matches.get_flag(arg::UNSTABLE), unstable,
verbosity: if matches.get_flag(arg::QUIET) { verbosity: if matches.get_flag(arg::QUIET) {
Verbosity::Quiet Verbosity::Quiet
} else { } else {
@ -834,6 +761,7 @@ mod tests {
$(shell_args: $shell_args:expr,)? $(shell_args: $shell_args:expr,)?
$(subcommand: $subcommand:expr,)? $(subcommand: $subcommand:expr,)?
$(unsorted: $unsorted:expr,)? $(unsorted: $unsorted:expr,)?
$(unstable: $unstable:expr,)?
$(verbosity: $verbosity:expr,)? $(verbosity: $verbosity:expr,)?
} => { } => {
#[test] #[test]
@ -854,6 +782,7 @@ mod tests {
$(shell_args: $shell_args,)? $(shell_args: $shell_args,)?
$(subcommand: $subcommand,)? $(subcommand: $subcommand,)?
$(unsorted: $unsorted,)? $(unsorted: $unsorted,)?
$(unstable: $unstable,)?
$(verbosity: $verbosity,)? $(verbosity: $verbosity,)?
..testing::config(&[]) ..testing::config(&[])
}; };
@ -1368,6 +1297,7 @@ mod tests {
name: subcommand_summary, name: subcommand_summary,
args: ["--summary"], args: ["--summary"],
subcommand: Subcommand::Summary, subcommand: Subcommand::Summary,
unstable: true,
} }
test! { test! {

View File

@ -1,4 +1,6 @@
#[derive(Debug, PartialEq)] use super::*;
#[derive(Debug, PartialEq, Clone, ValueEnum)]
pub(crate) enum DumpFormat { pub(crate) enum DumpFormat {
Json, Json,
Just, Just,

View File

@ -14,6 +14,7 @@ pub(crate) enum Item<'src> {
}, },
Module { Module {
absolute: Option<PathBuf>, absolute: Option<PathBuf>,
doc: Option<&'src str>,
name: Name<'src>, name: Name<'src>,
optional: bool, optional: bool,
relative: Option<StringLiteral<'src>>, relative: Option<StringLiteral<'src>>,

9
src/justfile.pest Normal file
View File

@ -0,0 +1,9 @@
identifier = { (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")+ }
recipe = { identifier ~ ":" ~ NEWLINE ~ recipe_body }
recipe_body = { recipe_line* }
recipe_line = {

View File

@ -13,6 +13,7 @@ struct Invocation<'src: 'run, 'run> {
pub(crate) struct Justfile<'src> { pub(crate) struct Justfile<'src> {
pub(crate) aliases: Table<'src, Alias<'src>>, pub(crate) aliases: Table<'src, Alias<'src>>,
pub(crate) assignments: Table<'src, Assignment<'src>>, pub(crate) assignments: Table<'src, Assignment<'src>>,
pub(crate) doc: Option<&'src str>,
#[serde(rename = "first", serialize_with = "keyed::serialize_option")] #[serde(rename = "first", serialize_with = "keyed::serialize_option")]
pub(crate) default: Option<Rc<Recipe<'src>>>, pub(crate) default: Option<Rc<Recipe<'src>>>,
#[serde(skip)] #[serde(skip)]

View File

@ -23,29 +23,30 @@ pub(crate) use {
crate::{ crate::{
alias::Alias, analyzer::Analyzer, argument_parser::ArgumentParser, assignment::Assignment, alias::Alias, analyzer::Analyzer, argument_parser::ArgumentParser, assignment::Assignment,
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding, assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation, color::Color, color_display::ColorDisplay, command_color::CommandColor,
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler, command_ext::CommandExt, compilation::Compilation, compile_error::CompileError,
condition::Condition, conditional_operator::ConditionalOperator, config::Config, compile_error_kind::CompileErrorKind, compiler::Compiler, condition::Condition,
config_error::ConfigError, constants::constants, count::Count, delimiter::Delimiter, conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError,
dependency::Dependency, dump_format::DumpFormat, enclosure::Enclosure, error::Error, constants::constants, count::Count, delimiter::Delimiter, dependency::Dependency,
evaluator::Evaluator, execution_context::ExecutionContext, expression::Expression, dump_format::DumpFormat, enclosure::Enclosure, error::Error, evaluator::Evaluator,
fragment::Fragment, function::Function, interrupt_guard::InterruptGuard, execution_context::ExecutionContext, expression::Expression, fragment::Fragment,
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyed::Keyed, function::Function, interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler,
keyword::Keyword, lexer::Lexer, line::Line, list::List, load_dotenv::load_dotenv, item::Item, justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line,
loader::Loader, module_path::ModulePath, name::Name, namepath::Namepath, ordinal::Ordinal, list::List, load_dotenv::load_dotenv, loader::Loader, module_path::ModulePath, name::Name,
output::output, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind, namepath::Namepath, ordinal::Ordinal, output::output, output_error::OutputError,
parser::Parser, platform::Platform, platform_interface::PlatformInterface, position::Position, parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform,
positional::Positional, ran::Ran, range_ext::RangeExt, recipe::Recipe, platform_interface::PlatformInterface, position::Position, positional::Positional, ran::Ran,
recipe_resolver::RecipeResolver, recipe_signature::RecipeSignature, scope::Scope, range_ext::RangeExt, recipe::Recipe, recipe_resolver::RecipeResolver,
search::Search, search_config::SearchConfig, search_error::SearchError, set::Set, recipe_signature::RecipeSignature, scope::Scope, search::Search, search_config::SearchConfig,
setting::Setting, settings::Settings, shebang::Shebang, shell::Shell, search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang,
show_whitespace::ShowWhitespace, source::Source, string_kind::StringKind, shell::Shell, show_whitespace::ShowWhitespace, source::Source, string_kind::StringKind,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table, string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
verbosity::Verbosity, warning::Warning, verbosity::Verbosity, warning::Warning,
}, },
camino::Utf8Path, camino::Utf8Path,
clap::ValueEnum,
derivative::Derivative, derivative::Derivative,
edit_distance::edit_distance, edit_distance::edit_distance,
lexiclean::Lexiclean, lexiclean::Lexiclean,
@ -128,6 +129,7 @@ mod attribute;
mod binding; mod binding;
mod color; mod color;
mod color_display; mod color_display;
mod command_color;
mod command_ext; mod command_ext;
mod compilation; mod compilation;
mod compile_error; mod compile_error;

View File

@ -36,7 +36,18 @@ pub(crate) struct Parser<'run, 'src> {
working_directory: &'run Path, working_directory: &'run Path,
} }
#[derive(pest_derive::Parser)]
#[grammar = "justfile.pest"]
struct JustfileParser;
impl<'run, 'src> Parser<'run, 'src> { impl<'run, 'src> Parser<'run, 'src> {
pub(crate) fn parse_new(path: &'src Path, src: &'src str) -> CompileResult<'src, Ast<'src>> {
todo!()
}
/// Parse `tokens` into an `Ast` /// Parse `tokens` into an `Ast`
pub(crate) fn parse( pub(crate) fn parse(
file_depth: u32, file_depth: u32,
@ -365,12 +376,15 @@ impl<'run, 'src> Parser<'run, 'src> {
}); });
} }
Some(Keyword::Mod) Some(Keyword::Mod)
if self.next_are(&[Identifier, Identifier, StringToken]) if self.next_are(&[Identifier, Identifier, Comment])
|| self.next_are(&[Identifier, Identifier, Identifier, StringToken])
|| self.next_are(&[Identifier, Identifier, Eof]) || self.next_are(&[Identifier, Identifier, Eof])
|| self.next_are(&[Identifier, Identifier, Eol]) || self.next_are(&[Identifier, Identifier, Eol])
|| self.next_are(&[Identifier, Identifier, Identifier, StringToken])
|| self.next_are(&[Identifier, Identifier, StringToken])
|| self.next_are(&[Identifier, QuestionMark]) => || self.next_are(&[Identifier, QuestionMark]) =>
{ {
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
self.presume_keyword(Keyword::Mod)?; self.presume_keyword(Keyword::Mod)?;
let optional = self.accepted(QuestionMark)?; let optional = self.accepted(QuestionMark)?;
@ -386,6 +400,7 @@ impl<'run, 'src> Parser<'run, 'src> {
items.push(Item::Module { items.push(Item::Module {
absolute: None, absolute: None,
doc,
name, name,
optional, optional,
relative, relative,

View File

@ -435,6 +435,27 @@ impl Subcommand {
} }
fn list_module(config: &Config, module: &Justfile, depth: usize) { fn list_module(config: &Config, module: &Justfile, depth: usize) {
fn format_doc(
config: &Config,
name: &str,
doc: Option<&str>,
max_signature_width: usize,
signature_widths: &BTreeMap<&str, usize>,
) {
if let Some(doc) = doc {
if doc.lines().count() <= 1 {
print!(
"{:padding$}{} {}",
"",
config.color.stdout().doc().paint("#"),
config.color.stdout().doc().paint(doc),
padding = max_signature_width.saturating_sub(signature_widths[name]) + 1,
);
}
}
println!();
}
let aliases = if config.no_aliases { let aliases = if config.no_aliases {
BTreeMap::new() BTreeMap::new()
} else { } else {
@ -468,6 +489,11 @@ impl Subcommand {
); );
} }
} }
if !config.list_submodules {
for (name, _) in &module.modules {
signature_widths.insert(name, UnicodeWidthStr::width(format!("{name} ...").as_str()));
}
}
signature_widths signature_widths
}; };
@ -554,20 +580,15 @@ impl Subcommand {
RecipeSignature { name, recipe }.color_display(config.color.stdout()) RecipeSignature { name, recipe }.color_display(config.color.stdout())
); );
if let Some(doc) = doc { format_doc(
if doc.lines().count() <= 1 { config,
print!( name,
"{:padding$}{} {}", doc.as_deref(),
"", max_signature_width,
config.color.stdout().doc().paint("#"), &signature_widths,
config.color.stdout().doc().paint(&doc),
padding = max_signature_width.saturating_sub(signature_widths[name]) + 1,
); );
} }
} }
println!();
}
}
} }
if config.list_submodules { if config.list_submodules {
@ -582,7 +603,14 @@ impl Subcommand {
} }
} else { } else {
for submodule in module.modules(config) { for submodule in module.modules(config) {
println!("{list_prefix}{} ...", submodule.name(),); print!("{list_prefix}{} ...", submodule.name());
format_doc(
config,
submodule.name(),
submodule.doc,
max_signature_width,
&signature_widths,
);
} }
} }
} }

View File

@ -77,7 +77,7 @@ pub(crate) fn analysis_error(
let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new(); let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
paths.insert("justfile".into(), "justfile".into()); paths.insert("justfile".into(), "justfile".into());
match Analyzer::analyze(&[], &paths, &asts, &root, None) { match Analyzer::analyze(&asts, None, &[], None, &paths, &root) {
Ok(_) => panic!("Analysis unexpectedly succeeded"), Ok(_) => panic!("Analysis unexpectedly succeeded"),
Err(have) => { Err(have) => {
let want = CompileError { let want = CompileError {

View File

@ -1,4 +1,6 @@
#[derive(Copy, Clone, Debug, PartialEq)] use super::*;
#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)]
pub(crate) enum UseColor { pub(crate) enum UseColor {
Auto, Auto,
Always, Always,

View File

@ -34,10 +34,10 @@ b := env_var_or_default('ZADDY', 'HTAP')
x := env_var_or_default('XYZ', 'ABC') x := env_var_or_default('XYZ', 'ABC')
foo: foo:
/bin/echo '{{p}}' '{{b}}' '{{x}}' /usr/bin/env echo '{{p}}' '{{b}}' '{{x}}'
"#, "#,
stdout: format!("{} HTAP ABC\n", env::var("USER").unwrap()).as_str(), stdout: format!("{} HTAP ABC\n", env::var("USER").unwrap()).as_str(),
stderr: format!("/bin/echo '{}' 'HTAP' 'ABC'\n", env::var("USER").unwrap()).as_str(), stderr: format!("/usr/bin/env echo '{}' 'HTAP' 'ABC'\n", env::var("USER").unwrap()).as_str(),
} }
#[cfg(not(windows))] #[cfg(not(windows))]
@ -52,10 +52,10 @@ ext := extension('/foo/bar/baz.hello')
jn := join('a', 'b') jn := join('a', 'b')
foo: foo:
/bin/echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}' '{{jn}}' /usr/bin/env echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}' '{{jn}}'
"#, "#,
stdout: "/foo/bar/baz baz baz.hello /foo/bar hello a/b\n", stdout: "/foo/bar/baz baz baz.hello /foo/bar hello a/b\n",
stderr: "/bin/echo '/foo/bar/baz' 'baz' 'baz.hello' '/foo/bar' 'hello' 'a/b'\n", stderr: "/usr/bin/env echo '/foo/bar/baz' 'baz' 'baz.hello' '/foo/bar' 'hello' 'a/b'\n",
} }
#[cfg(not(windows))] #[cfg(not(windows))]
@ -69,10 +69,10 @@ dir := parent_directory('/foo/')
ext := extension('/foo/bar/baz.hello.ciao') ext := extension('/foo/bar/baz.hello.ciao')
foo: foo:
/bin/echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}' /usr/bin/env echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}'
"#, "#,
stdout: "/foo/bar/baz baz.hello baz.hello.ciao / ciao\n", stdout: "/foo/bar/baz baz.hello baz.hello.ciao / ciao\n",
stderr: "/bin/echo '/foo/bar/baz' 'baz.hello' 'baz.hello.ciao' '/' 'ciao'\n", stderr: "/usr/bin/env echo '/foo/bar/baz' 'baz.hello' 'baz.hello.ciao' '/' 'ciao'\n",
} }
#[cfg(not(windows))] #[cfg(not(windows))]
@ -82,7 +82,7 @@ test! {
we := without_extension('') we := without_extension('')
foo: foo:
/bin/echo '{{we}}' /usr/bin/env echo '{{we}}'
"#, "#,
stdout: "", stdout: "",
stderr: format!("{} {}\n{}\n{}\n{}\n{}\n", stderr: format!("{} {}\n{}\n{}\n{}\n{}\n",
@ -102,7 +102,7 @@ test! {
we := extension('') we := extension('')
foo: foo:
/bin/echo '{{we}}' /usr/bin/env echo '{{we}}'
"#, "#,
stdout: "", stdout: "",
stderr: format!("{}\n{}\n{}\n{}\n{}\n", stderr: format!("{}\n{}\n{}\n{}\n{}\n",
@ -121,7 +121,7 @@ test! {
we := extension('foo') we := extension('foo')
foo: foo:
/bin/echo '{{we}}' /usr/bin/env echo '{{we}}'
"#, "#,
stdout: "", stdout: "",
stderr: format!("{}\n{}\n{}\n{}\n{}\n", stderr: format!("{}\n{}\n{}\n{}\n{}\n",
@ -140,7 +140,7 @@ test! {
we := file_stem('') we := file_stem('')
foo: foo:
/bin/echo '{{we}}' /usr/bin/env echo '{{we}}'
"#, "#,
stdout: "", stdout: "",
stderr: format!("{}\n{}\n{}\n{}\n{}\n", stderr: format!("{}\n{}\n{}\n{}\n{}\n",
@ -159,7 +159,7 @@ test! {
we := file_name('') we := file_name('')
foo: foo:
/bin/echo '{{we}}' /usr/bin/env echo '{{we}}'
"#, "#,
stdout: "", stdout: "",
stderr: format!("{}\n{}\n{}\n{}\n{}\n", stderr: format!("{}\n{}\n{}\n{}\n{}\n",
@ -178,7 +178,7 @@ test! {
we := parent_directory('') we := parent_directory('')
foo: foo:
/bin/echo '{{we}}' /usr/bin/env echo '{{we}}'
"#, "#,
stdout: "", stdout: "",
stderr: format!("{} {}\n{}\n{}\n{}\n{}\n", stderr: format!("{} {}\n{}\n{}\n{}\n{}\n",
@ -198,7 +198,7 @@ test! {
we := parent_directory('/') we := parent_directory('/')
foo: foo:
/bin/echo '{{we}}' /usr/bin/env echo '{{we}}'
"#, "#,
stdout: "", stdout: "",
stderr: format!("{} {}\n{}\n{}\n{}\n{}\n", stderr: format!("{} {}\n{}\n{}\n{}\n{}\n",
@ -220,10 +220,10 @@ b := env_var_or_default('ZADDY', 'HTAP')
x := env_var_or_default('XYZ', 'ABC') x := env_var_or_default('XYZ', 'ABC')
foo: foo:
/bin/echo '{{p}}' '{{b}}' '{{x}}' /usr/bin/env echo '{{p}}' '{{b}}' '{{x}}'
"#, "#,
stdout: format!("{} HTAP ABC\n", env::var("USERNAME").unwrap()).as_str(), stdout: format!("{} HTAP ABC\n", env::var("USERNAME").unwrap()).as_str(),
stderr: format!("/bin/echo '{}' 'HTAP' 'ABC'\n", env::var("USERNAME").unwrap()).as_str(), stderr: format!("/usr/bin/env echo '{}' 'HTAP' 'ABC'\n", env::var("USERNAME").unwrap()).as_str(),
} }
test! { test! {

View File

@ -18,6 +18,7 @@ fn alias() {
", ",
json!({ json!({
"first": "foo", "first": "foo",
"doc": null,
"aliases": { "aliases": {
"f": { "f": {
"name": "f", "name": "f",
@ -80,6 +81,7 @@ fn assignment() {
} }
}, },
"first": null, "first": null,
"doc": null,
"modules": {}, "modules": {},
"recipes": {}, "recipes": {},
"settings": { "settings": {
@ -117,6 +119,7 @@ fn body() {
"aliases": {}, "aliases": {},
"assignments": {}, "assignments": {},
"first": "foo", "first": "foo",
"doc": null,
"modules": {}, "modules": {},
"recipes": { "recipes": {
"foo": { "foo": {
@ -170,6 +173,7 @@ fn dependencies() {
"aliases": {}, "aliases": {},
"assignments": {}, "assignments": {},
"first": "foo", "first": "foo",
"doc": null,
"modules": {}, "modules": {},
"recipes": { "recipes": {
"bar": { "bar": {
@ -248,6 +252,7 @@ fn dependency_argument() {
json!({ json!({
"aliases": {}, "aliases": {},
"first": "foo", "first": "foo",
"doc": null,
"assignments": { "assignments": {
"x": { "x": {
"export": false, "export": false,
@ -341,6 +346,7 @@ fn duplicate_recipes() {
", ",
json!({ json!({
"first": "foo", "first": "foo",
"doc": null,
"aliases": { "aliases": {
"f": { "f": {
"attributes": [], "attributes": [],
@ -414,6 +420,7 @@ fn duplicate_variables() {
} }
}, },
"first": null, "first": null,
"doc": null,
"modules": {}, "modules": {},
"recipes": {}, "recipes": {},
"settings": { "settings": {
@ -446,6 +453,7 @@ fn doc_comment() {
json!({ json!({
"aliases": {}, "aliases": {},
"first": "foo", "first": "foo",
"doc": null,
"assignments": {}, "assignments": {},
"modules": {}, "modules": {},
"recipes": { "recipes": {
@ -494,6 +502,7 @@ fn empty_justfile() {
"aliases": {}, "aliases": {},
"assignments": {}, "assignments": {},
"first": null, "first": null,
"doc": null,
"modules": {}, "modules": {},
"recipes": {}, "recipes": {},
"settings": { "settings": {
@ -533,6 +542,7 @@ fn parameters() {
json!({ json!({
"aliases": {}, "aliases": {},
"first": "a", "first": "a",
"doc": null,
"assignments": {}, "assignments": {},
"modules": {}, "modules": {},
"recipes": { "recipes": {
@ -685,6 +695,7 @@ fn priors() {
"aliases": {}, "aliases": {},
"assignments": {}, "assignments": {},
"first": "a", "first": "a",
"doc": null,
"modules": {}, "modules": {},
"recipes": { "recipes": {
"a": { "a": {
@ -768,6 +779,7 @@ fn private() {
"aliases": {}, "aliases": {},
"assignments": {}, "assignments": {},
"first": "_foo", "first": "_foo",
"doc": null,
"modules": {}, "modules": {},
"recipes": { "recipes": {
"_foo": { "_foo": {
@ -815,6 +827,7 @@ fn quiet() {
"aliases": {}, "aliases": {},
"assignments": {}, "assignments": {},
"first": "foo", "first": "foo",
"doc": null,
"modules": {}, "modules": {},
"recipes": { "recipes": {
"foo": { "foo": {
@ -874,6 +887,7 @@ fn settings() {
"aliases": {}, "aliases": {},
"assignments": {}, "assignments": {},
"first": "foo", "first": "foo",
"doc": null,
"modules": {}, "modules": {},
"recipes": { "recipes": {
"foo": { "foo": {
@ -927,6 +941,7 @@ fn shebang() {
"aliases": {}, "aliases": {},
"assignments": {}, "assignments": {},
"first": "foo", "first": "foo",
"doc": null,
"modules": {}, "modules": {},
"recipes": { "recipes": {
"foo": { "foo": {
@ -974,6 +989,7 @@ fn simple() {
"aliases": {}, "aliases": {},
"assignments": {}, "assignments": {},
"first": "foo", "first": "foo",
"doc": null,
"modules": {}, "modules": {},
"recipes": { "recipes": {
"foo": { "foo": {
@ -1024,6 +1040,7 @@ fn attribute() {
"aliases": {}, "aliases": {},
"assignments": {}, "assignments": {},
"first": "foo", "first": "foo",
"doc": null,
"modules": {}, "modules": {},
"recipes": { "recipes": {
"foo": { "foo": {
@ -1068,6 +1085,7 @@ fn module() {
Test::new() Test::new()
.justfile( .justfile(
" "
# hello
mod foo mod foo
", ",
) )
@ -1082,11 +1100,13 @@ fn module() {
"aliases": {}, "aliases": {},
"assignments": {}, "assignments": {},
"first": null, "first": null,
"doc": null,
"modules": { "modules": {
"foo": { "foo": {
"aliases": {}, "aliases": {},
"assignments": {}, "assignments": {},
"first": "bar", "first": "bar",
"doc": "hello",
"modules": {}, "modules": {},
"recipes": { "recipes": {
"bar": { "bar": {

View File

@ -353,3 +353,55 @@ fn nested_modules_are_properly_indented() {
) )
.run(); .run();
} }
#[test]
fn module_doc_rendered() {
Test::new()
.write("foo.just", "")
.justfile(
"
# Module foo
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "--list"])
.stdout(
"
Available recipes:
foo ... # Module foo
",
)
.run();
}
#[test]
fn module_doc_aligned() {
Test::new()
.write("foo.just", "")
.write("bar.just", "")
.justfile(
"
# Module foo
mod foo
# comment
mod very_long_name_for_module \"bar.just\" # comment
# will change your world
recipe:
@echo Hi
",
)
.test_round_trip(false)
.args(["--unstable", "--list"])
.stdout(
"
Available recipes:
recipe # will change your world
foo ... # Module foo
very_long_name_for_module ... # comment
",
)
.run();
}

View File

@ -8,8 +8,6 @@ fn modules_are_unstable() {
mod foo mod foo
", ",
) )
.arg("foo")
.arg("foo")
.stderr( .stderr(
"error: Modules are currently unstable. \ "error: Modules are currently unstable. \
Invoke `just` with the `--unstable` flag to enable unstable features.\n", Invoke `just` with the `--unstable` flag to enable unstable features.\n",
@ -781,3 +779,18 @@ fn colon_separated_path_components_are_not_used_as_arguments() {
.status(1) .status(1)
.run(); .run();
} }
#[test]
fn comments_can_follow_modules() {
Test::new()
.write("foo.just", "foo:\n @echo FOO")
.justfile(
"
mod foo # this is foo
",
)
.test_round_trip(false)
.args(["--unstable", "foo", "foo"])
.stdout("FOO\n")
.run();
}

View File

@ -71,3 +71,18 @@ fn submodule_recipes() {
.stdout("bar foo::foo foo::bar::bar foo::bar::baz::baz foo::bar::baz::biz::biz\n") .stdout("bar foo::foo foo::bar::bar foo::bar::baz::baz foo::bar::baz::biz::biz\n")
.run(); .run();
} }
#[test]
fn summary_implies_unstable() {
Test::new()
.write("foo.just", "foo:")
.justfile(
"
mod foo
",
)
.test_round_trip(false)
.arg("--summary")
.stdout("foo::foo\n")
.run();
}