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)]
70 commands: Vec<String>,
71 #[arg(last = true)]
73 arguments: Vec<String>,
74}
75
76#[derive(Parser, Clone, Debug)]
78#[command(bin_name = constants::GARDEN)]
79#[command(styles = clap_cargo::style::CLAP_STYLING)]
80pub struct CustomOptions {
81 #[arg(long, short = 'D')]
83 define: Vec<String>,
84 #[arg(long, short = 'N')]
86 dry_run: bool,
87 #[arg(long, short)]
89 keep_going: bool,
90 #[arg(long, short, default_value = "*")]
92 trees: String,
93 #[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
101 exit_on_error: bool,
102 #[arg(long, short)]
104 force: bool,
105 #[arg(
107 long = "jobs",
108 short = 'j',
109 require_equals = false,
110 num_args = 0..=1,
111 default_missing_value = "0",
112 value_name = "JOBS",
113 )]
114 num_jobs: Option<usize>,
115 #[arg(short, long)]
117 quiet: bool,
118 #[arg(short, long, action = clap::ArgAction::Count)]
120 verbose: u8,
121 #[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
127 word_split: bool,
128 queries: Vec<String>,
130 #[arg(last = true)]
132 arguments: Vec<String>,
133}
134
135pub fn main_cmd(app_context: &model::ApplicationContext, options: &mut CmdOptions) -> Result<()> {
137 app_context
138 .get_root_config_mut()
139 .apply_defines(&options.define);
140 app_context
141 .get_root_config_mut()
142 .update_quiet_and_verbose_variables(options.quiet, options.verbose);
143 if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
144 debug!("jobs: {:?}", options.num_jobs);
145 debug!("query: {}", options.query);
146 debug!("commands: {:?}", options.commands);
147 debug!("arguments: {:?}", options.arguments);
148 debug!("trees: {:?}", options.trees);
149 }
150 if !app_context.get_root_config().shell_exit_on_error {
151 options.exit_on_error = false;
152 }
153 if !app_context.get_root_config().shell_word_split {
154 options.word_split = false;
155 }
156 let mut params: CmdParams = options.clone().into();
157 params.update(&app_context.options)?;
158
159 let exit_status = if options.num_jobs.is_some() {
160 cmd_parallel(app_context, &options.query, ¶ms)?
161 } else {
162 cmd(app_context, &options.query, ¶ms)?
163 };
164
165 errors::exit_status_into_result(exit_status)
166}
167
168#[derive(Clone, Debug, Default)]
172pub struct CmdParams {
173 commands: Vec<String>,
174 arguments: Vec<String>,
175 queries: Vec<String>,
176 tree_pattern: glob::Pattern,
177 breadth_first: bool,
178 dry_run: bool,
179 force: bool,
180 keep_going: bool,
181 num_jobs: Option<usize>,
182 #[default(true)]
183 exit_on_error: bool,
184 quiet: bool,
185 verbose: u8,
186 #[default(true)]
187 word_split: bool,
188}
189
190impl From<CmdOptions> for CmdParams {
192 fn from(options: CmdOptions) -> Self {
193 Self {
194 arguments: options.arguments.clone(),
195 breadth_first: options.breadth_first,
196 commands: options.commands.clone(),
197 dry_run: options.dry_run,
198 exit_on_error: options.exit_on_error,
199 force: options.force,
200 keep_going: options.keep_going,
201 num_jobs: options.num_jobs,
202 quiet: options.quiet,
203 tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
204 verbose: options.verbose,
205 word_split: options.word_split,
206 ..Default::default()
207 }
208 }
209}
210
211impl From<CustomOptions> for CmdParams {
213 fn from(options: CustomOptions) -> Self {
214 let mut params = Self {
215 arguments: options.arguments.clone(),
217 breadth_first: options.num_jobs.is_none(),
218 dry_run: options.dry_run,
226 exit_on_error: options.exit_on_error,
227 force: options.force,
228 keep_going: options.keep_going,
229 num_jobs: options.num_jobs,
230 queries: options.queries.clone(),
231 quiet: options.quiet,
232 tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
233 verbose: options.verbose,
234 word_split: options.word_split,
235 ..Default::default()
236 };
237
238 if params.queries.is_empty() {
240 params.queries.push(constants::DOT.into());
241 }
242
243 params
244 }
245}
246
247impl CmdParams {
248 fn update(&mut self, options: &cli::MainOptions) -> Result<()> {
250 self.quiet |= options.quiet;
251 self.verbose += options.verbose;
252 cmd::initialize_threads_option(self.num_jobs)?;
253
254 Ok(())
255 }
256}
257
258fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
260 let mut cmd = I::command();
261 err.format(&mut cmd)
262}
263
264pub fn main_custom(app_context: &model::ApplicationContext, arguments: &Vec<String>) -> Result<()> {
266 let name = &arguments[0];
268 let garden_custom = format!("garden {name}");
269 let cli = CustomOptions::command().bin_name(garden_custom);
270 let matches = cli.get_matches_from(arguments);
271
272 let mut options = <CustomOptions as FromArgMatches>::from_arg_matches(&matches)
273 .map_err(format_error::<CustomOptions>)?;
274 app_context
275 .get_root_config_mut()
276 .apply_defines(&options.define);
277 app_context
278 .get_root_config_mut()
279 .update_quiet_and_verbose_variables(options.quiet, options.verbose);
280 if !app_context.get_root_config().shell_exit_on_error {
281 options.exit_on_error = false;
282 }
283 if !app_context.get_root_config().shell_word_split {
284 options.word_split = false;
285 }
286
287 if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
288 debug!("jobs: {:?}", options.num_jobs);
289 debug!("command: {}", name);
290 debug!("queries: {:?}", options.queries);
291 debug!("arguments: {:?}", options.arguments);
292 debug!("trees: {:?}", options.trees);
293 }
294
295 let mut params: CmdParams = options.clone().into();
297 params.update(&app_context.options)?;
298 params.commands.push(name.to_string());
299
300 cmds(app_context, ¶ms)
301}
302
303fn cmd(app_context: &model::ApplicationContext, query: &str, params: &CmdParams) -> Result<i32> {
315 let config = app_context.get_root_config_mut();
316 let contexts = query::resolve_trees(app_context, config, None, query);
317 if params.breadth_first {
318 run_cmd_breadth_first(app_context, &contexts, params)
319 } else {
320 run_cmd_depth_first(app_context, &contexts, params)
321 }
322}
323
324fn cmd_parallel(
326 app_context: &model::ApplicationContext,
327 query: &str,
328 params: &CmdParams,
329) -> Result<i32> {
330 let config = app_context.get_root_config_mut();
331 let contexts = query::resolve_trees(app_context, config, None, query);
332 if params.breadth_first {
333 run_cmd_breadth_first_parallel(app_context, &contexts, params)
334 } else {
335 run_cmd_depth_first_parallel(app_context, &contexts, params)
336 }
337}
338
339struct ShellParams {
341 shell_command: Vec<String>,
343 is_shell: bool,
345}
346
347impl ShellParams {
348 fn new(shell: &str, exit_on_error: bool, word_split: bool) -> Self {
349 let mut shell_command = cmd::shlex_split(shell);
350 let basename = path::str_basename(&shell_command[0]);
351 let is_shell = path::is_shell(basename);
353 let is_zsh = matches!(basename, constants::SHELL_ZSH);
354 let is_dash_e = matches!(
356 basename,
357 constants::SHELL_BUN
358 | constants::SHELL_NODE
359 | constants::SHELL_NODEJS
360 | constants::SHELL_PERL
361 | constants::SHELL_RUBY
362 );
363 let is_custom = shell_command.len() > 1;
366 if !is_custom {
367 if word_split && is_zsh {
368 shell_command.push(string!("-o"));
369 shell_command.push(string!("shwordsplit"));
370 }
371 if is_zsh {
372 shell_command.push(string!("+o"));
373 shell_command.push(string!("nomatch"));
374 }
375 if exit_on_error && is_shell {
376 shell_command.push(string!("-e"));
377 }
378 if is_dash_e {
379 shell_command.push(string!("-e"));
380 } else {
381 shell_command.push(string!("-c"));
382 }
383 }
384
385 Self {
386 shell_command,
387 is_shell,
388 }
389 }
390
391 fn from_str(shell: &str) -> Self {
393 let shell_command = cmd::shlex_split(shell);
394 let basename = path::str_basename(&shell_command[0]);
395 let is_shell = path::is_shell(basename);
397
398 Self {
399 shell_command,
400 is_shell,
401 }
402 }
403
404 fn from_context_and_params(
406 app_context: &model::ApplicationContext,
407 params: &CmdParams,
408 ) -> Self {
409 let shell = app_context.get_root_config().shell.as_str();
410 Self::new(shell, params.exit_on_error, params.word_split)
411 }
412}
413
414fn get_tree_from_context<'a>(
417 app_context: &'a model::ApplicationContext,
418 context: &model::TreeContext,
419 params: &CmdParams,
420) -> Option<(&'a model::Configuration, &'a model::Tree)> {
421 if !params.tree_pattern.matches(&context.tree) {
423 return None;
424 }
425 let config = match context.config {
427 Some(config_id) => app_context.get_config(config_id),
428 None => app_context.get_root_config(),
429 };
430 let tree = config.trees.get(&context.tree)?;
431 if tree.is_symlink {
432 return None;
433 }
434
435 Some((config, tree))
436}
437
438fn get_command_environment<'a>(
440 app_context: &'a model::ApplicationContext,
441 context: &model::TreeContext,
442 params: &CmdParams,
443) -> Option<(Option<String>, &'a String, model::Environment)> {
444 let (config, tree) = get_tree_from_context(app_context, context, params)?;
445 let Ok(tree_path) = tree.path_as_ref() else {
447 return None;
448 };
449 let env = eval::environment(app_context, config, context);
451 let mut fallback_path = None;
453 let display_options = display::DisplayOptions {
454 branches: config.tree_branches,
455 quiet: params.quiet,
456 verbose: params.verbose,
457 ..std::default::Default::default()
458 };
459 if !display::print_tree(tree, &display_options) {
460 if params.force {
462 fallback_path = Some(config.fallback_execdir_string());
463 } else {
464 return None;
465 }
466 }
467
468 Some((fallback_path, tree_path, env))
469}
470
471fn expand_and_run_command(
473 app_context: &model::ApplicationContext,
474 context: &model::TreeContext,
475 name: &str,
476 path: &str,
477 shell_params: &ShellParams,
478 params: &CmdParams,
479 env: &model::Environment,
480) -> Result<i32, i32> {
481 let mut exit_status = errors::EX_OK;
482 let command_names = cmd::expand_command_names(app_context, context, name);
484 for command_name in &command_names {
485 let cmd_seq_vec = eval::command(app_context, context, command_name);
489 app_context.get_root_config_mut().reset();
490
491 if let Err(cmd_status) = run_cmd_vec(path, shell_params, env, &cmd_seq_vec, params) {
492 exit_status = cmd_status;
493 if !params.keep_going {
494 return Err(cmd_status);
495 }
496 }
497 }
498
499 Ok(exit_status)
500}
501
502fn run_cmd_breadth_first(
504 app_context: &model::ApplicationContext,
505 contexts: &[model::TreeContext],
506 params: &CmdParams,
507) -> Result<i32> {
508 let mut exit_status: i32 = errors::EX_OK;
509 let shell_params = ShellParams::from_context_and_params(app_context, params);
510 for name in ¶ms.commands {
513 for context in contexts {
515 let Some((fallback_path, tree_path, env)) =
516 get_command_environment(app_context, context, params)
517 else {
518 continue;
519 };
520 let path = fallback_path.as_ref().unwrap_or(tree_path);
521 match expand_and_run_command(
522 app_context,
523 context,
524 name,
525 path,
526 &shell_params,
527 params,
528 &env,
529 ) {
530 Ok(cmd_status) => {
531 if cmd_status != errors::EX_OK {
532 exit_status = cmd_status;
533 }
534 }
535 Err(cmd_status) => return Ok(cmd_status),
536 }
537 }
538 }
539
540 Ok(exit_status)
542}
543
544fn run_cmd_breadth_first_parallel(
549 app_context: &model::ApplicationContext,
550 contexts: &[model::TreeContext],
551 params: &CmdParams,
552) -> Result<i32> {
553 let exit_status = atomic::AtomicI32::new(errors::EX_OK);
554 let shell_params = ShellParams::from_context_and_params(app_context, params);
555 params.commands.par_iter().for_each(|name| {
557 let app_context_clone = app_context.clone();
559 let app_context = &app_context_clone;
560 for context in contexts {
562 let Some((fallback_path, tree_path, env)) =
563 get_command_environment(app_context, context, params)
564 else {
565 continue;
566 };
567 let path = fallback_path.as_ref().unwrap_or(tree_path);
568 match expand_and_run_command(
569 app_context,
570 context,
571 name,
572 path,
573 &shell_params,
574 params,
575 &env,
576 ) {
577 Ok(cmd_status) => {
578 if cmd_status != errors::EX_OK {
579 exit_status.store(cmd_status, atomic::Ordering::Release);
580 }
581 }
582 Err(cmd_status) => {
583 exit_status.store(cmd_status, atomic::Ordering::Release);
584 break;
585 }
586 }
587 }
588 });
589
590 Ok(exit_status.load(atomic::Ordering::Acquire))
592}
593
594fn run_cmd_depth_first(
596 app_context: &model::ApplicationContext,
597 contexts: &[model::TreeContext],
598 params: &CmdParams,
599) -> Result<i32> {
600 let mut exit_status: i32 = errors::EX_OK;
601 let shell_params = ShellParams::from_context_and_params(app_context, params);
602 for context in contexts {
604 let Some((fallback_path, tree_path, env)) =
605 get_command_environment(app_context, context, params)
606 else {
607 continue;
608 };
609 let path = fallback_path.as_ref().unwrap_or(tree_path);
610 for name in ¶ms.commands {
612 match expand_and_run_command(
613 app_context,
614 context,
615 name,
616 path,
617 &shell_params,
618 params,
619 &env,
620 ) {
621 Ok(cmd_status) => {
622 if cmd_status != errors::EX_OK {
623 exit_status = cmd_status;
624 }
625 }
626 Err(cmd_status) => return Ok(cmd_status),
627 }
628 }
629 }
630
631 Ok(exit_status)
633}
634
635fn run_cmd_depth_first_parallel(
639 app_context: &model::ApplicationContext,
640 contexts: &[model::TreeContext],
641 params: &CmdParams,
642) -> Result<i32> {
643 let exit_status = atomic::AtomicI32::new(errors::EX_OK);
644 let shell_params = ShellParams::from_context_and_params(app_context, params);
645 contexts.par_iter().for_each(|context| {
647 let app_context_clone = app_context.clone();
649 let app_context = &app_context_clone;
650 let Some((fallback_path, tree_path, env)) =
651 get_command_environment(app_context, context, params)
652 else {
653 return;
654 };
655 let path = fallback_path.as_ref().unwrap_or(tree_path);
656 for name in ¶ms.commands {
658 match expand_and_run_command(
659 app_context,
660 context,
661 name,
662 path,
663 &shell_params,
664 params,
665 &env,
666 ) {
667 Ok(cmd_status) => {
668 if cmd_status != errors::EX_OK {
669 exit_status.store(cmd_status, atomic::Ordering::Release);
670 }
671 }
672 Err(cmd_status) => {
673 exit_status.store(cmd_status, atomic::Ordering::Release);
674 break;
675 }
676 }
677 }
678 });
679
680 Ok(exit_status.load(atomic::Ordering::Acquire))
684}
685
686fn run_cmd_vec(
694 path: &str,
695 shell_params: &ShellParams,
696 env: &model::Environment,
697 cmd_seq_vec: &[Vec<String>],
698 params: &CmdParams,
699) -> Result<(), i32> {
700 let current_exe = cmd::current_exe();
702 let mut exit_status = errors::EX_OK;
703 for cmd_seq in cmd_seq_vec {
704 for cmd_str in cmd_seq {
705 if params.verbose > 1 {
706 eprintln!("{} {}", ":".cyan(), &cmd_str.trim_end().green());
707 }
708 if params.dry_run {
709 continue;
710 }
711 let cmd_shell_params;
713 let (cmd_str, shell_params) = match syntax::split_shebang(cmd_str) {
714 Some((shell_cmd, cmd_str)) => {
715 cmd_shell_params = ShellParams::from_str(shell_cmd);
716 (cmd_str, &cmd_shell_params)
717 }
718 None => (cmd_str.as_str(), shell_params),
719 };
720 let mut exec = subprocess::Exec::cmd(&shell_params.shell_command[0]).cwd(path);
721 exec = exec.args(&shell_params.shell_command[1..]);
722 exec = exec.arg(cmd_str);
723 if shell_params.is_shell {
724 exec = exec.arg(current_exe.as_str());
728 }
729 exec = exec.args(¶ms.arguments);
730 for (k, v) in env {
732 exec = exec.env(k, v);
733 }
734 let status = cmd::status(exec);
737 if status != errors::EX_OK {
738 exit_status = status;
739 if params.exit_on_error {
740 return Err(status);
741 }
742 } else {
743 exit_status = errors::EX_OK;
744 }
745 }
746 if exit_status != errors::EX_OK {
747 return Err(exit_status);
748 }
749 }
750
751 Ok(())
752}
753
754fn cmds(app: &model::ApplicationContext, params: &CmdParams) -> Result<()> {
756 let exit_status = atomic::AtomicI32::new(errors::EX_OK);
757 if params.num_jobs.is_some() {
758 params.queries.par_iter().for_each(|query| {
759 let status = cmd_parallel(&app.clone(), query, params).unwrap_or(errors::EX_IOERR);
760 if status != errors::EX_OK {
761 exit_status.store(status, atomic::Ordering::Release);
762 }
763 });
764 } else {
765 for query in ¶ms.queries {
766 let status = cmd(app, query, params).unwrap_or(errors::EX_IOERR);
767 if status != errors::EX_OK {
768 exit_status.store(status, atomic::Ordering::Release);
769 if !params.keep_going {
770 break;
771 }
772 }
773 }
774 }
775 errors::exit_status_into_result(exit_status.load(atomic::Ordering::Acquire))
777}