Move subcommand functions into Subcommand (#918)

This commit is contained in:
Casey Rodarmor 2021-07-26 17:19:52 -07:00 committed by GitHub
parent 4ada364ede
commit ce0376cfdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 518 additions and 518 deletions

View File

@ -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
View 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"#,
)];

View File

@ -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);
}
} }

View File

@ -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()))

View File

@ -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;

View File

@ -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() {

View File

@ -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
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
"#;
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
$recipes = Get-JustFileRecipes -CommandElements $elementValues
$completions += $recipes
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText"#,
)];
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"#,
)];
impl Subcommand { impl Subcommand {
pub(crate) fn completions(shell: &str) -> RunResult<'static, ()> { pub(crate) fn run<'src>(&self, config: &Config, loader: &'src Loader) -> Result<(), Error<'src>> {
use Subcommand::*;
if let Init = self {
return Self::init(config);
}
if let Completions { shell } = self {
return Self::completions(&shell);
}
let search = Search::find(&config.search_config, &config.invocation_directory)?;
if let Edit = self {
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);
}
} }