Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow listing submodules with --list PATH #2108

Merged
merged 4 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,24 @@ Available recipes:
lint
```

Recipes in submodules can be listed with `just --list PATH`, where `PATH` is a
space- or `::`-separated module path:

```
$ cat justfile
mod foo
$ cat foo.just
mod bar
$ cat bar.just
baz:
$ just --unstable foo bar
Available recipes:
baz
$ just --unstable foo::bar
Available recipes:
baz
```

`just --summary` is more concise:

```sh
Expand Down
8 changes: 8 additions & 0 deletions completions/just.bash
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ _just() {
COMPREPLY=($(compgen -W "bash elvish fish powershell zsh" -- "${cur}"))
return 0
;;
--list)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-l)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--show)
COMPREPLY=($(compgen -f "${cur}"))
return 0
Expand Down
4 changes: 2 additions & 2 deletions completions/just.elvish
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ set edit:completion:arg-completer[just] = {|@words|
cand -c 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set'
cand --command 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set'
cand --completions 'Print shell completion script for <SHELL>'
cand -l 'List available recipes and their arguments'
cand --list 'List available recipes and their arguments'
cand -s 'Show information about <RECIPE>'
cand --show 'Show information about <RECIPE>'
cand --dotenv-filename 'Search for environment file named <DOTENV-FILENAME> instead of `.env`'
Expand Down Expand Up @@ -64,8 +66,6 @@ set edit:completion:arg-completer[just] = {|@words|
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'
cand --groups 'List recipe groups'
cand --man 'Print man page'
cand --summary 'List names of available recipes'
Expand Down
2 changes: 1 addition & 1 deletion completions/just.fish
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ complete -c just -l shell-arg -d 'Invoke shell with <SHELL-ARG> as an argument'
complete -c just -s d -l working-directory -d 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set' -r -F
complete -c just -s c -l command -d 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set' -r
complete -c just -l completions -d 'Print shell completion script for <SHELL>' -r -f -a "{bash '',elvish '',fish '',powershell '',zsh ''}"
complete -c just -s l -l list -d 'List available recipes and their arguments' -r
complete -c just -s s -l show -d 'Show information about <RECIPE>' -r
complete -c just -l dotenv-filename -d 'Search for environment file named <DOTENV-FILENAME> instead of `.env`' -r
complete -c just -s E -l dotenv-path -d 'Load <DOTENV-PATH> as environment file instead of searching for one' -r -F
Expand All @@ -72,7 +73,6 @@ complete -c just -s e -l edit -d 'Edit justfile with editor given by $VISUAL or
complete -c just -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 -l fmt -d 'Format and overwrite justfile'
complete -c just -l init -d 'Initialize new justfile in project root'
complete -c just -s l -l list -d 'List available recipes and their arguments'
complete -c just -l groups -d 'List recipe groups'
complete -c just -l man -d 'Print man page'
complete -c just -l summary -d 'List names of available recipes'
Expand Down
4 changes: 2 additions & 2 deletions completions/just.powershell
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
[CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set')
[CompletionResult]::new('--command', 'command', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set')
[CompletionResult]::new('--completions', 'completions', [CompletionResultType]::ParameterName, 'Print shell completion script for <SHELL>')
[CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List available recipes and their arguments')
[CompletionResult]::new('--list', 'list', [CompletionResultType]::ParameterName, 'List available recipes and their arguments')
[CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Show information about <RECIPE>')
[CompletionResult]::new('--show', 'show', [CompletionResultType]::ParameterName, 'Show information about <RECIPE>')
[CompletionResult]::new('--dotenv-filename', 'dotenv-filename', [CompletionResultType]::ParameterName, 'Search for environment file named <DOTENV-FILENAME> instead of `.env`')
Expand Down Expand Up @@ -67,8 +69,6 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
[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')
[CompletionResult]::new('--groups', 'groups', [CompletionResultType]::ParameterName, 'List recipe groups')
[CompletionResult]::new('--man', 'man', [CompletionResultType]::ParameterName, 'Print man page')
[CompletionResult]::new('--summary', 'summary', [CompletionResultType]::ParameterName, 'List names of available recipes')
Expand Down
4 changes: 2 additions & 2 deletions completions/just.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ _just() {
'*-c+[Run an arbitrary command with the working directory, \`.env\`, overrides, and exports set]: : ' \
'*--command=[Run an arbitrary command with the working directory, \`.env\`, overrides, and exports set]: : ' \
'*--completions=[Print shell completion script for <SHELL>]:SHELL:(bash elvish fish powershell zsh)' \
'-l+[List available recipes and their arguments]' \
'--list=[List available recipes and their arguments]' \
'-s+[Show information about <RECIPE>]: :(_just_commands)' \
'--show=[Show information about <RECIPE>]: :(_just_commands)' \
'(-E --dotenv-path)--dotenv-filename=[Search for environment file named <DOTENV-FILENAME> instead of \`.env\`]: : ' \
Expand Down Expand Up @@ -62,8 +64,6 @@ _just() {
'--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]' \
'--groups[List recipe groups]' \
'--man[Print man page]' \
'--summary[List names of available recipes]' \
Expand Down
43 changes: 25 additions & 18 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@ mod cmd {
VARIABLES,
];

pub(crate) const ARGLESS: &[&str] = &[
CHANGELOG, DUMP, EDIT, FORMAT, INIT, LIST, MAN, SUMMARY, VARIABLES,
];
pub(crate) const ARGLESS: &[&str] =
&[CHANGELOG, DUMP, EDIT, FORMAT, INIT, MAN, SUMMARY, VARIABLES];
}

mod arg {
Expand Down Expand Up @@ -417,7 +416,9 @@ impl Config {
Arg::new(cmd::LIST)
.short('l')
.long("list")
.action(ArgAction::SetTrue)
.num_args(0..)
.value_name("PATH")
.action(ArgAction::Set)
.help("List available recipes and their arguments"),
)
.arg(
Expand Down Expand Up @@ -663,8 +664,18 @@ impl Config {
Subcommand::Format
} else if matches.get_flag(cmd::INIT) {
Subcommand::Init
} else if matches.get_flag(cmd::LIST) {
Subcommand::List
} else if let Some(path) = matches.get_many::<String>(cmd::LIST) {
Subcommand::List {
path: path
.clone()
.map(|s| (*s).as_str())
.collect::<Vec<&str>>()
.as_slice()
.try_into()
.map_err(|()| ConfigError::ListPath {
path: path.cloned().collect(),
})?,
}
} else if matches.get_flag(cmd::GROUPS) {
Subcommand::Groups
} else if matches.get_flag(cmd::MAN) {
Expand Down Expand Up @@ -1273,13 +1284,19 @@ mod tests {
test! {
name: subcommand_list_long,
args: ["--list"],
subcommand: Subcommand::List,
subcommand: Subcommand::List{ path: ModulePath{ path: Vec::new(), spaced: false } },
}

test! {
name: subcommand_list_short,
args: ["-l"],
subcommand: Subcommand::List,
subcommand: Subcommand::List{ path: ModulePath{ path: Vec::new(), spaced: false } },
}

test! {
name: subcommand_list_arguments,
args: ["--list", "bar"],
subcommand: Subcommand::List{ path: ModulePath{ path: vec!["bar".into()], spaced: false } },
}

test! {
Expand Down Expand Up @@ -1511,16 +1528,6 @@ mod tests {
},
}

error! {
name: list_arguments,
args: ["--list", "bar"],
error: ConfigError::SubcommandArguments { subcommand, arguments },
check: {
assert_eq!(subcommand, cmd::LIST);
assert_eq!(arguments, &["bar"]);
},
}

error! {
name: dump_arguments,
args: ["--dump", "bar"],
Expand Down
5 changes: 3 additions & 2 deletions src/config_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ pub(crate) enum ConfigError {
#[snafu(display("Failed to get current directory: {}", source))]
CurrentDir { source: io::Error },
#[snafu(display(
"Internal config error, this may indicate a bug in just: {} \
"Internal config error, this may indicate a bug in just: {message} \
consider filing an issue: https://github.com/casey/just/issues/new",
message
))]
Internal { message: String },
#[snafu(display("Invalid module path `{}`", path.join(" ")))]
ListPath { path: Vec<String> },
#[snafu(display(
"Path-prefixed recipes may not be used with `--working-directory` or `--justfile`."
))]
Expand Down
6 changes: 6 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ pub(crate) enum Error<'src> {
recipe: &'src str,
line_number: Option<usize>,
},
UnknownSubmodule {
path: ModulePath,
},
UnknownOverrides {
overrides: Vec<String>,
},
Expand Down Expand Up @@ -432,6 +435,9 @@ impl<'src> ColorDisplay for Error<'src> {
write!(f, "Recipe `{recipe}` failed for an unknown reason")?;
}
}
UnknownSubmodule { path } => {
write!(f, "Justfile does not contain submodule `{path}`")?;
}
UnknownOverrides { overrides } => {
let count = Count("Variable", overrides.len());
let overrides = List::and_ticked(overrides);
Expand Down
4 changes: 2 additions & 2 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,12 +273,12 @@ impl<'src> Lexer<'src> {
}

/// True if `c` can be the first character of an identifier
fn is_identifier_start(c: char) -> bool {
pub(crate) fn is_identifier_start(c: char) -> bool {
matches!(c, 'a'..='z' | 'A'..='Z' | '_')
}

/// True if `c` can be a continuation character of an identifier
fn is_identifier_continue(c: char) -> bool {
pub(crate) fn is_identifier_continue(c: char) -> bool {
Self::is_identifier_start(c) || matches!(c, '0'..='9' | '-')
}

Expand Down
18 changes: 10 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ pub(crate) use {
evaluator::Evaluator, expression::Expression, fragment::Fragment, function::Function,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List,
load_dotenv::load_dotenv, loader::Loader, name::Name, namepath::Namepath, ordinal::Ordinal,
output::output, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
parser::Parser, platform::Platform, platform_interface::PlatformInterface, position::Position,
positional::Positional, ran::Ran, range_ext::RangeExt, recipe::Recipe,
recipe_context::RecipeContext, recipe_resolver::RecipeResolver,
recipe_signature::RecipeSignature, scope::Scope, search::Search, search_config::SearchConfig,
search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang,
shell::Shell, show_whitespace::ShowWhitespace, source::Source, string_kind::StringKind,
load_dotenv::load_dotenv, loader::Loader, module_path::ModulePath, name::Name,
namepath::Namepath, ordinal::Ordinal, output::output, output_error::OutputError,
parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform,
platform_interface::PlatformInterface, position::Position, positional::Positional, ran::Ran,
range_ext::RangeExt, recipe::Recipe, recipe_context::RecipeContext,
recipe_resolver::RecipeResolver, recipe_signature::RecipeSignature, scope::Scope,
search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
setting::Setting, settings::Settings, shebang::Shebang, shell::Shell,
show_whitespace::ShowWhitespace, source::Source, 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,
Expand Down Expand Up @@ -153,6 +154,7 @@ mod line;
mod list;
mod load_dotenv;
mod loader;
mod module_path;
mod name;
mod namepath;
mod ordinal;
Expand Down
100 changes: 100 additions & 0 deletions src/module_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use super::*;

#[derive(Debug, PartialEq, Clone)]
pub(crate) struct ModulePath {
pub(crate) path: Vec<String>,
pub(crate) spaced: bool,
}

impl TryFrom<&[&str]> for ModulePath {
type Error = ();

fn try_from(path: &[&str]) -> Result<Self, Self::Error> {
let spaced = path.len() > 1;

let path = if path.len() == 1 {
let first = path[0];

if first.starts_with(':') || first.ends_with(':') || first.contains(":::") {
return Err(());
}

first
.split("::")
.map(str::to_string)
.collect::<Vec<String>>()
} else {
path.iter().map(|s| (*s).to_string()).collect()
};

for name in &path {
if name.is_empty() {
return Err(());
}

for (i, c) in name.chars().enumerate() {
if i == 0 {
if !Lexer::is_identifier_start(c) {
return Err(());
}
} else if !Lexer::is_identifier_continue(c) {
return Err(());
}
}
}

Ok(Self { path, spaced })
}
}

impl Display for ModulePath {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
for (i, name) in self.path.iter().enumerate() {
if i > 0 {
if self.spaced {
write!(f, " ")?;
} else {
write!(f, "::")?;
}
}
write!(f, "{name}")?;
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn try_from_ok() {
#[track_caller]
fn case(path: &[&str], expected: &[&str], display: &str) {
let actual = ModulePath::try_from(path).unwrap();
assert_eq!(actual.path, expected);
assert_eq!(actual.to_string(), display);
}

case(&[], &[], "");
case(&["foo"], &["foo"], "foo");
case(&["foo0"], &["foo0"], "foo0");
case(&["foo", "bar"], &["foo", "bar"], "foo bar");
case(&["foo::bar"], &["foo", "bar"], "foo::bar");
}

#[test]
fn try_from_err() {
#[track_caller]
fn case(path: &[&str]) {
assert!(ModulePath::try_from(path).is_err());
}

case(&[":foo"]);
case(&["foo:"]);
case(&["foo:::bar"]);
case(&["0foo"]);
case(&["f$oo"]);
case(&[""]);
}
}
Loading