1use std::sync::atomic;
2
3use anyhow::Result;
4use better_default::Default;
5use clap::{CommandFactory, FromArgMatches, Parser};
6use rayon::prelude::*;
7use yansi::Paint;
8
9use crate::cli::GardenOptions;
10use crate::{cli, cmd, constants, display, errors, eval, model, path, query, syntax};
11
12#[derive(Parser, Clone, Debug)]
14#[command(author, about, long_about)]
15pub struct CmdOptions {
16 #[arg(long, short)]
18 breadth_first: bool,
19 #[arg(long, short = 'N')]
21 dry_run: bool,
22 #[arg(long, short)]
24 keep_going: bool,
25 #[arg(long, short, default_value = "*")]
27 trees: String,
28 #[arg(long, short = 'D')]
30 define: Vec<String>,
31 #[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
39 exit_on_error: bool,
40 #[arg(long, short)]
42 force: bool,
43 #[arg(
45 long = "jobs",
46 short = 'j',
47 require_equals = false,
48 num_args = 0..=1,
49 default_missing_value = "0",
50 value_name = "JOBS",
51 )]
52 num_jobs: Option<usize>,
53 #[arg(short, long)]
55 quiet: bool,
56 #[arg(short, long, action = clap::ArgAction::Count)]
58 verbose: u8,
59 #[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
65 word_split: bool,
66 query: String,
68 #[arg(required = true, value_terminator = "--")]
72 commands: Vec<String>,
73 #[arg(last = true)]
75 arguments: Vec<String>,
76}
77
78#[derive(Parser, Clone, Debug)]
80#[command(bin_name = constants::GARDEN)]
81#[command(styles = clap_cargo::style::CLAP_STYLING)]
82pub struct CustomOptions {
83 #[arg(long, short = 'D')]
85 define: Vec<String>,
86 #[arg(long, short = 'N')]
88 dry_run: bool,
89 #[arg(long, short)]
91 keep_going: bool,
92 #[arg(long, short, default_value = "*")]
94 trees: String,
95 #[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
103 exit_on_error: bool,
104 #[arg(long, short)]
106 force: bool,
107 #[arg(
109 long = "jobs",
110 short = 'j',
111 require_equals = false,
112 num_args = 0..=1,
113 default_missing_value = "0",
114 value_name = "JOBS",
115 )]
116 num_jobs: Option<usize>,
117 #[arg(short, long)]
119 quiet: bool,
120 #[arg(short, long, action = clap::ArgAction::Count)]
122 verbose: u8,
123 #[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
129 word_split: bool,
130 #[arg(value_terminator = "--")]
134 queries: Vec<String>,
135 #[arg(last = true)]
137 arguments: Vec<String>,
138}
139
140pub fn main_cmd(app_context: &model::ApplicationContext, options: &mut CmdOptions) -> Result<()> {
142 app_context
143 .get_root_config_mut()
144 .apply_defines(&options.define);
145 app_context
146 .get_root_config_mut()
147 .update_quiet_and_verbose_variables(options.quiet, options.verbose);
148 if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
149 debug!("jobs: {:?}", options.num_jobs);
150 debug!("query: {}", options.query);
151 debug!("commands: {:?}", options.commands);
152 debug!("arguments: {:?}", options.arguments);
153 debug!("trees: {:?}", options.trees);
154 }
155 if !app_context.get_root_config().shell_exit_on_error {
156 options.exit_on_error = false;
157 }
158 if !app_context.get_root_config().shell_word_split {
159 options.word_split = false;
160 }
161 let mut params: CmdParams = options.clone().into();
162 params.update(&app_context.options)?;
163
164 let exit_status = if options.num_jobs.is_some() {
165 cmd_parallel(app_context, &options.query, ¶ms)?
166 } else {
167 cmd(app_context, &options.query, ¶ms)?
168 };
169
170 errors::exit_status_into_result(exit_status)
171}
172
173#[derive(Clone, Debug, Default)]
177pub struct CmdParams {
178 commands: Vec<String>,
179 arguments: Vec<String>,
180 queries: Vec<String>,
181 tree_pattern: glob::Pattern,
182 breadth_first: bool,
183 dry_run: bool,
184 force: bool,
185 keep_going: bool,
186 num_jobs: Option<usize>,
187 #[default(true)]
188 exit_on_error: bool,
189 quiet: bool,
190 verbose: u8,
191 #[default(true)]
192 word_split: bool,
193}
194
195impl From<CmdOptions> for CmdParams {
197 fn from(options: CmdOptions) -> Self {
198 Self {
199 commands: options.commands.clone(),
200 arguments: options.arguments.clone(),
201 breadth_first: options.breadth_first,
202 dry_run: options.dry_run,
203 exit_on_error: options.exit_on_error,
204 force: options.force,
205 keep_going: options.keep_going,
206 num_jobs: options.num_jobs,
207 tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
208 quiet: options.quiet,
209 verbose: options.verbose,
210 word_split: options.word_split,
211 ..Default::default()
212 }
213 }
214}
215
216impl From<CustomOptions> for CmdParams {
218 fn from(options: CustomOptions) -> Self {
219 let mut params = Self {
220 arguments: options.arguments.clone(),
222 queries: options.queries.clone(),
223 breadth_first: options.num_jobs.is_none(),
231 dry_run: options.dry_run,
232 keep_going: options.keep_going,
233 exit_on_error: options.exit_on_error,
234 force: options.force,
235 num_jobs: options.num_jobs,
236 tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
237 quiet: options.quiet,
238 verbose: options.verbose,
239 word_split: options.word_split,
240 ..Default::default()
241 };
242
243 if params.queries.is_empty() {
245 params.queries.push(constants::DOT.into());
246 }
247
248 params
249 }
250}
251
252impl CmdParams {
253 fn update(&mut self, options: &cli::MainOptions) -> Result<()> {
255 self.quiet |= options.quiet;
256 self.verbose += options.verbose;
257 cmd::initialize_threads_option(self.num_jobs)?;
258
259 Ok(())
260 }
261}
262
263fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
265 let mut cmd = I::command();
266 err.format(&mut cmd)
267}
268
269pub fn main_custom(app_context: &model::ApplicationContext, arguments: &Vec<String>) -> Result<()> {
271 let name = &arguments[0];
273 let garden_custom = format!("garden {name}");
274 let cli = CustomOptions::command().bin_name(garden_custom);
275 let matches = cli.get_matches_from(arguments);
276
277 let mut options = <CustomOptions as FromArgMatches>::from_arg_matches(&matches)
278 .map_err(format_error::<CustomOptions>)?;
279 app_context
280 .get_root_config_mut()
281 .apply_defines(&options.define);
282 app_context
283 .get_root_config_mut()
284 .update_quiet_and_verbose_variables(options.quiet, options.verbose);
285 if !app_context.get_root_config().shell_exit_on_error {
286 options.exit_on_error = false;
287 }
288 if !app_context.get_root_config().shell_word_split {
289 options.word_split = false;
290 }
291
292 if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
293 debug!("jobs: {:?}", options.num_jobs);
294 debug!("command: {}", name);
295 debug!("queries: {:?}", options.queries);
296 debug!("arguments: {:?}", options.arguments);
297 debug!("trees: {:?}", options.trees);
298 }
299
300 let mut params: CmdParams = options.clone().into();
302 params.update(&app_context.options)?;
303 params.commands.push(name.to_string());
304
305 cmds(app_context, ¶ms)
306}
307
308fn cmd(app_context: &model::ApplicationContext, query: &str, params: &CmdParams) -> Result<i32> {
320 let config = app_context.get_root_config_mut();
321 let contexts = query::resolve_trees(app_context, config, None, query);
322 if params.breadth_first {
323 run_cmd_breadth_first(app_context, &contexts, params)
324 } else {
325 run_cmd_depth_first(app_context, &contexts, params)
326 }
327}
328
329fn cmd_parallel(
331 app_context: &model::ApplicationContext,
332 query: &str,
333 params: &CmdParams,
334) -> Result<i32> {
335 let config = app_context.get_root_config_mut();
336 let contexts = query::resolve_trees(app_context, config, None, query);
337 if params.breadth_first {
338 run_cmd_breadth_first_parallel(app_context, &contexts, params)
339 } else {
340 run_cmd_depth_first_parallel(app_context, &contexts, params)
341 }
342}
343
344struct ShellParams {
346 shell_command: Vec<String>,
348 is_shell: bool,
350}
351
352impl ShellParams {
353 fn new(shell: &str, exit_on_error: bool, word_split: bool) -> Self {
354 let mut shell_command = cmd::shlex_split(shell);
355 let basename = path::str_basename(&shell_command[0]);
356 let is_shell = path::is_shell(basename);
358 let is_zsh = matches!(basename, constants::SHELL_ZSH);
359 let is_dash_e = matches!(
361 basename,
362 constants::SHELL_BUN
363 | constants::SHELL_NODE
364 | constants::SHELL_NODEJS
365 | constants::SHELL_PERL
366 | constants::SHELL_RUBY
367 );
368 let is_custom = shell_command.len() > 1;
371 if !is_custom {
372 if word_split && is_zsh {
373 shell_command.push(string!("-o"));
374 shell_command.push(string!("shwordsplit"));
375 }
376 if is_zsh {
377 shell_command.push(string!("+o"));
378 shell_command.push(string!("nomatch"));
379 }
380 if exit_on_error && is_shell {
381 shell_command.push(string!("-e"));
382 }
383 if is_dash_e {
384 shell_command.push(string!("-e"));
385 } else {
386 shell_command.push(string!("-c"));
387 }
388 }
389
390 Self {
391 shell_command,
392 is_shell,
393 }
394 }
395
396 fn from_str(shell: &str) -> Self {
398 let shell_command = cmd::shlex_split(shell);
399 let basename = path::str_basename(&shell_command[0]);
400 let is_shell = path::is_shell(basename);
402
403 Self {
404 shell_command,
405 is_shell,
406 }
407 }
408
409 fn from_context_and_params(
411 app_context: &model::ApplicationContext,
412 params: &CmdParams,
413 ) -> Self {
414 let shell = app_context.get_root_config().shell.as_str();
415 Self::new(shell, params.exit_on_error, params.word_split)
416 }
417}
418
419fn get_tree_from_context<'a>(
422 app_context: &'a model::ApplicationContext,
423 context: &model::TreeContext,
424 params: &CmdParams,
425) -> Option<(&'a model::Configuration, &'a model::Tree)> {
426 if !params.tree_pattern.matches(&context.tree) {
428 return None;
429 }
430 let config = match context.config {
432 Some(config_id) => app_context.get_config(config_id),
433 None => app_context.get_root_config(),
434 };
435 let tree = config.trees.get(&context.tree)?;
436 if tree.is_symlink {
437 return None;
438 }
439
440 Some((config, tree))
441}
442
443fn get_command_environment<'a>(
445 app_context: &'a model::ApplicationContext,
446 context: &model::TreeContext,
447 params: &CmdParams,
448) -> Option<(Option<String>, &'a String, model::Environment)> {
449 let (config, tree) = get_tree_from_context(app_context, context, params)?;
450 let Ok(tree_path) = tree.path_as_ref() else {
452 return None;
453 };
454 let env = eval::environment(app_context, config, context);
456 let mut fallback_path = None;
458 let display_options = display::DisplayOptions {
459 branches: config.tree_branches,
460 quiet: params.quiet,
461 verbose: params.verbose,
462 ..std::default::Default::default()
463 };
464 if !display::print_tree(tree, &display_options) {
465 if params.force {
467 fallback_path = Some(config.fallback_execdir_string());
468 } else {
469 return None;
470 }
471 }
472
473 Some((fallback_path, tree_path, env))
474}
475
476fn expand_and_run_command(
478 app_context: &model::ApplicationContext,
479 context: &model::TreeContext,
480 name: &str,
481 path: &str,
482 shell_params: &ShellParams,
483 params: &CmdParams,
484 env: &model::Environment,
485) -> Result<i32, i32> {
486 let mut exit_status = errors::EX_OK;
487 let command_names = cmd::expand_command_names(app_context, context, name);
489 for command_name in &command_names {
490 let cmd_seq_vec = eval::command(app_context, context, command_name);
494 app_context.get_root_config_mut().reset();
495
496 if let Err(cmd_status) = run_cmd_vec(path, shell_params, env, &cmd_seq_vec, params) {
497 exit_status = cmd_status;
498 if !params.keep_going {
499 return Err(cmd_status);
500 }
501 }
502 }
503
504 Ok(exit_status)
505}
506
507fn run_cmd_breadth_first(
509 app_context: &model::ApplicationContext,
510 contexts: &[model::TreeContext],
511 params: &CmdParams,
512) -> Result<i32> {
513 let mut exit_status: i32 = errors::EX_OK;
514 let shell_params = ShellParams::from_context_and_params(app_context, params);
515 for name in ¶ms.commands {
518 for context in contexts {
520 let Some((fallback_path, tree_path, env)) =
521 get_command_environment(app_context, context, params)
522 else {
523 continue;
524 };
525 let path = fallback_path.as_ref().unwrap_or(tree_path);
526 match expand_and_run_command(
527 app_context,
528 context,
529 name,
530 path,
531 &shell_params,
532 params,
533 &env,
534 ) {
535 Ok(cmd_status) => {
536 if cmd_status != errors::EX_OK {
537 exit_status = cmd_status;
538 }
539 }
540 Err(cmd_status) => return Ok(cmd_status),
541 }
542 }
543 }
544
545 Ok(exit_status)
547}
548
549fn run_cmd_breadth_first_parallel(
554 app_context: &model::ApplicationContext,
555 contexts: &[model::TreeContext],
556 params: &CmdParams,
557) -> Result<i32> {
558 let exit_status = atomic::AtomicI32::new(errors::EX_OK);
559 let shell_params = ShellParams::from_context_and_params(app_context, params);
560 params.commands.par_iter().for_each(|name| {
562 let app_context_clone = app_context.clone();
564 let app_context = &app_context_clone;
565 for context in contexts {
567 let Some((fallback_path, tree_path, env)) =
568 get_command_environment(app_context, context, params)
569 else {
570 continue;
571 };
572 let path = fallback_path.as_ref().unwrap_or(tree_path);
573 match expand_and_run_command(
574 app_context,
575 context,
576 name,
577 path,
578 &shell_params,
579 params,
580 &env,
581 ) {
582 Ok(cmd_status) => {
583 if cmd_status != errors::EX_OK {
584 exit_status.store(cmd_status, atomic::Ordering::Release);
585 }
586 }
587 Err(cmd_status) => {
588 exit_status.store(cmd_status, atomic::Ordering::Release);
589 break;
590 }
591 }
592 }
593 });
594
595 Ok(exit_status.load(atomic::Ordering::Acquire))
597}
598
599fn run_cmd_depth_first(
601 app_context: &model::ApplicationContext,
602 contexts: &[model::TreeContext],
603 params: &CmdParams,
604) -> Result<i32> {
605 let mut exit_status: i32 = errors::EX_OK;
606 let shell_params = ShellParams::from_context_and_params(app_context, params);
607 for context in contexts {
609 let Some((fallback_path, tree_path, env)) =
610 get_command_environment(app_context, context, params)
611 else {
612 continue;
613 };
614 let path = fallback_path.as_ref().unwrap_or(tree_path);
615 for name in ¶ms.commands {
617 match expand_and_run_command(
618 app_context,
619 context,
620 name,
621 path,
622 &shell_params,
623 params,
624 &env,
625 ) {
626 Ok(cmd_status) => {
627 if cmd_status != errors::EX_OK {
628 exit_status = cmd_status;
629 }
630 }
631 Err(cmd_status) => return Ok(cmd_status),
632 }
633 }
634 }
635
636 Ok(exit_status)
638}
639
640fn run_cmd_depth_first_parallel(
644 app_context: &model::ApplicationContext,
645 contexts: &[model::TreeContext],
646 params: &CmdParams,
647) -> Result<i32> {
648 let exit_status = atomic::AtomicI32::new(errors::EX_OK);
649 let shell_params = ShellParams::from_context_and_params(app_context, params);
650 contexts.par_iter().for_each(|context| {
652 let app_context_clone = app_context.clone();
654 let app_context = &app_context_clone;
655 let Some((fallback_path, tree_path, env)) =
656 get_command_environment(app_context, context, params)
657 else {
658 return;
659 };
660 let path = fallback_path.as_ref().unwrap_or(tree_path);
661 for name in ¶ms.commands {
663 match expand_and_run_command(
664 app_context,
665 context,
666 name,
667 path,
668 &shell_params,
669 params,
670 &env,
671 ) {
672 Ok(cmd_status) => {
673 if cmd_status != errors::EX_OK {
674 exit_status.store(cmd_status, atomic::Ordering::Release);
675 }
676 }
677 Err(cmd_status) => {
678 exit_status.store(cmd_status, atomic::Ordering::Release);
679 break;
680 }
681 }
682 }
683 });
684
685 Ok(exit_status.load(atomic::Ordering::Acquire))
689}
690
691fn run_cmd_vec(
699 path: &str,
700 shell_params: &ShellParams,
701 env: &model::Environment,
702 cmd_seq_vec: &[Vec<String>],
703 params: &CmdParams,
704) -> Result<(), i32> {
705 let current_exe = cmd::current_exe();
707 let mut exit_status = errors::EX_OK;
708 for cmd_seq in cmd_seq_vec {
709 for cmd_str in cmd_seq {
710 if params.verbose > 1 {
711 eprintln!("{} {}", ":".cyan(), &cmd_str.trim_end().green());
712 }
713 if params.dry_run {
714 continue;
715 }
716 let cmd_shell_params;
718 let (cmd_str, shell_params) = match syntax::split_shebang(cmd_str) {
719 Some((shell_cmd, cmd_str)) => {
720 cmd_shell_params = ShellParams::from_str(shell_cmd);
721 (cmd_str, &cmd_shell_params)
722 }
723 None => (cmd_str.as_str(), shell_params),
724 };
725 let mut exec = subprocess::Exec::cmd(&shell_params.shell_command[0]).cwd(path);
726 exec = exec.args(&shell_params.shell_command[1..]);
727 exec = exec.arg(cmd_str);
728 if shell_params.is_shell {
729 exec = exec.arg(current_exe.as_str());
733 }
734 exec = exec.args(¶ms.arguments);
735 for (k, v) in env {
737 exec = exec.env(k, v);
738 }
739 let status = cmd::status(exec);
742 if status != errors::EX_OK {
743 exit_status = status;
744 if params.exit_on_error {
745 return Err(status);
746 }
747 } else {
748 exit_status = errors::EX_OK;
749 }
750 }
751 if exit_status != errors::EX_OK {
752 return Err(exit_status);
753 }
754 }
755
756 Ok(())
757}
758
759fn cmds(app: &model::ApplicationContext, params: &CmdParams) -> Result<()> {
761 let exit_status = atomic::AtomicI32::new(errors::EX_OK);
762 if params.num_jobs.is_some() {
763 params.queries.par_iter().for_each(|query| {
764 let status = cmd_parallel(&app.clone(), query, params).unwrap_or(errors::EX_IOERR);
765 if status != errors::EX_OK {
766 exit_status.store(status, atomic::Ordering::Release);
767 }
768 });
769 } else {
770 for query in ¶ms.queries {
771 let status = cmd(app, query, params).unwrap_or(errors::EX_IOERR);
772 if status != errors::EX_OK {
773 exit_status.store(status, atomic::Ordering::Release);
774 if !params.keep_going {
775 break;
776 }
777 }
778 }
779 }
780 errors::exit_status_into_result(exit_status.load(atomic::Ordering::Acquire))
782}