use std::sync::atomic;
use anyhow::Result;
use better_default::Default;
use clap::{CommandFactory, FromArgMatches, Parser};
use rayon::prelude::*;
use yansi::Paint;
use crate::cli::GardenOptions;
use crate::{cli, cmd, constants, display, errors, eval, model, path, query, syntax};
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct CmdOptions {
#[arg(long, short)]
breadth_first: bool,
#[arg(long, short = 'N')]
dry_run: bool,
#[arg(long, short)]
keep_going: bool,
#[arg(long, short, default_value = "*")]
trees: String,
#[arg(long, short = 'D')]
define: Vec<String>,
#[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
exit_on_error: bool,
#[arg(long, short)]
force: bool,
#[arg(
long = "jobs",
short = 'j',
require_equals = false,
num_args = 0..=1,
default_missing_value = "0",
value_name = "JOBS",
)]
num_jobs: Option<usize>,
#[arg(short, long)]
quiet: bool,
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[arg(short = 'x', long)]
echo: bool,
#[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
word_split: bool,
query: String,
#[arg(required = true, value_terminator = "--")]
commands: Vec<String>,
#[arg(last = true)]
arguments: Vec<String>,
}
#[derive(Parser, Clone, Debug)]
#[command(bin_name = constants::GARDEN)]
#[command(styles = clap_cargo::style::CLAP_STYLING)]
pub struct CustomOptions {
#[arg(long, short = 'D')]
define: Vec<String>,
#[arg(long, short = 'N')]
dry_run: bool,
#[arg(long, short)]
keep_going: bool,
#[arg(long, short, default_value = "*")]
trees: String,
#[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
exit_on_error: bool,
#[arg(long, short)]
force: bool,
#[arg(
long = "jobs",
short = 'j',
require_equals = false,
num_args = 0..=1,
default_missing_value = "0",
value_name = "JOBS",
)]
num_jobs: Option<usize>,
#[arg(short, long)]
quiet: bool,
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[arg(short = 'x', long)]
echo: bool,
#[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
word_split: bool,
#[arg(value_terminator = "--")]
queries: Vec<String>,
#[arg(last = true)]
arguments: Vec<String>,
}
pub fn main_cmd(app_context: &model::ApplicationContext, options: &mut CmdOptions) -> Result<()> {
app_context
.get_root_config_mut()
.apply_defines(&options.define);
app_context
.get_root_config_mut()
.update_quiet_and_verbose_variables(options.quiet, options.verbose);
if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
debug!("jobs: {:?}", options.num_jobs);
debug!("query: {}", options.query);
debug!("commands: {:?}", options.commands);
debug!("arguments: {:?}", options.arguments);
debug!("trees: {:?}", options.trees);
}
if !app_context.get_root_config().shell_exit_on_error {
options.exit_on_error = false;
}
if !app_context.get_root_config().shell_word_split {
options.word_split = false;
}
let mut params: CmdParams = options.clone().into();
params.update(&app_context.options)?;
let exit_status = if options.num_jobs.is_some() {
cmd_parallel(app_context, &options.query, ¶ms)?
} else {
cmd(app_context, &options.query, ¶ms)?
};
errors::exit_status_into_result(exit_status)
}
#[derive(Clone, Debug, Default)]
pub struct CmdParams {
commands: Vec<String>,
arguments: Vec<String>,
queries: Vec<String>,
tree_pattern: glob::Pattern,
breadth_first: bool,
dry_run: bool,
force: bool,
keep_going: bool,
num_jobs: Option<usize>,
echo: bool,
#[default(true)]
exit_on_error: bool,
quiet: bool,
verbose: u8,
#[default(true)]
word_split: bool,
}
impl From<CmdOptions> for CmdParams {
fn from(options: CmdOptions) -> Self {
Self {
arguments: options.arguments.clone(),
breadth_first: options.breadth_first,
commands: options.commands.clone(),
dry_run: options.dry_run,
echo: options.echo,
exit_on_error: options.exit_on_error,
force: options.force,
keep_going: options.keep_going,
num_jobs: options.num_jobs,
quiet: options.quiet,
tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
verbose: options.verbose,
word_split: options.word_split,
..Default::default()
}
}
}
impl From<CustomOptions> for CmdParams {
fn from(options: CustomOptions) -> Self {
let mut params = Self {
arguments: options.arguments.clone(),
breadth_first: options.num_jobs.is_none(),
dry_run: options.dry_run,
echo: options.echo,
exit_on_error: options.exit_on_error,
force: options.force,
keep_going: options.keep_going,
num_jobs: options.num_jobs,
queries: options.queries.clone(),
quiet: options.quiet,
tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
verbose: options.verbose,
word_split: options.word_split,
..Default::default()
};
if params.queries.is_empty() {
params.queries.push(constants::DOT.into());
}
params
}
}
impl CmdParams {
fn update(&mut self, options: &cli::MainOptions) -> Result<()> {
self.quiet |= options.quiet;
self.verbose += options.verbose;
cmd::initialize_threads_option(self.num_jobs)?;
Ok(())
}
}
fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
let mut cmd = I::command();
err.format(&mut cmd)
}
pub fn main_custom(app_context: &model::ApplicationContext, arguments: &Vec<String>) -> Result<()> {
let name = &arguments[0];
let garden_custom = format!("garden {name}");
let cli = CustomOptions::command().bin_name(garden_custom);
let matches = cli.get_matches_from(arguments);
let mut options = <CustomOptions as FromArgMatches>::from_arg_matches(&matches)
.map_err(format_error::<CustomOptions>)?;
app_context
.get_root_config_mut()
.apply_defines(&options.define);
app_context
.get_root_config_mut()
.update_quiet_and_verbose_variables(options.quiet, options.verbose);
if !app_context.get_root_config().shell_exit_on_error {
options.exit_on_error = false;
}
if !app_context.get_root_config().shell_word_split {
options.word_split = false;
}
if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
debug!("jobs: {:?}", options.num_jobs);
debug!("command: {}", name);
debug!("queries: {:?}", options.queries);
debug!("arguments: {:?}", options.arguments);
debug!("trees: {:?}", options.trees);
}
let mut params: CmdParams = options.clone().into();
params.update(&app_context.options)?;
params.commands.push(name.to_string());
cmds(app_context, ¶ms)
}
fn cmd(app_context: &model::ApplicationContext, query: &str, params: &CmdParams) -> Result<i32> {
let config = app_context.get_root_config_mut();
let contexts = query::resolve_trees(app_context, config, None, query);
if params.breadth_first {
run_cmd_breadth_first(app_context, &contexts, params)
} else {
run_cmd_depth_first(app_context, &contexts, params)
}
}
fn cmd_parallel(
app_context: &model::ApplicationContext,
query: &str,
params: &CmdParams,
) -> Result<i32> {
let config = app_context.get_root_config_mut();
let contexts = query::resolve_trees(app_context, config, None, query);
if params.breadth_first {
run_cmd_breadth_first_parallel(app_context, &contexts, params)
} else {
run_cmd_depth_first_parallel(app_context, &contexts, params)
}
}
struct ShellParams {
shell_command: Vec<String>,
is_shell: bool,
}
impl ShellParams {
fn new(shell: &str, echo: bool, exit_on_error: bool, word_split: bool) -> Self {
let mut shell_command = cmd::shlex_split(shell);
let basename = path::str_basename(&shell_command[0]);
let is_shell = path::is_shell(basename);
let is_zsh = matches!(basename, constants::SHELL_ZSH);
let is_dash_e = matches!(
basename,
constants::SHELL_BUN
| constants::SHELL_NODE
| constants::SHELL_NODEJS
| constants::SHELL_PERL
| constants::SHELL_RUBY
);
let is_custom = shell_command.len() > 1;
if !is_custom {
if word_split && is_zsh {
shell_command.push(string!("-o"));
shell_command.push(string!("shwordsplit"));
}
if is_zsh {
shell_command.push(string!("+o"));
shell_command.push(string!("nomatch"));
}
if echo && is_shell {
shell_command.push(string!("-x"));
}
if exit_on_error && is_shell {
shell_command.push(string!("-e"));
}
if is_dash_e {
shell_command.push(string!("-e"));
} else {
shell_command.push(string!("-c"));
}
}
Self {
shell_command,
is_shell,
}
}
fn from_str(shell: &str) -> Self {
let shell_command = cmd::shlex_split(shell);
let basename = path::str_basename(&shell_command[0]);
let is_shell = path::is_shell(basename);
Self {
shell_command,
is_shell,
}
}
fn from_context_and_params(
app_context: &model::ApplicationContext,
params: &CmdParams,
) -> Self {
let shell = app_context.get_root_config().shell.as_str();
Self::new(shell, params.echo, params.exit_on_error, params.word_split)
}
}
fn get_tree_from_context<'a>(
app_context: &'a model::ApplicationContext,
context: &model::TreeContext,
params: &CmdParams,
) -> Option<(&'a model::Configuration, &'a model::Tree)> {
if !params.tree_pattern.matches(&context.tree) {
return None;
}
let config = match context.config {
Some(config_id) => app_context.get_config(config_id),
None => app_context.get_root_config(),
};
let tree = config.trees.get(&context.tree)?;
if tree.is_symlink {
return None;
}
Some((config, tree))
}
fn get_command_environment<'a>(
app_context: &'a model::ApplicationContext,
context: &model::TreeContext,
params: &CmdParams,
) -> Option<(Option<String>, &'a String, model::Environment)> {
let (config, tree) = get_tree_from_context(app_context, context, params)?;
let Ok(tree_path) = tree.path_as_ref() else {
return None;
};
let env = eval::environment(app_context, config, context);
let mut fallback_path = None;
let display_options = display::DisplayOptions {
branches: config.tree_branches,
quiet: params.quiet,
verbose: params.verbose,
..std::default::Default::default()
};
if !display::print_tree(tree, &display_options) {
if params.force {
fallback_path = Some(config.fallback_execdir_string());
} else {
return None;
}
}
Some((fallback_path, tree_path, env))
}
fn expand_and_run_command(
app_context: &model::ApplicationContext,
context: &model::TreeContext,
name: &str,
path: &str,
shell_params: &ShellParams,
params: &CmdParams,
env: &model::Environment,
) -> Result<i32, i32> {
let mut exit_status = errors::EX_OK;
let command_names = cmd::expand_command_names(app_context, context, name);
for command_name in &command_names {
let cmd_seq_vec = eval::command(app_context, context, command_name);
app_context.get_root_config_mut().reset();
if let Err(cmd_status) = run_cmd_vec(path, shell_params, env, &cmd_seq_vec, params) {
exit_status = cmd_status;
if !params.keep_going {
return Err(cmd_status);
}
}
}
Ok(exit_status)
}
fn run_cmd_breadth_first(
app_context: &model::ApplicationContext,
contexts: &[model::TreeContext],
params: &CmdParams,
) -> Result<i32> {
let mut exit_status: i32 = errors::EX_OK;
let shell_params = ShellParams::from_context_and_params(app_context, params);
for name in ¶ms.commands {
for context in contexts {
let Some((fallback_path, tree_path, env)) =
get_command_environment(app_context, context, params)
else {
continue;
};
let path = fallback_path.as_ref().unwrap_or(tree_path);
match expand_and_run_command(
app_context,
context,
name,
path,
&shell_params,
params,
&env,
) {
Ok(cmd_status) => {
if cmd_status != errors::EX_OK {
exit_status = cmd_status;
}
}
Err(cmd_status) => return Ok(cmd_status),
}
}
}
Ok(exit_status)
}
fn run_cmd_breadth_first_parallel(
app_context: &model::ApplicationContext,
contexts: &[model::TreeContext],
params: &CmdParams,
) -> Result<i32> {
let exit_status = atomic::AtomicI32::new(errors::EX_OK);
let shell_params = ShellParams::from_context_and_params(app_context, params);
params.commands.par_iter().for_each(|name| {
let app_context_clone = app_context.clone();
let app_context = &app_context_clone;
for context in contexts {
let Some((fallback_path, tree_path, env)) =
get_command_environment(app_context, context, params)
else {
continue;
};
let path = fallback_path.as_ref().unwrap_or(tree_path);
match expand_and_run_command(
app_context,
context,
name,
path,
&shell_params,
params,
&env,
) {
Ok(cmd_status) => {
if cmd_status != errors::EX_OK {
exit_status.store(cmd_status, atomic::Ordering::Release);
}
}
Err(cmd_status) => {
exit_status.store(cmd_status, atomic::Ordering::Release);
break;
}
}
}
});
Ok(exit_status.load(atomic::Ordering::Acquire))
}
fn run_cmd_depth_first(
app_context: &model::ApplicationContext,
contexts: &[model::TreeContext],
params: &CmdParams,
) -> Result<i32> {
let mut exit_status: i32 = errors::EX_OK;
let shell_params = ShellParams::from_context_and_params(app_context, params);
for context in contexts {
let Some((fallback_path, tree_path, env)) =
get_command_environment(app_context, context, params)
else {
continue;
};
let path = fallback_path.as_ref().unwrap_or(tree_path);
for name in ¶ms.commands {
match expand_and_run_command(
app_context,
context,
name,
path,
&shell_params,
params,
&env,
) {
Ok(cmd_status) => {
if cmd_status != errors::EX_OK {
exit_status = cmd_status;
}
}
Err(cmd_status) => return Ok(cmd_status),
}
}
}
Ok(exit_status)
}
fn run_cmd_depth_first_parallel(
app_context: &model::ApplicationContext,
contexts: &[model::TreeContext],
params: &CmdParams,
) -> Result<i32> {
let exit_status = atomic::AtomicI32::new(errors::EX_OK);
let shell_params = ShellParams::from_context_and_params(app_context, params);
contexts.par_iter().for_each(|context| {
let app_context_clone = app_context.clone();
let app_context = &app_context_clone;
let Some((fallback_path, tree_path, env)) =
get_command_environment(app_context, context, params)
else {
return;
};
let path = fallback_path.as_ref().unwrap_or(tree_path);
for name in ¶ms.commands {
match expand_and_run_command(
app_context,
context,
name,
path,
&shell_params,
params,
&env,
) {
Ok(cmd_status) => {
if cmd_status != errors::EX_OK {
exit_status.store(cmd_status, atomic::Ordering::Release);
}
}
Err(cmd_status) => {
exit_status.store(cmd_status, atomic::Ordering::Release);
break;
}
}
}
});
Ok(exit_status.load(atomic::Ordering::Acquire))
}
fn run_cmd_vec(
path: &str,
shell_params: &ShellParams,
env: &model::Environment,
cmd_seq_vec: &[Vec<String>],
params: &CmdParams,
) -> Result<(), i32> {
let current_exe = cmd::current_exe();
let mut exit_status = errors::EX_OK;
for cmd_seq in cmd_seq_vec {
for cmd_str in cmd_seq {
if params.verbose > 1 {
eprintln!("{} {}", ":".cyan(), &cmd_str.trim_end().green());
}
if params.dry_run {
continue;
}
let cmd_shell_params;
let (cmd_str, shell_params) = match syntax::split_shebang(cmd_str) {
Some((shell_cmd, cmd_str)) => {
cmd_shell_params = ShellParams::from_str(shell_cmd);
(cmd_str, &cmd_shell_params)
}
None => (cmd_str.as_str(), shell_params),
};
let mut exec = subprocess::Exec::cmd(&shell_params.shell_command[0]).cwd(path);
exec = exec.args(&shell_params.shell_command[1..]);
exec = exec.arg(cmd_str);
if shell_params.is_shell {
exec = exec.arg(current_exe.as_str());
}
exec = exec.args(¶ms.arguments);
for (k, v) in env {
exec = exec.env(k, v);
}
let status = cmd::status(exec);
if status != errors::EX_OK {
exit_status = status;
if params.exit_on_error {
return Err(status);
}
} else {
exit_status = errors::EX_OK;
}
}
if exit_status != errors::EX_OK {
return Err(exit_status);
}
}
Ok(())
}
fn cmds(app: &model::ApplicationContext, params: &CmdParams) -> Result<()> {
let exit_status = atomic::AtomicI32::new(errors::EX_OK);
if params.num_jobs.is_some() {
params.queries.par_iter().for_each(|query| {
let status = cmd_parallel(&app.clone(), query, params).unwrap_or(errors::EX_IOERR);
if status != errors::EX_OK {
exit_status.store(status, atomic::Ordering::Release);
}
});
} else {
for query in ¶ms.queries {
let status = cmd(app, query, params).unwrap_or(errors::EX_IOERR);
if status != errors::EX_OK {
exit_status.store(status, atomic::Ordering::Release);
if !params.keep_going {
break;
}
}
}
}
errors::exit_status_into_result(exit_status.load(atomic::Ordering::Acquire))
}