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)]
81pub struct CustomOptions {
82 #[arg(long, short = 'D')]
84 define: Vec<String>,
85 #[arg(long, short = 'N')]
87 dry_run: bool,
88 #[arg(long, short)]
90 keep_going: bool,
91 #[arg(long, short, default_value = "*")]
93 trees: String,
94 #[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
102 exit_on_error: bool,
103 #[arg(long, short)]
105 force: bool,
106 #[arg(
108 long = "jobs",
109 short = 'j',
110 require_equals = false,
111 num_args = 0..=1,
112 default_missing_value = "0",
113 value_name = "JOBS",
114 )]
115 num_jobs: Option<usize>,
116 #[arg(short, long)]
118 quiet: bool,
119 #[arg(short, long, action = clap::ArgAction::Count)]
121 verbose: u8,
122 #[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
128 word_split: bool,
129 #[arg(value_terminator = "--")]
133 queries: Vec<String>,
134 #[arg(last = true)]
136 arguments: Vec<String>,
137}
138
139pub fn main_cmd(app_context: &model::ApplicationContext, options: &mut CmdOptions) -> Result<()> {
141 app_context
142 .get_root_config_mut()
143 .apply_defines(&options.define);
144 app_context
145 .get_root_config_mut()
146 .update_quiet_and_verbose_variables(options.quiet, options.verbose);
147 if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
148 debug!("jobs: {:?}", options.num_jobs);
149 debug!("query: {}", options.query);
150 debug!("commands: {:?}", options.commands);
151 debug!("arguments: {:?}", options.arguments);
152 debug!("trees: {:?}", options.trees);
153 }
154 if !app_context.get_root_config().shell_exit_on_error {
155 options.exit_on_error = false;
156 }
157 if !app_context.get_root_config().shell_word_split {
158 options.word_split = false;
159 }
160 let mut params: CmdParams = options.clone().into();
161 params.update(&app_context.options)?;
162
163 let exit_status = if options.num_jobs.is_some() {
164 cmd_parallel(app_context, &options.query, ¶ms)?
165 } else {
166 cmd(app_context, &options.query, ¶ms)?
167 };
168
169 errors::exit_status_into_result(exit_status)
170}
171
172#[derive(Clone, Debug, Default)]
176pub struct CmdParams {
177 commands: Vec<String>,
178 arguments: Vec<String>,
179 queries: Vec<String>,
180 tree_pattern: glob::Pattern,
181 breadth_first: bool,
182 dry_run: bool,
183 force: bool,
184 keep_going: bool,
185 num_jobs: Option<usize>,
186 #[default(true)]
187 exit_on_error: bool,
188 quiet: bool,
189 verbose: u8,
190 #[default(true)]
191 word_split: bool,
192}
193
194impl From<CmdOptions> for CmdParams {
196 fn from(options: CmdOptions) -> Self {
197 Self {
198 commands: options.commands.clone(),
199 arguments: options.arguments.clone(),
200 breadth_first: options.breadth_first,
201 dry_run: options.dry_run,
202 exit_on_error: options.exit_on_error,
203 force: options.force,
204 keep_going: options.keep_going,
205 num_jobs: options.num_jobs,
206 tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
207 quiet: options.quiet,
208 verbose: options.verbose,
209 word_split: options.word_split,
210 ..Default::default()
211 }
212 }
213}
214
215impl From<CustomOptions> for CmdParams {
217 fn from(options: CustomOptions) -> Self {
218 let mut params = Self {
219 arguments: options.arguments.clone(),
221 queries: options.queries.clone(),
222 breadth_first: options.num_jobs.is_none(),
230 dry_run: options.dry_run,
231 keep_going: options.keep_going,
232 exit_on_error: options.exit_on_error,
233 force: options.force,
234 num_jobs: options.num_jobs,
235 tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
236 quiet: options.quiet,
237 verbose: options.verbose,
238 word_split: options.word_split,
239 ..Default::default()
240 };
241
242 if params.queries.is_empty() {
244 params.queries.push(constants::DOT.into());
245 }
246
247 params
248 }
249}
250
251impl CmdParams {
252 fn update(&mut self, options: &cli::MainOptions) -> Result<()> {
254 self.quiet |= options.quiet;
255 self.verbose += options.verbose;
256 cmd::initialize_threads_option(self.num_jobs)?;
257
258 Ok(())
259 }
260}
261
262fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
264 let mut cmd = I::command();
265 err.format(&mut cmd)
266}
267
268pub fn main_custom(app_context: &model::ApplicationContext, arguments: &Vec<String>) -> Result<()> {
270 let name = &arguments[0];
272 let garden_custom = format!("garden {name}");
273 let cli = CustomOptions::command().bin_name(garden_custom);
274 let matches = cli.get_matches_from(arguments);
275
276 let mut options = <CustomOptions as FromArgMatches>::from_arg_matches(&matches)
277 .map_err(format_error::<CustomOptions>)?;
278 app_context
279 .get_root_config_mut()
280 .apply_defines(&options.define);
281 app_context
282 .get_root_config_mut()
283 .update_quiet_and_verbose_variables(options.quiet, options.verbose);
284 if !app_context.get_root_config().shell_exit_on_error {
285 options.exit_on_error = false;
286 }
287 if !app_context.get_root_config().shell_word_split {
288 options.word_split = false;
289 }
290
291 if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
292 debug!("jobs: {:?}", options.num_jobs);
293 debug!("command: {}", name);
294 debug!("queries: {:?}", options.queries);
295 debug!("arguments: {:?}", options.arguments);
296 debug!("trees: {:?}", options.trees);
297 }
298
299 let mut params: CmdParams = options.clone().into();
301 params.update(&app_context.options)?;
302 params.commands.push(name.to_string());
303
304 cmds(app_context, ¶ms)
305}
306
307fn cmd(app_context: &model::ApplicationContext, query: &str, params: &CmdParams) -> Result<i32> {
319 let config = app_context.get_root_config_mut();
320 let contexts = query::resolve_trees(app_context, config, None, query);
321 if params.breadth_first {
322 run_cmd_breadth_first(app_context, &contexts, params)
323 } else {
324 run_cmd_depth_first(app_context, &contexts, params)
325 }
326}
327
328fn cmd_parallel(
330 app_context: &model::ApplicationContext,
331 query: &str,
332 params: &CmdParams,
333) -> Result<i32> {
334 let config = app_context.get_root_config_mut();
335 let contexts = query::resolve_trees(app_context, config, None, query);
336 if params.breadth_first {
337 run_cmd_breadth_first_parallel(app_context, &contexts, params)
338 } else {
339 run_cmd_depth_first_parallel(app_context, &contexts, params)
340 }
341}
342
343struct ShellParams {
345 shell_command: Vec<String>,
347 is_shell: bool,
349}
350
351impl ShellParams {
352 fn new(shell: &str, exit_on_error: bool, word_split: bool) -> Self {
353 let mut shell_command = cmd::shlex_split(shell);
354 let basename = path::str_basename(&shell_command[0]);
355 let is_shell = path::is_shell(basename);
357 let is_zsh = matches!(basename, constants::SHELL_ZSH);
358 let is_dash_e = matches!(
360 basename,
361 constants::SHELL_BUN
362 | constants::SHELL_NODE
363 | constants::SHELL_NODEJS
364 | constants::SHELL_PERL
365 | constants::SHELL_RUBY
366 );
367 let is_custom = shell_command.len() > 1;
370 if !is_custom {
371 if word_split && is_zsh {
372 shell_command.push(string!("-o"));
373 shell_command.push(string!("shwordsplit"));
374 }
375 if is_zsh {
376 shell_command.push(string!("+o"));
377 shell_command.push(string!("nomatch"));
378 }
379 if exit_on_error && is_shell {
380 shell_command.push(string!("-e"));
381 }
382 if is_dash_e {
383 shell_command.push(string!("-e"));
384 } else {
385 shell_command.push(string!("-c"));
386 }
387 }
388
389 Self {
390 shell_command,
391 is_shell,
392 }
393 }
394
395 fn from_str(shell: &str) -> Self {
397 let shell_command = cmd::shlex_split(shell);
398 let basename = path::str_basename(&shell_command[0]);
399 let is_shell = path::is_shell(basename);
401
402 Self {
403 shell_command,
404 is_shell,
405 }
406 }
407
408 fn from_context_and_params(
410 app_context: &model::ApplicationContext,
411 params: &CmdParams,
412 ) -> Self {
413 let shell = app_context.get_root_config().shell.as_str();
414 Self::new(shell, params.exit_on_error, params.word_split)
415 }
416}
417
418fn get_tree_from_context<'a>(
421 app_context: &'a model::ApplicationContext,
422 context: &model::TreeContext,
423 params: &CmdParams,
424) -> Option<(&'a model::Configuration, &'a model::Tree)> {
425 if !params.tree_pattern.matches(&context.tree) {
427 return None;
428 }
429 let config = match context.config {
431 Some(config_id) => app_context.get_config(config_id),
432 None => app_context.get_root_config(),
433 };
434 let tree = config.trees.get(&context.tree)?;
435 if tree.is_symlink {
436 return None;
437 }
438
439 Some((config, tree))
440}
441
442fn get_command_environment<'a>(
444 app_context: &'a model::ApplicationContext,
445 context: &model::TreeContext,
446 params: &CmdParams,
447) -> Option<(Option<String>, &'a String, model::Environment)> {
448 let (config, tree) = get_tree_from_context(app_context, context, params)?;
449 let Ok(tree_path) = tree.path_as_ref() else {
451 return None;
452 };
453 let env = eval::environment(app_context, config, context);
455 let mut fallback_path = None;
457 if !display::print_tree(
458 tree,
459 config.tree_branches,
460 params.verbose,
461 params.quiet,
462 params.force,
463 ) {
464 if params.force {
466 fallback_path = Some(config.fallback_execdir_string());
467 } else {
468 return None;
469 }
470 }
471
472 Some((fallback_path, tree_path, env))
473}
474
475fn expand_and_run_command(
477 app_context: &model::ApplicationContext,
478 context: &model::TreeContext,
479 name: &str,
480 path: &str,
481 shell_params: &ShellParams,
482 params: &CmdParams,
483 env: &model::Environment,
484) -> Result<i32, i32> {
485 let mut exit_status = errors::EX_OK;
486 let command_names = cmd::expand_command_names(app_context, context, name);
488 for command_name in &command_names {
489 let cmd_seq_vec = eval::command(app_context, context, command_name);
493 app_context.get_root_config_mut().reset();
494
495 if let Err(cmd_status) = run_cmd_vec(path, shell_params, env, &cmd_seq_vec, params) {
496 exit_status = cmd_status;
497 if !params.keep_going {
498 return Err(cmd_status);
499 }
500 }
501 }
502
503 Ok(exit_status)
504}
505
506fn run_cmd_breadth_first(
508 app_context: &model::ApplicationContext,
509 contexts: &[model::TreeContext],
510 params: &CmdParams,
511) -> Result<i32> {
512 let mut exit_status: i32 = errors::EX_OK;
513 let shell_params = ShellParams::from_context_and_params(app_context, params);
514 for name in ¶ms.commands {
517 for context in contexts {
519 let Some((fallback_path, tree_path, env)) =
520 get_command_environment(app_context, context, params)
521 else {
522 continue;
523 };
524 let path = fallback_path.as_ref().unwrap_or(tree_path);
525 match expand_and_run_command(
526 app_context,
527 context,
528 name,
529 path,
530 &shell_params,
531 params,
532 &env,
533 ) {
534 Ok(cmd_status) => {
535 if cmd_status != errors::EX_OK {
536 exit_status = cmd_status;
537 }
538 }
539 Err(cmd_status) => return Ok(cmd_status),
540 }
541 }
542 }
543
544 Ok(exit_status)
546}
547
548fn run_cmd_breadth_first_parallel(
553 app_context: &model::ApplicationContext,
554 contexts: &[model::TreeContext],
555 params: &CmdParams,
556) -> Result<i32> {
557 let exit_status = atomic::AtomicI32::new(errors::EX_OK);
558 let shell_params = ShellParams::from_context_and_params(app_context, params);
559 params.commands.par_iter().for_each(|name| {
561 let app_context_clone = app_context.clone();
563 let app_context = &app_context_clone;
564 for context in contexts {
566 let Some((fallback_path, tree_path, env)) =
567 get_command_environment(app_context, context, params)
568 else {
569 continue;
570 };
571 let path = fallback_path.as_ref().unwrap_or(tree_path);
572 match expand_and_run_command(
573 app_context,
574 context,
575 name,
576 path,
577 &shell_params,
578 params,
579 &env,
580 ) {
581 Ok(cmd_status) => {
582 if cmd_status != errors::EX_OK {
583 exit_status.store(cmd_status, atomic::Ordering::Release);
584 }
585 }
586 Err(cmd_status) => {
587 exit_status.store(cmd_status, atomic::Ordering::Release);
588 break;
589 }
590 }
591 }
592 });
593
594 Ok(exit_status.load(atomic::Ordering::Acquire))
596}
597
598fn run_cmd_depth_first(
600 app_context: &model::ApplicationContext,
601 contexts: &[model::TreeContext],
602 params: &CmdParams,
603) -> Result<i32> {
604 let mut exit_status: i32 = errors::EX_OK;
605 let shell_params = ShellParams::from_context_and_params(app_context, params);
606 for context in contexts {
608 let Some((fallback_path, tree_path, env)) =
609 get_command_environment(app_context, context, params)
610 else {
611 continue;
612 };
613 let path = fallback_path.as_ref().unwrap_or(tree_path);
614 for name in ¶ms.commands {
616 match expand_and_run_command(
617 app_context,
618 context,
619 name,
620 path,
621 &shell_params,
622 params,
623 &env,
624 ) {
625 Ok(cmd_status) => {
626 if cmd_status != errors::EX_OK {
627 exit_status = cmd_status;
628 }
629 }
630 Err(cmd_status) => return Ok(cmd_status),
631 }
632 }
633 }
634
635 Ok(exit_status)
637}
638
639fn run_cmd_depth_first_parallel(
643 app_context: &model::ApplicationContext,
644 contexts: &[model::TreeContext],
645 params: &CmdParams,
646) -> Result<i32> {
647 let exit_status = atomic::AtomicI32::new(errors::EX_OK);
648 let shell_params = ShellParams::from_context_and_params(app_context, params);
649 contexts.par_iter().for_each(|context| {
651 let app_context_clone = app_context.clone();
653 let app_context = &app_context_clone;
654 let Some((fallback_path, tree_path, env)) =
655 get_command_environment(app_context, context, params)
656 else {
657 return;
658 };
659 let path = fallback_path.as_ref().unwrap_or(tree_path);
660 for name in ¶ms.commands {
662 match expand_and_run_command(
663 app_context,
664 context,
665 name,
666 path,
667 &shell_params,
668 params,
669 &env,
670 ) {
671 Ok(cmd_status) => {
672 if cmd_status != errors::EX_OK {
673 exit_status.store(cmd_status, atomic::Ordering::Release);
674 }
675 }
676 Err(cmd_status) => {
677 exit_status.store(cmd_status, atomic::Ordering::Release);
678 break;
679 }
680 }
681 }
682 });
683
684 Ok(exit_status.load(atomic::Ordering::Acquire))
688}
689
690fn run_cmd_vec(
698 path: &str,
699 shell_params: &ShellParams,
700 env: &model::Environment,
701 cmd_seq_vec: &[Vec<String>],
702 params: &CmdParams,
703) -> Result<(), i32> {
704 let current_exe = cmd::current_exe();
706 let mut exit_status = errors::EX_OK;
707 for cmd_seq in cmd_seq_vec {
708 for cmd_str in cmd_seq {
709 if params.verbose > 1 {
710 eprintln!("{} {}", ":".cyan(), &cmd_str.trim_end().green());
711 }
712 if params.dry_run {
713 continue;
714 }
715 let cmd_shell_params;
717 let (cmd_str, shell_params) = match syntax::split_shebang(cmd_str) {
718 Some((shell_cmd, cmd_str)) => {
719 cmd_shell_params = ShellParams::from_str(shell_cmd);
720 (cmd_str, &cmd_shell_params)
721 }
722 None => (cmd_str.as_str(), shell_params),
723 };
724 let mut exec = subprocess::Exec::cmd(&shell_params.shell_command[0]).cwd(path);
725 exec = exec.args(&shell_params.shell_command[1..]);
726 exec = exec.arg(cmd_str);
727 if shell_params.is_shell {
728 exec = exec.arg(current_exe.as_str());
732 }
733 exec = exec.args(¶ms.arguments);
734 for (k, v) in env {
736 exec = exec.env(k, v);
737 }
738 let status = cmd::status(exec);
741 if status != errors::EX_OK {
742 exit_status = status;
743 if params.exit_on_error {
744 return Err(status);
745 }
746 } else {
747 exit_status = errors::EX_OK;
748 }
749 }
750 if exit_status != errors::EX_OK {
751 return Err(exit_status);
752 }
753 }
754
755 Ok(())
756}
757
758fn cmds(app: &model::ApplicationContext, params: &CmdParams) -> Result<()> {
760 let exit_status = atomic::AtomicI32::new(errors::EX_OK);
761 if params.num_jobs.is_some() {
762 params.queries.par_iter().for_each(|query| {
763 let status = cmd_parallel(&app.clone(), query, params).unwrap_or(errors::EX_IOERR);
764 if status != errors::EX_OK {
765 exit_status.store(status, atomic::Ordering::Release);
766 }
767 });
768 } else {
769 for query in ¶ms.queries {
770 let status = cmd(app, query, params).unwrap_or(errors::EX_IOERR);
771 if status != errors::EX_OK {
772 exit_status.store(status, atomic::Ordering::Release);
773 if !params.keep_going {
774 break;
775 }
776 }
777 }
778 }
779 errors::exit_status_into_result(exit_status.load(atomic::Ordering::Acquire))
781}