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(short = 'x', long)]
61 echo: bool,
62 #[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
68 word_split: bool,
69 query: String,
71 #[arg(required = true, value_terminator = "--")]
73 commands: Vec<String>,
74 #[arg(last = true)]
76 arguments: Vec<String>,
77}
78
79#[derive(Parser, Clone, Debug)]
81#[command(bin_name = constants::GARDEN)]
82#[command(styles = clap_cargo::style::CLAP_STYLING)]
83pub struct CustomOptions {
84 #[arg(long, short = 'D')]
86 define: Vec<String>,
87 #[arg(long, short = 'N')]
89 dry_run: bool,
90 #[arg(long, short)]
92 keep_going: bool,
93 #[arg(long, short, default_value = "*")]
95 trees: String,
96 #[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
104 exit_on_error: bool,
105 #[arg(long, short)]
107 force: bool,
108 #[arg(
110 long = "jobs",
111 short = 'j',
112 require_equals = false,
113 num_args = 0..=1,
114 default_missing_value = "0",
115 value_name = "JOBS",
116 )]
117 num_jobs: Option<usize>,
118 #[arg(short, long)]
120 quiet: bool,
121 #[arg(short, long, action = clap::ArgAction::Count)]
123 verbose: u8,
124 #[arg(short = 'x', long)]
126 echo: bool,
127 #[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
133 word_split: bool,
134 #[arg(value_terminator = "--")]
136 queries: Vec<String>,
137 #[arg(last = true)]
139 arguments: Vec<String>,
140}
141
142pub fn main_cmd(app_context: &model::ApplicationContext, options: &mut CmdOptions) -> Result<()> {
144 app_context
145 .get_root_config_mut()
146 .apply_defines(&options.define);
147 app_context
148 .get_root_config_mut()
149 .update_quiet_and_verbose_variables(options.quiet, options.verbose);
150 if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
151 debug!("jobs: {:?}", options.num_jobs);
152 debug!("query: {}", options.query);
153 debug!("commands: {:?}", options.commands);
154 debug!("arguments: {:?}", options.arguments);
155 debug!("trees: {:?}", options.trees);
156 }
157 if !app_context.get_root_config().shell_exit_on_error {
158 options.exit_on_error = false;
159 }
160 if !app_context.get_root_config().shell_word_split {
161 options.word_split = false;
162 }
163 let mut params: CmdParams = options.clone().into();
164 params.update(&app_context.options)?;
165
166 let exit_status = if options.num_jobs.is_some() {
167 cmd_parallel(app_context, &options.query, ¶ms)?
168 } else {
169 cmd(app_context, &options.query, ¶ms)?
170 };
171
172 errors::exit_status_into_result(exit_status)
173}
174
175#[derive(Clone, Debug, Default)]
179pub struct CmdParams {
180 commands: Vec<String>,
181 arguments: Vec<String>,
182 queries: Vec<String>,
183 tree_pattern: glob::Pattern,
184 breadth_first: bool,
185 dry_run: bool,
186 force: bool,
187 keep_going: bool,
188 num_jobs: Option<usize>,
189 echo: bool,
190 #[default(true)]
191 exit_on_error: bool,
192 quiet: bool,
193 verbose: u8,
194 #[default(true)]
195 word_split: bool,
196}
197
198impl From<CmdOptions> for CmdParams {
200 fn from(options: CmdOptions) -> Self {
201 Self {
202 arguments: options.arguments.clone(),
203 breadth_first: options.breadth_first,
204 commands: options.commands.clone(),
205 dry_run: options.dry_run,
206 echo: options.echo,
207 exit_on_error: options.exit_on_error,
208 force: options.force,
209 keep_going: options.keep_going,
210 num_jobs: options.num_jobs,
211 quiet: options.quiet,
212 tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
213 verbose: options.verbose,
214 word_split: options.word_split,
215 ..Default::default()
216 }
217 }
218}
219
220impl From<CustomOptions> for CmdParams {
222 fn from(options: CustomOptions) -> Self {
223 let mut params = Self {
224 arguments: options.arguments.clone(),
226 breadth_first: options.num_jobs.is_none(),
227 dry_run: options.dry_run,
235 echo: options.echo,
236 exit_on_error: options.exit_on_error,
237 force: options.force,
238 keep_going: options.keep_going,
239 num_jobs: options.num_jobs,
240 queries: options.queries.clone(),
241 quiet: options.quiet,
242 tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
243 verbose: options.verbose,
244 word_split: options.word_split,
245 ..Default::default()
246 };
247
248 if params.queries.is_empty() {
250 params.queries.push(constants::DOT.into());
251 }
252
253 params
254 }
255}
256
257impl CmdParams {
258 fn update(&mut self, options: &cli::MainOptions) -> Result<()> {
260 self.quiet |= options.quiet;
261 self.verbose += options.verbose;
262 cmd::initialize_threads_option(self.num_jobs)?;
263
264 Ok(())
265 }
266}
267
268fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
270 let mut cmd = I::command();
271 err.format(&mut cmd)
272}
273
274pub fn main_custom(app_context: &model::ApplicationContext, arguments: &Vec<String>) -> Result<()> {
276 let name = &arguments[0];
278 let garden_custom = format!("garden {name}");
279 let cli = CustomOptions::command().bin_name(garden_custom);
280 let matches = cli.get_matches_from(arguments);
281
282 let mut options = <CustomOptions as FromArgMatches>::from_arg_matches(&matches)
283 .map_err(format_error::<CustomOptions>)?;
284 app_context
285 .get_root_config_mut()
286 .apply_defines(&options.define);
287 app_context
288 .get_root_config_mut()
289 .update_quiet_and_verbose_variables(options.quiet, options.verbose);
290 if !app_context.get_root_config().shell_exit_on_error {
291 options.exit_on_error = false;
292 }
293 if !app_context.get_root_config().shell_word_split {
294 options.word_split = false;
295 }
296
297 if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
298 debug!("jobs: {:?}", options.num_jobs);
299 debug!("command: {}", name);
300 debug!("queries: {:?}", options.queries);
301 debug!("arguments: {:?}", options.arguments);
302 debug!("trees: {:?}", options.trees);
303 }
304
305 let mut params: CmdParams = options.clone().into();
307 params.update(&app_context.options)?;
308 params.commands.push(name.to_string());
309
310 cmds(app_context, ¶ms)
311}
312
313fn cmd(app_context: &model::ApplicationContext, query: &str, params: &CmdParams) -> Result<i32> {
325 let config = app_context.get_root_config_mut();
326 let contexts = query::resolve_trees(app_context, config, None, query);
327 if params.breadth_first {
328 run_cmd_breadth_first(app_context, &contexts, params)
329 } else {
330 run_cmd_depth_first(app_context, &contexts, params)
331 }
332}
333
334fn cmd_parallel(
336 app_context: &model::ApplicationContext,
337 query: &str,
338 params: &CmdParams,
339) -> Result<i32> {
340 let config = app_context.get_root_config_mut();
341 let contexts = query::resolve_trees(app_context, config, None, query);
342 if params.breadth_first {
343 run_cmd_breadth_first_parallel(app_context, &contexts, params)
344 } else {
345 run_cmd_depth_first_parallel(app_context, &contexts, params)
346 }
347}
348
349struct ShellParams {
351 shell_command: Vec<String>,
353 is_shell: bool,
355}
356
357impl ShellParams {
358 fn new(shell: &str, echo: bool, exit_on_error: bool, word_split: bool) -> Self {
359 let mut shell_command = cmd::shlex_split(shell);
360 let basename = path::str_basename(&shell_command[0]);
361 let is_shell = path::is_shell(basename);
363 let is_zsh = matches!(basename, constants::SHELL_ZSH);
364 let is_dash_e = matches!(
366 basename,
367 constants::SHELL_BUN
368 | constants::SHELL_NODE
369 | constants::SHELL_NODEJS
370 | constants::SHELL_PERL
371 | constants::SHELL_RUBY
372 );
373 let is_custom = shell_command.len() > 1;
376 if !is_custom {
377 if word_split && is_zsh {
378 shell_command.push(string!("-o"));
379 shell_command.push(string!("shwordsplit"));
380 }
381 if is_zsh {
382 shell_command.push(string!("+o"));
383 shell_command.push(string!("nomatch"));
384 }
385 if echo && is_shell {
386 shell_command.push(string!("-x"));
387 }
388 if exit_on_error && is_shell {
389 shell_command.push(string!("-e"));
390 }
391 if is_dash_e {
392 shell_command.push(string!("-e"));
393 } else {
394 shell_command.push(string!("-c"));
395 }
396 }
397
398 Self {
399 shell_command,
400 is_shell,
401 }
402 }
403
404 fn from_str(shell: &str) -> Self {
406 let shell_command = cmd::shlex_split(shell);
407 let basename = path::str_basename(&shell_command[0]);
408 let is_shell = path::is_shell(basename);
410
411 Self {
412 shell_command,
413 is_shell,
414 }
415 }
416
417 fn from_context_and_params(
419 app_context: &model::ApplicationContext,
420 params: &CmdParams,
421 ) -> Self {
422 let shell = app_context.get_root_config().shell.as_str();
423 Self::new(shell, params.echo, params.exit_on_error, params.word_split)
424 }
425}
426
427fn get_tree_from_context<'a>(
430 app_context: &'a model::ApplicationContext,
431 context: &model::TreeContext,
432 params: &CmdParams,
433) -> Option<(&'a model::Configuration, &'a model::Tree)> {
434 if !params.tree_pattern.matches(&context.tree) {
436 return None;
437 }
438 let config = match context.config {
440 Some(config_id) => app_context.get_config(config_id),
441 None => app_context.get_root_config(),
442 };
443 let tree = config.trees.get(&context.tree)?;
444 if tree.is_symlink {
445 return None;
446 }
447
448 Some((config, tree))
449}
450
451fn get_command_environment<'a>(
453 app_context: &'a model::ApplicationContext,
454 context: &model::TreeContext,
455 params: &CmdParams,
456) -> Option<(Option<String>, &'a String, model::Environment)> {
457 let (config, tree) = get_tree_from_context(app_context, context, params)?;
458 let Ok(tree_path) = tree.path_as_ref() else {
460 return None;
461 };
462 let env = eval::environment(app_context, config, context);
464 let mut fallback_path = None;
466 let display_options = display::DisplayOptions {
467 branches: config.tree_branches,
468 quiet: params.quiet,
469 verbose: params.verbose,
470 ..std::default::Default::default()
471 };
472 if !display::print_tree(tree, &display_options) {
473 if params.force {
475 fallback_path = Some(config.fallback_execdir_string());
476 } else {
477 return None;
478 }
479 }
480
481 Some((fallback_path, tree_path, env))
482}
483
484fn expand_and_run_command(
486 app_context: &model::ApplicationContext,
487 context: &model::TreeContext,
488 name: &str,
489 path: &str,
490 shell_params: &ShellParams,
491 params: &CmdParams,
492 env: &model::Environment,
493) -> Result<i32, i32> {
494 let mut exit_status = errors::EX_OK;
495 let command_names = cmd::expand_command_names(app_context, context, name);
497 for command_name in &command_names {
498 let cmd_seq_vec = eval::command(app_context, context, command_name);
502 app_context.get_root_config_mut().reset();
503
504 if let Err(cmd_status) = run_cmd_vec(path, shell_params, env, &cmd_seq_vec, params) {
505 exit_status = cmd_status;
506 if !params.keep_going {
507 return Err(cmd_status);
508 }
509 }
510 }
511
512 Ok(exit_status)
513}
514
515fn run_cmd_breadth_first(
517 app_context: &model::ApplicationContext,
518 contexts: &[model::TreeContext],
519 params: &CmdParams,
520) -> Result<i32> {
521 let mut exit_status: i32 = errors::EX_OK;
522 let shell_params = ShellParams::from_context_and_params(app_context, params);
523 for name in ¶ms.commands {
526 for context in contexts {
528 let Some((fallback_path, tree_path, env)) =
529 get_command_environment(app_context, context, params)
530 else {
531 continue;
532 };
533 let path = fallback_path.as_ref().unwrap_or(tree_path);
534 match expand_and_run_command(
535 app_context,
536 context,
537 name,
538 path,
539 &shell_params,
540 params,
541 &env,
542 ) {
543 Ok(cmd_status) => {
544 if cmd_status != errors::EX_OK {
545 exit_status = cmd_status;
546 }
547 }
548 Err(cmd_status) => return Ok(cmd_status),
549 }
550 }
551 }
552
553 Ok(exit_status)
555}
556
557fn run_cmd_breadth_first_parallel(
562 app_context: &model::ApplicationContext,
563 contexts: &[model::TreeContext],
564 params: &CmdParams,
565) -> Result<i32> {
566 let exit_status = atomic::AtomicI32::new(errors::EX_OK);
567 let shell_params = ShellParams::from_context_and_params(app_context, params);
568 params.commands.par_iter().for_each(|name| {
570 let app_context_clone = app_context.clone();
572 let app_context = &app_context_clone;
573 for context in contexts {
575 let Some((fallback_path, tree_path, env)) =
576 get_command_environment(app_context, context, params)
577 else {
578 continue;
579 };
580 let path = fallback_path.as_ref().unwrap_or(tree_path);
581 match expand_and_run_command(
582 app_context,
583 context,
584 name,
585 path,
586 &shell_params,
587 params,
588 &env,
589 ) {
590 Ok(cmd_status) => {
591 if cmd_status != errors::EX_OK {
592 exit_status.store(cmd_status, atomic::Ordering::Release);
593 }
594 }
595 Err(cmd_status) => {
596 exit_status.store(cmd_status, atomic::Ordering::Release);
597 break;
598 }
599 }
600 }
601 });
602
603 Ok(exit_status.load(atomic::Ordering::Acquire))
605}
606
607fn run_cmd_depth_first(
609 app_context: &model::ApplicationContext,
610 contexts: &[model::TreeContext],
611 params: &CmdParams,
612) -> Result<i32> {
613 let mut exit_status: i32 = errors::EX_OK;
614 let shell_params = ShellParams::from_context_and_params(app_context, params);
615 for context in contexts {
617 let Some((fallback_path, tree_path, env)) =
618 get_command_environment(app_context, context, params)
619 else {
620 continue;
621 };
622 let path = fallback_path.as_ref().unwrap_or(tree_path);
623 for name in ¶ms.commands {
625 match expand_and_run_command(
626 app_context,
627 context,
628 name,
629 path,
630 &shell_params,
631 params,
632 &env,
633 ) {
634 Ok(cmd_status) => {
635 if cmd_status != errors::EX_OK {
636 exit_status = cmd_status;
637 }
638 }
639 Err(cmd_status) => return Ok(cmd_status),
640 }
641 }
642 }
643
644 Ok(exit_status)
646}
647
648fn run_cmd_depth_first_parallel(
652 app_context: &model::ApplicationContext,
653 contexts: &[model::TreeContext],
654 params: &CmdParams,
655) -> Result<i32> {
656 let exit_status = atomic::AtomicI32::new(errors::EX_OK);
657 let shell_params = ShellParams::from_context_and_params(app_context, params);
658 contexts.par_iter().for_each(|context| {
660 let app_context_clone = app_context.clone();
662 let app_context = &app_context_clone;
663 let Some((fallback_path, tree_path, env)) =
664 get_command_environment(app_context, context, params)
665 else {
666 return;
667 };
668 let path = fallback_path.as_ref().unwrap_or(tree_path);
669 for name in ¶ms.commands {
671 match expand_and_run_command(
672 app_context,
673 context,
674 name,
675 path,
676 &shell_params,
677 params,
678 &env,
679 ) {
680 Ok(cmd_status) => {
681 if cmd_status != errors::EX_OK {
682 exit_status.store(cmd_status, atomic::Ordering::Release);
683 }
684 }
685 Err(cmd_status) => {
686 exit_status.store(cmd_status, atomic::Ordering::Release);
687 break;
688 }
689 }
690 }
691 });
692
693 Ok(exit_status.load(atomic::Ordering::Acquire))
697}
698
699fn run_cmd_vec(
707 path: &str,
708 shell_params: &ShellParams,
709 env: &model::Environment,
710 cmd_seq_vec: &[Vec<String>],
711 params: &CmdParams,
712) -> Result<(), i32> {
713 let current_exe = cmd::current_exe();
715 let mut exit_status = errors::EX_OK;
716 for cmd_seq in cmd_seq_vec {
717 for cmd_str in cmd_seq {
718 if params.verbose > 1 {
719 eprintln!("{} {}", ":".cyan(), &cmd_str.trim_end().green());
720 }
721 if params.dry_run {
722 continue;
723 }
724 let cmd_shell_params;
726 let (cmd_str, shell_params) = match syntax::split_shebang(cmd_str) {
727 Some((shell_cmd, cmd_str)) => {
728 cmd_shell_params = ShellParams::from_str(shell_cmd);
729 (cmd_str, &cmd_shell_params)
730 }
731 None => (cmd_str.as_str(), shell_params),
732 };
733 let mut exec = subprocess::Exec::cmd(&shell_params.shell_command[0]).cwd(path);
734 exec = exec.args(&shell_params.shell_command[1..]);
735 exec = exec.arg(cmd_str);
736 if shell_params.is_shell {
737 exec = exec.arg(current_exe.as_str());
741 }
742 exec = exec.args(¶ms.arguments);
743 for (k, v) in env {
745 exec = exec.env(k, v);
746 }
747 let status = cmd::status(exec);
750 if status != errors::EX_OK {
751 exit_status = status;
752 if params.exit_on_error {
753 return Err(status);
754 }
755 } else {
756 exit_status = errors::EX_OK;
757 }
758 }
759 if exit_status != errors::EX_OK {
760 return Err(exit_status);
761 }
762 }
763
764 Ok(())
765}
766
767fn cmds(app: &model::ApplicationContext, params: &CmdParams) -> Result<()> {
769 let exit_status = atomic::AtomicI32::new(errors::EX_OK);
770 if params.num_jobs.is_some() {
771 params.queries.par_iter().for_each(|query| {
772 let status = cmd_parallel(&app.clone(), query, params).unwrap_or(errors::EX_IOERR);
773 if status != errors::EX_OK {
774 exit_status.store(status, atomic::Ordering::Release);
775 }
776 });
777 } else {
778 for query in ¶ms.queries {
779 let status = cmd(app, query, params).unwrap_or(errors::EX_IOERR);
780 if status != errors::EX_OK {
781 exit_status.store(status, atomic::Ordering::Release);
782 if !params.keep_going {
783 break;
784 }
785 }
786 }
787 }
788 errors::exit_status_into_result(exit_status.load(atomic::Ordering::Acquire))
790}