Move subcommand functions into Subcommand (#918)
This commit is contained in:
parent
4ada364ede
commit
ce0376cfdf
@ -31,7 +31,7 @@ pub(crate) use typed_arena::Arena;
|
|||||||
pub(crate) use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
pub(crate) use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
|
|
||||||
// modules
|
// modules
|
||||||
pub(crate) use crate::{config_error, setting};
|
pub(crate) use crate::{completions, config, config_error, setting};
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
pub(crate) use crate::{load_dotenv::load_dotenv, output::output, unindent::unindent};
|
pub(crate) use crate::{load_dotenv::load_dotenv, output::output, unindent::unindent};
|
||||||
|
174
src/completions.rs
Normal file
174
src/completions.rs
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
pub(crate) const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes
|
||||||
|
just --summary 2> /dev/null | tr " " "\n" || echo ""
|
||||||
|
end
|
||||||
|
|
||||||
|
# don't suggest files right off
|
||||||
|
complete -c just -n "__fish_is_first_arg" --no-files
|
||||||
|
|
||||||
|
# complete recipes
|
||||||
|
complete -c just -a '(__fish_just_complete_recipes)'
|
||||||
|
|
||||||
|
# autogenerated completions
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub(crate) const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
|
||||||
|
(
|
||||||
|
r#" _arguments "${_arguments_options[@]}" \"#,
|
||||||
|
r#" local common=("#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r#"'*--set=[Override <VARIABLE> with <VALUE>]' \"#,
|
||||||
|
r#"'*--set[Override <VARIABLE> with <VALUE>]: :_just_variables' \"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r#"'-s+[Show information about <RECIPE>]' \
|
||||||
|
'--show=[Show information about <RECIPE>]' \"#,
|
||||||
|
r#"'-s+[Show information about <RECIPE>]: :_just_commands' \
|
||||||
|
'--show=[Show information about <RECIPE>]: :_just_commands' \"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"'::ARGUMENTS -- Overrides and recipe(s) to run, defaulting to the first recipe in the \
|
||||||
|
justfile:_files' \\
|
||||||
|
&& ret=0
|
||||||
|
\x20\x20\x20\x20
|
||||||
|
",
|
||||||
|
r#")
|
||||||
|
|
||||||
|
_arguments "${_arguments_options[@]}" $common \
|
||||||
|
'1: :_just_commands' \
|
||||||
|
'*: :->args' \
|
||||||
|
&& ret=0
|
||||||
|
|
||||||
|
case $state in
|
||||||
|
args)
|
||||||
|
curcontext="${curcontext%:*}-${words[2]}:"
|
||||||
|
|
||||||
|
local lastarg=${words[${#words}]}
|
||||||
|
local recipe
|
||||||
|
|
||||||
|
local cmds; cmds=(
|
||||||
|
${(s: :)$(_call_program commands just --summary)}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find first recipe name
|
||||||
|
for ((i = 2; i < $#words; i++ )) do
|
||||||
|
if [[ ${cmds[(I)${words[i]}]} -gt 0 ]]; then
|
||||||
|
recipe=${words[i]}
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $lastarg = */* ]]; then
|
||||||
|
# Arguments contain slash would be recognised as a file
|
||||||
|
_arguments -s -S $common '*:: :_files'
|
||||||
|
elif [[ $lastarg = *=* ]]; then
|
||||||
|
# Arguments contain equal would be recognised as a variable
|
||||||
|
_message "value"
|
||||||
|
elif [[ $recipe ]]; then
|
||||||
|
# Show usage message
|
||||||
|
_message "`just --show $recipe`"
|
||||||
|
# Or complete with other commands
|
||||||
|
#_arguments -s -S $common '*:: :_just_commands'
|
||||||
|
else
|
||||||
|
_arguments -s -S $common '*:: :_just_commands'
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return ret
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
" local commands; commands=(
|
||||||
|
\x20\x20\x20\x20\x20\x20\x20\x20
|
||||||
|
)",
|
||||||
|
r#" [[ $PREFIX = -* ]] && return 1
|
||||||
|
integer ret=1
|
||||||
|
local variables; variables=(
|
||||||
|
${(s: :)$(_call_program commands just --variables)}
|
||||||
|
)
|
||||||
|
local commands; commands=(
|
||||||
|
${${${(M)"${(f)$(_call_program commands just --list)}":# *}/ ##/}/ ##/:Args: }
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r#" _describe -t commands 'just commands' commands "$@""#,
|
||||||
|
r#" if compset -P '*='; then
|
||||||
|
case "${${words[-1]%=*}#*=}" in
|
||||||
|
*) _message 'value' && ret=0 ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
_describe -t variables 'variables' variables -qS "=" && ret=0
|
||||||
|
_describe -t commands 'just commands' commands "$@"
|
||||||
|
fi
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r#"_just "$@""#,
|
||||||
|
r#"(( $+functions[_just_variables] )) ||
|
||||||
|
_just_variables() {
|
||||||
|
[[ $PREFIX = -* ]] && return 1
|
||||||
|
integer ret=1
|
||||||
|
local variables; variables=(
|
||||||
|
${(s: :)$(_call_program commands just --variables)}
|
||||||
|
)
|
||||||
|
|
||||||
|
if compset -P '*='; then
|
||||||
|
case "${${words[-1]%=*}#*=}" in
|
||||||
|
*) _message 'value' && ret=0 ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
_describe -t variables 'variables' variables && ret=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
_just "$@""#,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(crate) const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
|
||||||
|
r#"$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
|
||||||
|
Sort-Object -Property ListItemText"#,
|
||||||
|
r#"function Get-JustFileRecipes([string[]]$CommandElements) {
|
||||||
|
$justFileIndex = $commandElements.IndexOf("--justfile");
|
||||||
|
|
||||||
|
if ($justFileIndex -ne -1 && $justFileIndex + 1 -le $commandElements.Length) {
|
||||||
|
$justFileLocation = $commandElements[$justFileIndex + 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$justArgs = @("--summary")
|
||||||
|
|
||||||
|
if (Test-Path $justFileLocation) {
|
||||||
|
$justArgs += @("--justfile", $justFileLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
$recipes = $(just @justArgs) -split ' '
|
||||||
|
return $recipes | ForEach-Object { [CompletionResult]::new($_) }
|
||||||
|
}
|
||||||
|
|
||||||
|
$elementValues = $commandElements | Select-Object -ExpandProperty Value
|
||||||
|
$recipes = Get-JustFileRecipes -CommandElements $elementValues
|
||||||
|
$completions += $recipes
|
||||||
|
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
|
||||||
|
Sort-Object -Property ListItemText"#,
|
||||||
|
)];
|
||||||
|
|
||||||
|
pub(crate) const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
|
||||||
|
r#" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
fi"#,
|
||||||
|
r#" if [[ ${cur} == -* ]] ; then
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
elif [[ ${COMP_CWORD} -eq 1 ]]; then
|
||||||
|
local recipes=$(just --summary --color never 2> /dev/null)
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi"#,
|
||||||
|
)];
|
343
src/config.rs
343
src/config.rs
@ -11,7 +11,6 @@ pub(crate) const CHOOSE_HELP: &str = "Select one or more recipes to run using a
|
|||||||
|
|
||||||
pub(crate) const DEFAULT_SHELL: &str = "sh";
|
pub(crate) const DEFAULT_SHELL: &str = "sh";
|
||||||
pub(crate) const DEFAULT_SHELL_ARG: &str = "-cu";
|
pub(crate) const DEFAULT_SHELL_ARG: &str = "-cu";
|
||||||
pub(crate) const INIT_JUSTFILE: &str = "default:\n\techo 'Hello, world!'\n";
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub(crate) struct Config {
|
pub(crate) struct Config {
|
||||||
@ -532,163 +531,7 @@ impl Config {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run_subcommand<'src>(self, loader: &'src Loader) -> Result<(), Error<'src>> {
|
pub(crate) fn require_unstable(&self, message: &str) -> Result<(), Error<'static>> {
|
||||||
use Subcommand::*;
|
|
||||||
|
|
||||||
if self.subcommand == Init {
|
|
||||||
return self.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Completions { shell } = self.subcommand {
|
|
||||||
return Subcommand::completions(&shell);
|
|
||||||
}
|
|
||||||
|
|
||||||
let search = Search::find(&self.search_config, &self.invocation_directory)?;
|
|
||||||
|
|
||||||
if self.subcommand == Edit {
|
|
||||||
return Self::edit(&search);
|
|
||||||
}
|
|
||||||
|
|
||||||
let src = loader.load(&search.justfile)?;
|
|
||||||
|
|
||||||
let tokens = Lexer::lex(&src)?;
|
|
||||||
let ast = Parser::parse(&tokens)?;
|
|
||||||
let justfile = Analyzer::analyze(ast.clone())?;
|
|
||||||
|
|
||||||
if self.verbosity.loud() {
|
|
||||||
for warning in &justfile.warnings {
|
|
||||||
warning.write(&mut io::stderr(), self.color.stderr()).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match &self.subcommand {
|
|
||||||
Choose { overrides, chooser } =>
|
|
||||||
self.choose(justfile, &search, overrides, chooser.as_deref())?,
|
|
||||||
Command { overrides, .. } => self.run(justfile, &search, overrides, &[])?,
|
|
||||||
Dump => Self::dump(ast),
|
|
||||||
Evaluate { overrides, .. } => self.run(justfile, &search, overrides, &[])?,
|
|
||||||
Format => self.format(ast, &search)?,
|
|
||||||
List => self.list(justfile),
|
|
||||||
Run {
|
|
||||||
arguments,
|
|
||||||
overrides,
|
|
||||||
} => self.run(justfile, &search, overrides, arguments)?,
|
|
||||||
Show { ref name } => Self::show(&name, justfile)?,
|
|
||||||
Summary => self.summary(justfile),
|
|
||||||
Variables => Self::variables(justfile),
|
|
||||||
Completions { .. } | Edit | Init => unreachable!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn choose<'src>(
|
|
||||||
&self,
|
|
||||||
justfile: Justfile<'src>,
|
|
||||||
search: &Search,
|
|
||||||
overrides: &BTreeMap<String, String>,
|
|
||||||
chooser: Option<&str>,
|
|
||||||
) -> Result<(), Error<'src>> {
|
|
||||||
let recipes = justfile
|
|
||||||
.public_recipes(self.unsorted)
|
|
||||||
.iter()
|
|
||||||
.filter(|recipe| recipe.min_arguments() == 0)
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<&Recipe<Dependency>>>();
|
|
||||||
|
|
||||||
if recipes.is_empty() {
|
|
||||||
return Err(Error::NoChoosableRecipes);
|
|
||||||
}
|
|
||||||
|
|
||||||
let chooser = chooser
|
|
||||||
.map(OsString::from)
|
|
||||||
.or_else(|| env::var_os(CHOOSER_ENVIRONMENT_KEY))
|
|
||||||
.unwrap_or_else(|| OsString::from(CHOOSER_DEFAULT));
|
|
||||||
|
|
||||||
let result = justfile
|
|
||||||
.settings
|
|
||||||
.shell_command(self)
|
|
||||||
.arg(&chooser)
|
|
||||||
.current_dir(&search.working_directory)
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.spawn();
|
|
||||||
|
|
||||||
let mut child = match result {
|
|
||||||
Ok(child) => child,
|
|
||||||
Err(io_error) => {
|
|
||||||
return Err(Error::ChooserInvoke {
|
|
||||||
shell_binary: justfile.settings.shell_binary(self).to_owned(),
|
|
||||||
shell_arguments: justfile.settings.shell_arguments(self).join(" "),
|
|
||||||
chooser,
|
|
||||||
io_error,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for recipe in recipes {
|
|
||||||
if let Err(io_error) = child
|
|
||||||
.stdin
|
|
||||||
.as_mut()
|
|
||||||
.expect("Child was created with piped stdio")
|
|
||||||
.write_all(format!("{}\n", recipe.name).as_bytes())
|
|
||||||
{
|
|
||||||
return Err(Error::ChooserWrite { io_error, chooser });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = match child.wait_with_output() {
|
|
||||||
Ok(output) => output,
|
|
||||||
Err(io_error) => {
|
|
||||||
return Err(Error::ChooserRead { io_error, chooser });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
return Err(Error::ChooserStatus {
|
|
||||||
status: output.status,
|
|
||||||
chooser,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
|
|
||||||
let recipes = stdout
|
|
||||||
.trim()
|
|
||||||
.split_whitespace()
|
|
||||||
.map(str::to_owned)
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
|
|
||||||
self.run(justfile, search, overrides, &recipes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dump(ast: Ast) {
|
|
||||||
print!("{}", ast);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn edit(search: &Search) -> Result<(), Error<'static>> {
|
|
||||||
let editor = env::var_os("VISUAL")
|
|
||||||
.or_else(|| env::var_os("EDITOR"))
|
|
||||||
.unwrap_or_else(|| "vim".into());
|
|
||||||
|
|
||||||
let error = Command::new(&editor)
|
|
||||||
.current_dir(&search.working_directory)
|
|
||||||
.arg(&search.justfile)
|
|
||||||
.status();
|
|
||||||
|
|
||||||
let status = match error {
|
|
||||||
Err(io_error) => return Err(Error::EditorInvoke { editor, io_error }),
|
|
||||||
Ok(status) => status,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !status.success() {
|
|
||||||
return Err(Error::EditorStatus { editor, status });
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn require_unstable(&self, message: &str) -> Result<(), Error<'static>> {
|
|
||||||
if self.unstable {
|
if self.unstable {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
@ -698,183 +541,8 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format(&self, ast: Ast, search: &Search) -> Result<(), Error<'static>> {
|
pub(crate) fn run<'src>(self, loader: &'src Loader) -> Result<(), Error<'src>> {
|
||||||
self.require_unstable("The `--fmt` command is currently unstable.")?;
|
self.subcommand.run(&self, loader)
|
||||||
|
|
||||||
if let Err(io_error) =
|
|
||||||
File::create(&search.justfile).and_then(|mut file| write!(file, "{}", ast))
|
|
||||||
{
|
|
||||||
Err(Error::WriteJustfile {
|
|
||||||
justfile: search.justfile.clone(),
|
|
||||||
io_error,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if self.verbosity.loud() {
|
|
||||||
eprintln!("Wrote justfile to `{}`", search.justfile.display());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn init(&self) -> Result<(), Error<'static>> {
|
|
||||||
let search = Search::init(&self.search_config, &self.invocation_directory)?;
|
|
||||||
|
|
||||||
if search.justfile.is_file() {
|
|
||||||
Err(Error::InitExists {
|
|
||||||
justfile: search.justfile,
|
|
||||||
})
|
|
||||||
} else if let Err(io_error) = fs::write(&search.justfile, INIT_JUSTFILE) {
|
|
||||||
Err(Error::WriteJustfile {
|
|
||||||
justfile: search.justfile,
|
|
||||||
io_error,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if self.verbosity.loud() {
|
|
||||||
eprintln!("Wrote justfile to `{}`", search.justfile.display());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list(&self, justfile: Justfile) {
|
|
||||||
// Construct a target to alias map.
|
|
||||||
let mut recipe_aliases: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
|
|
||||||
for alias in justfile.aliases.values() {
|
|
||||||
if alias.is_private() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !recipe_aliases.contains_key(alias.target.name.lexeme()) {
|
|
||||||
recipe_aliases.insert(alias.target.name.lexeme(), vec![alias.name.lexeme()]);
|
|
||||||
} else {
|
|
||||||
let aliases = recipe_aliases.get_mut(alias.target.name.lexeme()).unwrap();
|
|
||||||
aliases.push(alias.name.lexeme());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut line_widths: BTreeMap<&str, usize> = BTreeMap::new();
|
|
||||||
|
|
||||||
for (name, recipe) in &justfile.recipes {
|
|
||||||
if recipe.private {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for name in iter::once(name).chain(recipe_aliases.get(name).unwrap_or(&Vec::new())) {
|
|
||||||
let mut line_width = UnicodeWidthStr::width(*name);
|
|
||||||
|
|
||||||
for parameter in &recipe.parameters {
|
|
||||||
line_width += UnicodeWidthStr::width(format!(" {}", parameter).as_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
if line_width <= 30 {
|
|
||||||
line_widths.insert(name, line_width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let max_line_width = cmp::min(line_widths.values().cloned().max().unwrap_or(0), 30);
|
|
||||||
|
|
||||||
let doc_color = self.color.stdout().doc();
|
|
||||||
print!("{}", self.list_heading);
|
|
||||||
|
|
||||||
for recipe in justfile.public_recipes(self.unsorted) {
|
|
||||||
let name = recipe.name();
|
|
||||||
|
|
||||||
for (i, name) in iter::once(&name)
|
|
||||||
.chain(recipe_aliases.get(name).unwrap_or(&Vec::new()))
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
print!("{}{}", self.list_prefix, name);
|
|
||||||
for parameter in &recipe.parameters {
|
|
||||||
if self.color.stdout().active() {
|
|
||||||
print!(" {:#}", parameter);
|
|
||||||
} else {
|
|
||||||
print!(" {}", parameter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Declaring this outside of the nested loops will probably be more efficient,
|
|
||||||
// but it creates all sorts of lifetime issues with variables inside the loops.
|
|
||||||
// If this is inlined like the docs say, it shouldn't make any difference.
|
|
||||||
let print_doc = |doc| {
|
|
||||||
print!(
|
|
||||||
" {:padding$}{} {}",
|
|
||||||
"",
|
|
||||||
doc_color.paint("#"),
|
|
||||||
doc_color.paint(doc),
|
|
||||||
padding = max_line_width
|
|
||||||
.saturating_sub(line_widths.get(name).cloned().unwrap_or(max_line_width))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
match (i, recipe.doc) {
|
|
||||||
(0, Some(doc)) => print_doc(doc),
|
|
||||||
(0, None) => (),
|
|
||||||
_ => {
|
|
||||||
let alias_doc = format!("alias for `{}`", recipe.name);
|
|
||||||
print_doc(&alias_doc);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run<'src>(
|
|
||||||
&self,
|
|
||||||
justfile: Justfile<'src>,
|
|
||||||
search: &Search,
|
|
||||||
overrides: &BTreeMap<String, String>,
|
|
||||||
arguments: &[String],
|
|
||||||
) -> Result<(), Error<'src>> {
|
|
||||||
if let Err(error) = InterruptHandler::install(self.verbosity) {
|
|
||||||
warn!("Failed to set CTRL-C handler: {}", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
justfile.run(&self, search, overrides, arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show<'src>(name: &str, justfile: Justfile<'src>) -> Result<(), Error<'src>> {
|
|
||||||
if let Some(alias) = justfile.get_alias(name) {
|
|
||||||
let recipe = justfile.get_recipe(alias.target.name.lexeme()).unwrap();
|
|
||||||
println!("{}", alias);
|
|
||||||
println!("{}", recipe);
|
|
||||||
Ok(())
|
|
||||||
} else if let Some(recipe) = justfile.get_recipe(name) {
|
|
||||||
println!("{}", recipe);
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Error::UnknownRecipes {
|
|
||||||
recipes: vec![name.to_owned()],
|
|
||||||
suggestion: justfile.suggest_recipe(name),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn summary(&self, justfile: Justfile) {
|
|
||||||
if justfile.count() == 0 {
|
|
||||||
if self.verbosity.loud() {
|
|
||||||
eprintln!("Justfile contains no recipes.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let summary = justfile
|
|
||||||
.public_recipes(self.unsorted)
|
|
||||||
.iter()
|
|
||||||
.map(|recipe| recipe.name())
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join(" ");
|
|
||||||
println!("{}", summary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn variables(justfile: Justfile) {
|
|
||||||
for (i, (_, assignment)) in justfile.assignments.iter().enumerate() {
|
|
||||||
if i > 0 {
|
|
||||||
print!(" ");
|
|
||||||
}
|
|
||||||
print!("{}", assignment.name);
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1716,9 +1384,4 @@ ARGS:
|
|||||||
assert_eq!(overrides, map!{"bar": "baz"});
|
assert_eq!(overrides, map!{"bar": "baz"});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn init_justfile() {
|
|
||||||
testing::compile(INIT_JUSTFILE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,10 @@ impl<'src> Justfile<'src> {
|
|||||||
overrides: &BTreeMap<String, String>,
|
overrides: &BTreeMap<String, String>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
) -> RunResult<'src, ()> {
|
) -> RunResult<'src, ()> {
|
||||||
|
if let Err(error) = InterruptHandler::install(config.verbosity) {
|
||||||
|
warn!("Failed to set CTRL-C handler: {}", error);
|
||||||
|
}
|
||||||
|
|
||||||
let unknown_overrides = overrides
|
let unknown_overrides = overrides
|
||||||
.keys()
|
.keys()
|
||||||
.filter(|name| !self.assignments.contains_key(name.as_str()))
|
.filter(|name| !self.assignments.contains_key(name.as_str()))
|
||||||
|
@ -70,6 +70,7 @@ mod common;
|
|||||||
mod compile_error;
|
mod compile_error;
|
||||||
mod compile_error_kind;
|
mod compile_error_kind;
|
||||||
mod compiler;
|
mod compiler;
|
||||||
|
mod completions;
|
||||||
mod config;
|
mod config;
|
||||||
mod config_error;
|
mod config_error;
|
||||||
mod count;
|
mod count;
|
||||||
|
@ -26,7 +26,7 @@ pub fn run() -> Result<(), i32> {
|
|||||||
.and_then(|config| {
|
.and_then(|config| {
|
||||||
color = config.color;
|
color = config.color;
|
||||||
verbosity = config.verbosity;
|
verbosity = config.verbosity;
|
||||||
config.run_subcommand(&loader)
|
config.run(&loader)
|
||||||
})
|
})
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
if !verbosity.quiet() {
|
if !verbosity.quiet() {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
|
const INIT_JUSTFILE: &str = "default:\n\techo 'Hello, world!'\n";
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
pub(crate) enum Subcommand {
|
pub(crate) enum Subcommand {
|
||||||
Choose {
|
Choose {
|
||||||
@ -34,183 +36,138 @@ pub(crate) enum Subcommand {
|
|||||||
Variables,
|
Variables,
|
||||||
}
|
}
|
||||||
|
|
||||||
const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes
|
impl Subcommand {
|
||||||
just --summary 2> /dev/null | tr " " "\n" || echo ""
|
pub(crate) fn run<'src>(&self, config: &Config, loader: &'src Loader) -> Result<(), Error<'src>> {
|
||||||
end
|
use Subcommand::*;
|
||||||
|
|
||||||
# don't suggest files right off
|
if let Init = self {
|
||||||
complete -c just -n "__fish_is_first_arg" --no-files
|
return Self::init(config);
|
||||||
|
|
||||||
# complete recipes
|
|
||||||
complete -c just -a '(__fish_just_complete_recipes)'
|
|
||||||
|
|
||||||
# autogenerated completions
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
|
|
||||||
(
|
|
||||||
r#" _arguments "${_arguments_options[@]}" \"#,
|
|
||||||
r#" local common=("#,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
r#"'*--set=[Override <VARIABLE> with <VALUE>]' \"#,
|
|
||||||
r#"'*--set[Override <VARIABLE> with <VALUE>]: :_just_variables' \"#,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
r#"'-s+[Show information about <RECIPE>]' \
|
|
||||||
'--show=[Show information about <RECIPE>]' \"#,
|
|
||||||
r#"'-s+[Show information about <RECIPE>]: :_just_commands' \
|
|
||||||
'--show=[Show information about <RECIPE>]: :_just_commands' \"#,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"'::ARGUMENTS -- Overrides and recipe(s) to run, defaulting to the first recipe in the \
|
|
||||||
justfile:_files' \\
|
|
||||||
&& ret=0
|
|
||||||
\x20\x20\x20\x20
|
|
||||||
",
|
|
||||||
r#")
|
|
||||||
|
|
||||||
_arguments "${_arguments_options[@]}" $common \
|
|
||||||
'1: :_just_commands' \
|
|
||||||
'*: :->args' \
|
|
||||||
&& ret=0
|
|
||||||
|
|
||||||
case $state in
|
|
||||||
args)
|
|
||||||
curcontext="${curcontext%:*}-${words[2]}:"
|
|
||||||
|
|
||||||
local lastarg=${words[${#words}]}
|
|
||||||
local recipe
|
|
||||||
|
|
||||||
local cmds; cmds=(
|
|
||||||
${(s: :)$(_call_program commands just --summary)}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Find first recipe name
|
|
||||||
for ((i = 2; i < $#words; i++ )) do
|
|
||||||
if [[ ${cmds[(I)${words[i]}]} -gt 0 ]]; then
|
|
||||||
recipe=${words[i]}
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ $lastarg = */* ]]; then
|
|
||||||
# Arguments contain slash would be recognised as a file
|
|
||||||
_arguments -s -S $common '*:: :_files'
|
|
||||||
elif [[ $lastarg = *=* ]]; then
|
|
||||||
# Arguments contain equal would be recognised as a variable
|
|
||||||
_message "value"
|
|
||||||
elif [[ $recipe ]]; then
|
|
||||||
# Show usage message
|
|
||||||
_message "`just --show $recipe`"
|
|
||||||
# Or complete with other commands
|
|
||||||
#_arguments -s -S $common '*:: :_just_commands'
|
|
||||||
else
|
|
||||||
_arguments -s -S $common '*:: :_just_commands'
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
return ret
|
|
||||||
"#,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
" local commands; commands=(
|
|
||||||
\x20\x20\x20\x20\x20\x20\x20\x20
|
|
||||||
)",
|
|
||||||
r#" [[ $PREFIX = -* ]] && return 1
|
|
||||||
integer ret=1
|
|
||||||
local variables; variables=(
|
|
||||||
${(s: :)$(_call_program commands just --variables)}
|
|
||||||
)
|
|
||||||
local commands; commands=(
|
|
||||||
${${${(M)"${(f)$(_call_program commands just --list)}":# *}/ ##/}/ ##/:Args: }
|
|
||||||
)
|
|
||||||
"#,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
r#" _describe -t commands 'just commands' commands "$@""#,
|
|
||||||
r#" if compset -P '*='; then
|
|
||||||
case "${${words[-1]%=*}#*=}" in
|
|
||||||
*) _message 'value' && ret=0 ;;
|
|
||||||
esac
|
|
||||||
else
|
|
||||||
_describe -t variables 'variables' variables -qS "=" && ret=0
|
|
||||||
_describe -t commands 'just commands' commands "$@"
|
|
||||||
fi
|
|
||||||
"#,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
r#"_just "$@""#,
|
|
||||||
r#"(( $+functions[_just_variables] )) ||
|
|
||||||
_just_variables() {
|
|
||||||
[[ $PREFIX = -* ]] && return 1
|
|
||||||
integer ret=1
|
|
||||||
local variables; variables=(
|
|
||||||
${(s: :)$(_call_program commands just --variables)}
|
|
||||||
)
|
|
||||||
|
|
||||||
if compset -P '*='; then
|
|
||||||
case "${${words[-1]%=*}#*=}" in
|
|
||||||
*) _message 'value' && ret=0 ;;
|
|
||||||
esac
|
|
||||||
else
|
|
||||||
_describe -t variables 'variables' variables && ret=0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
_just "$@""#,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
|
|
||||||
r#"$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
|
|
||||||
Sort-Object -Property ListItemText"#,
|
|
||||||
r#"function Get-JustFileRecipes([string[]]$CommandElements) {
|
|
||||||
$justFileIndex = $commandElements.IndexOf("--justfile");
|
|
||||||
|
|
||||||
if ($justFileIndex -ne -1 && $justFileIndex + 1 -le $commandElements.Length) {
|
|
||||||
$justFileLocation = $commandElements[$justFileIndex + 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
$justArgs = @("--summary")
|
|
||||||
|
|
||||||
if (Test-Path $justFileLocation) {
|
|
||||||
$justArgs += @("--justfile", $justFileLocation)
|
|
||||||
}
|
|
||||||
|
|
||||||
$recipes = $(just @justArgs) -split ' '
|
|
||||||
return $recipes | ForEach-Object { [CompletionResult]::new($_) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$elementValues = $commandElements | Select-Object -ExpandProperty Value
|
if let Completions { shell } = self {
|
||||||
$recipes = Get-JustFileRecipes -CommandElements $elementValues
|
return Self::completions(&shell);
|
||||||
$completions += $recipes
|
}
|
||||||
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
|
|
||||||
Sort-Object -Property ListItemText"#,
|
|
||||||
)];
|
|
||||||
|
|
||||||
const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
|
let search = Search::find(&config.search_config, &config.invocation_directory)?;
|
||||||
r#" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
|
||||||
return 0
|
|
||||||
fi"#,
|
|
||||||
r#" if [[ ${cur} == -* ]] ; then
|
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
|
||||||
return 0
|
|
||||||
elif [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
||||||
local recipes=$(just --summary --color never 2> /dev/null)
|
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi"#,
|
|
||||||
)];
|
|
||||||
|
|
||||||
impl Subcommand {
|
if let Edit = self {
|
||||||
pub(crate) fn completions(shell: &str) -> RunResult<'static, ()> {
|
return Self::edit(&search);
|
||||||
|
}
|
||||||
|
|
||||||
|
let src = loader.load(&search.justfile)?;
|
||||||
|
|
||||||
|
let tokens = Lexer::lex(&src)?;
|
||||||
|
let ast = Parser::parse(&tokens)?;
|
||||||
|
let justfile = Analyzer::analyze(ast.clone())?;
|
||||||
|
|
||||||
|
if config.verbosity.loud() {
|
||||||
|
for warning in &justfile.warnings {
|
||||||
|
warning.write(&mut io::stderr(), config.color.stderr()).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Choose { overrides, chooser } =>
|
||||||
|
Self::choose(&config, justfile, &search, overrides, chooser.as_deref())?,
|
||||||
|
Command { overrides, .. } => justfile.run(&config, &search, overrides, &[])?,
|
||||||
|
Dump => Self::dump(ast),
|
||||||
|
Evaluate { overrides, .. } => justfile.run(&config, &search, overrides, &[])?,
|
||||||
|
Format => Self::format(&config, ast, &search)?,
|
||||||
|
List => Self::list(&config, justfile),
|
||||||
|
Run {
|
||||||
|
arguments,
|
||||||
|
overrides,
|
||||||
|
} => justfile.run(&config, &search, overrides, arguments)?,
|
||||||
|
Show { ref name } => Self::show(&name, justfile)?,
|
||||||
|
Summary => Self::summary(&config, justfile),
|
||||||
|
Variables => Self::variables(justfile),
|
||||||
|
Completions { .. } | Edit | Init => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn choose<'src>(
|
||||||
|
config: &Config,
|
||||||
|
justfile: Justfile<'src>,
|
||||||
|
search: &Search,
|
||||||
|
overrides: &BTreeMap<String, String>,
|
||||||
|
chooser: Option<&str>,
|
||||||
|
) -> Result<(), Error<'src>> {
|
||||||
|
let recipes = justfile
|
||||||
|
.public_recipes(config.unsorted)
|
||||||
|
.iter()
|
||||||
|
.filter(|recipe| recipe.min_arguments() == 0)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<&Recipe<Dependency>>>();
|
||||||
|
|
||||||
|
if recipes.is_empty() {
|
||||||
|
return Err(Error::NoChoosableRecipes);
|
||||||
|
}
|
||||||
|
|
||||||
|
let chooser = chooser
|
||||||
|
.map(OsString::from)
|
||||||
|
.or_else(|| env::var_os(config::CHOOSER_ENVIRONMENT_KEY))
|
||||||
|
.unwrap_or_else(|| OsString::from(config::CHOOSER_DEFAULT));
|
||||||
|
|
||||||
|
let result = justfile
|
||||||
|
.settings
|
||||||
|
.shell_command(&config)
|
||||||
|
.arg(&chooser)
|
||||||
|
.current_dir(&search.working_directory)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn();
|
||||||
|
|
||||||
|
let mut child = match result {
|
||||||
|
Ok(child) => child,
|
||||||
|
Err(io_error) => {
|
||||||
|
return Err(Error::ChooserInvoke {
|
||||||
|
shell_binary: justfile.settings.shell_binary(&config).to_owned(),
|
||||||
|
shell_arguments: justfile.settings.shell_arguments(&config).join(" "),
|
||||||
|
chooser,
|
||||||
|
io_error,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for recipe in recipes {
|
||||||
|
if let Err(io_error) = child
|
||||||
|
.stdin
|
||||||
|
.as_mut()
|
||||||
|
.expect("Child was created with piped stdio")
|
||||||
|
.write_all(format!("{}\n", recipe.name).as_bytes())
|
||||||
|
{
|
||||||
|
return Err(Error::ChooserWrite { io_error, chooser });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = match child.wait_with_output() {
|
||||||
|
Ok(output) => output,
|
||||||
|
Err(io_error) => {
|
||||||
|
return Err(Error::ChooserRead { io_error, chooser });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(Error::ChooserStatus {
|
||||||
|
status: output.status,
|
||||||
|
chooser,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
|
let recipes = stdout
|
||||||
|
.trim()
|
||||||
|
.split_whitespace()
|
||||||
|
.map(str::to_owned)
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
justfile.run(config, search, overrides, &recipes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn completions(shell: &str) -> RunResult<'static, ()> {
|
||||||
use clap::Shell;
|
use clap::Shell;
|
||||||
|
|
||||||
fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> {
|
fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> {
|
||||||
@ -237,19 +194,19 @@ impl Subcommand {
|
|||||||
|
|
||||||
match shell {
|
match shell {
|
||||||
Shell::Bash =>
|
Shell::Bash =>
|
||||||
for (needle, replacement) in BASH_COMPLETION_REPLACEMENTS {
|
for (needle, replacement) in completions::BASH_COMPLETION_REPLACEMENTS {
|
||||||
replace(&mut script, needle, replacement)?;
|
replace(&mut script, needle, replacement)?;
|
||||||
},
|
},
|
||||||
Shell::Fish => {
|
Shell::Fish => {
|
||||||
script.insert_str(0, FISH_RECIPE_COMPLETIONS);
|
script.insert_str(0, completions::FISH_RECIPE_COMPLETIONS);
|
||||||
},
|
},
|
||||||
Shell::PowerShell =>
|
Shell::PowerShell =>
|
||||||
for (needle, replacement) in POWERSHELL_COMPLETION_REPLACEMENTS {
|
for (needle, replacement) in completions::POWERSHELL_COMPLETION_REPLACEMENTS {
|
||||||
replace(&mut script, needle, replacement)?;
|
replace(&mut script, needle, replacement)?;
|
||||||
},
|
},
|
||||||
|
|
||||||
Shell::Zsh =>
|
Shell::Zsh =>
|
||||||
for (needle, replacement) in ZSH_COMPLETION_REPLACEMENTS {
|
for (needle, replacement) in completions::ZSH_COMPLETION_REPLACEMENTS {
|
||||||
replace(&mut script, needle, replacement)?;
|
replace(&mut script, needle, replacement)?;
|
||||||
},
|
},
|
||||||
Shell::Elvish => {},
|
Shell::Elvish => {},
|
||||||
@ -259,4 +216,205 @@ impl Subcommand {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dump(ast: Ast) {
|
||||||
|
print!("{}", ast);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit(search: &Search) -> Result<(), Error<'static>> {
|
||||||
|
let editor = env::var_os("VISUAL")
|
||||||
|
.or_else(|| env::var_os("EDITOR"))
|
||||||
|
.unwrap_or_else(|| "vim".into());
|
||||||
|
|
||||||
|
let error = Command::new(&editor)
|
||||||
|
.current_dir(&search.working_directory)
|
||||||
|
.arg(&search.justfile)
|
||||||
|
.status();
|
||||||
|
|
||||||
|
let status = match error {
|
||||||
|
Err(io_error) => return Err(Error::EditorInvoke { editor, io_error }),
|
||||||
|
Ok(status) => status,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
return Err(Error::EditorStatus { editor, status });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format(config: &Config, ast: Ast, search: &Search) -> Result<(), Error<'static>> {
|
||||||
|
config.require_unstable("The `--fmt` command is currently unstable.")?;
|
||||||
|
|
||||||
|
if let Err(io_error) =
|
||||||
|
File::create(&search.justfile).and_then(|mut file| write!(file, "{}", ast))
|
||||||
|
{
|
||||||
|
Err(Error::WriteJustfile {
|
||||||
|
justfile: search.justfile.clone(),
|
||||||
|
io_error,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if config.verbosity.loud() {
|
||||||
|
eprintln!("Wrote justfile to `{}`", search.justfile.display());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(config: &Config) -> Result<(), Error<'static>> {
|
||||||
|
let search = Search::init(&config.search_config, &config.invocation_directory)?;
|
||||||
|
|
||||||
|
if search.justfile.is_file() {
|
||||||
|
Err(Error::InitExists {
|
||||||
|
justfile: search.justfile,
|
||||||
|
})
|
||||||
|
} else if let Err(io_error) = fs::write(&search.justfile, INIT_JUSTFILE) {
|
||||||
|
Err(Error::WriteJustfile {
|
||||||
|
justfile: search.justfile,
|
||||||
|
io_error,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if config.verbosity.loud() {
|
||||||
|
eprintln!("Wrote justfile to `{}`", search.justfile.display());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(config: &Config, justfile: Justfile) {
|
||||||
|
// Construct a target to alias map.
|
||||||
|
let mut recipe_aliases: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
|
||||||
|
for alias in justfile.aliases.values() {
|
||||||
|
if alias.is_private() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !recipe_aliases.contains_key(alias.target.name.lexeme()) {
|
||||||
|
recipe_aliases.insert(alias.target.name.lexeme(), vec![alias.name.lexeme()]);
|
||||||
|
} else {
|
||||||
|
let aliases = recipe_aliases.get_mut(alias.target.name.lexeme()).unwrap();
|
||||||
|
aliases.push(alias.name.lexeme());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut line_widths: BTreeMap<&str, usize> = BTreeMap::new();
|
||||||
|
|
||||||
|
for (name, recipe) in &justfile.recipes {
|
||||||
|
if recipe.private {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for name in iter::once(name).chain(recipe_aliases.get(name).unwrap_or(&Vec::new())) {
|
||||||
|
let mut line_width = UnicodeWidthStr::width(*name);
|
||||||
|
|
||||||
|
for parameter in &recipe.parameters {
|
||||||
|
line_width += UnicodeWidthStr::width(format!(" {}", parameter).as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if line_width <= 30 {
|
||||||
|
line_widths.insert(name, line_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_line_width = cmp::min(line_widths.values().cloned().max().unwrap_or(0), 30);
|
||||||
|
|
||||||
|
let doc_color = config.color.stdout().doc();
|
||||||
|
print!("{}", config.list_heading);
|
||||||
|
|
||||||
|
for recipe in justfile.public_recipes(config.unsorted) {
|
||||||
|
let name = recipe.name();
|
||||||
|
|
||||||
|
for (i, name) in iter::once(&name)
|
||||||
|
.chain(recipe_aliases.get(name).unwrap_or(&Vec::new()))
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
print!("{}{}", config.list_prefix, name);
|
||||||
|
for parameter in &recipe.parameters {
|
||||||
|
if config.color.stdout().active() {
|
||||||
|
print!(" {:#}", parameter);
|
||||||
|
} else {
|
||||||
|
print!(" {}", parameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declaring this outside of the nested loops will probably be more efficient,
|
||||||
|
// but it creates all sorts of lifetime issues with variables inside the loops.
|
||||||
|
// If this is inlined like the docs say, it shouldn't make any difference.
|
||||||
|
let print_doc = |doc| {
|
||||||
|
print!(
|
||||||
|
" {:padding$}{} {}",
|
||||||
|
"",
|
||||||
|
doc_color.paint("#"),
|
||||||
|
doc_color.paint(doc),
|
||||||
|
padding = max_line_width
|
||||||
|
.saturating_sub(line_widths.get(name).cloned().unwrap_or(max_line_width))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
match (i, recipe.doc) {
|
||||||
|
(0, Some(doc)) => print_doc(doc),
|
||||||
|
(0, None) => (),
|
||||||
|
_ => {
|
||||||
|
let alias_doc = format!("alias for `{}`", recipe.name);
|
||||||
|
print_doc(&alias_doc);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show<'src>(name: &str, justfile: Justfile<'src>) -> Result<(), Error<'src>> {
|
||||||
|
if let Some(alias) = justfile.get_alias(name) {
|
||||||
|
let recipe = justfile.get_recipe(alias.target.name.lexeme()).unwrap();
|
||||||
|
println!("{}", alias);
|
||||||
|
println!("{}", recipe);
|
||||||
|
Ok(())
|
||||||
|
} else if let Some(recipe) = justfile.get_recipe(name) {
|
||||||
|
println!("{}", recipe);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::UnknownRecipes {
|
||||||
|
recipes: vec![name.to_owned()],
|
||||||
|
suggestion: justfile.suggest_recipe(name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn summary(config: &Config, justfile: Justfile) {
|
||||||
|
if justfile.count() == 0 {
|
||||||
|
if config.verbosity.loud() {
|
||||||
|
eprintln!("Justfile contains no recipes.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let summary = justfile
|
||||||
|
.public_recipes(config.unsorted)
|
||||||
|
.iter()
|
||||||
|
.map(|recipe| recipe.name())
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(" ");
|
||||||
|
println!("{}", summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn variables(justfile: Justfile) {
|
||||||
|
for (i, (_, assignment)) in justfile.assignments.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
print!(" ");
|
||||||
|
}
|
||||||
|
print!("{}", assignment.name);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn init_justfile() {
|
||||||
|
testing::compile(INIT_JUSTFILE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user