garden/cmds/
cmd.rs

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/// Run one or more custom commands over a tree query
13#[derive(Parser, Clone, Debug)]
14#[command(author, about, long_about)]
15pub struct CmdOptions {
16    /// Run a command in all trees before running the next command
17    #[arg(long, short)]
18    breadth_first: bool,
19    /// Perform a trial run without running commands
20    #[arg(long, short = 'N')]
21    dry_run: bool,
22    /// Continue to the next tree when errors occur
23    #[arg(long, short)]
24    keep_going: bool,
25    /// Filter trees by name post-query using a glob pattern
26    #[arg(long, short, default_value = "*")]
27    trees: String,
28    /// Set variables using 'name=value' expressions
29    #[arg(long, short = 'D')]
30    define: Vec<String>,
31    /// Do not pass "-e" to the shell.
32    /// Prevent the "errexit" shell option from being set. By default, the "-e" option
33    /// is passed to the configured shell so that multi-line and multi-statement
34    /// commands halt execution when the first statement with a non-zero exit code is
35    /// encountered. This option has the effect of making multi-line and
36    /// multi-statement commands run all statements even when an earlier statement
37    /// returns a non-zero exit code.
38    #[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
39    exit_on_error: bool,
40    /// Run commands even when the tree does not exist.
41    #[arg(long, short)]
42    force: bool,
43    /// Run commands in parallel using the specified number of jobs.
44    #[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    /// Be quiet
54    #[arg(short, long)]
55    quiet: bool,
56    /// Increase verbosity level (default: 0)
57    #[arg(short, long, action = clap::ArgAction::Count)]
58    verbose: u8,
59    /// Do not pass "-o shwordsplit" to zsh.
60    /// Prevent the "shwordsplit" shell option from being set when using zsh.
61    /// The "-o shwordsplit" option is passed to zsh by default so that unquoted
62    /// $variable expressions are subject to word splitting, just like other shells.
63    /// This option disables this behavior.
64    #[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
65    word_split: bool,
66    /// Tree query for the gardens, groups or trees to execute commands within
67    query: String,
68    /// Custom commands to run over the resolved trees
69    // NOTE: value_terminator may not be needed in future versions of clap_complete.
70    // https://github.com/clap-rs/clap/pull/4612
71    #[arg(required = true, value_terminator = "--")]
72    commands: Vec<String>,
73    /// Arguments to forward to custom commands
74    #[arg(last = true)]
75    arguments: Vec<String>,
76}
77
78/// Run custom garden commands
79#[derive(Parser, Clone, Debug)]
80#[command(bin_name = constants::GARDEN)]
81#[command(styles = clap_cargo::style::CLAP_STYLING)]
82pub struct CustomOptions {
83    /// Set variables using 'name=value' expressions
84    #[arg(long, short = 'D')]
85    define: Vec<String>,
86    /// Perform a trial run without running commands
87    #[arg(long, short = 'N')]
88    dry_run: bool,
89    /// Continue to the next tree when errors occur
90    #[arg(long, short)]
91    keep_going: bool,
92    /// Filter trees by name post-query using a glob pattern
93    #[arg(long, short, default_value = "*")]
94    trees: String,
95    /// Do not pass "-e" to the shell.
96    /// Prevent the "errexit" shell option from being set. By default, the "-e" option
97    /// is passed to the configured shell so that multi-line and multi-statement
98    /// commands halt execution when the first statement with a non-zero exit code is
99    /// encountered. This option has the effect of making multi-line and
100    /// multi-statement commands run all statements even when an earlier statement
101    /// returns a non-zero exit code.
102    #[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
103    exit_on_error: bool,
104    /// Run commands even when the tree does not exist.
105    #[arg(long, short)]
106    force: bool,
107    /// Run commands in parallel using the specified number of jobs.
108    #[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    /// Be quiet
118    #[arg(short, long)]
119    quiet: bool,
120    /// Increase verbosity level (default: 0)
121    #[arg(short, long, action = clap::ArgAction::Count)]
122    verbose: u8,
123    /// Do not pass "-o shwordsplit" to zsh.
124    /// Prevent the "shwordsplit" shell option from being set when using zsh.
125    /// The "-o shwordsplit" option is passed to zsh by default so that unquoted
126    /// $variable expressions are subject to word splitting, just like other shells.
127    /// This option disables this behavior.
128    #[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
129    word_split: bool,
130    /// Tree queries for the Gardens/Groups/Trees to execute commands within
131    // NOTE: value_terminator may not be needed in future versions of clap_complete.
132    // https://github.com/clap-rs/clap/pull/4612
133    #[arg(value_terminator = "--")]
134    queries: Vec<String>,
135    /// Arguments to forward to custom commands
136    #[arg(last = true)]
137    arguments: Vec<String>,
138}
139
140/// Main entry point for `garden cmd <query> <command>...`.
141pub 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, &params)?
166    } else {
167        cmd(app_context, &options.query, &params)?
168    };
169
170    errors::exit_status_into_result(exit_status)
171}
172
173/// CmdParams are used to control the execution of run_cmd_vec().
174///
175/// `garden cmd` and `garden <custom-cmd>` parse command line arguments into CmdParams.
176#[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
195/// Build CmdParams from a CmdOptions struct.
196impl 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
216/// Build CmdParams from a CustomOptions struct
217impl From<CustomOptions> for CmdParams {
218    fn from(options: CustomOptions) -> Self {
219        let mut params = Self {
220            // Add the custom command name to the list of commands. cmds() operates on a vec of commands.
221            arguments: options.arguments.clone(),
222            queries: options.queries.clone(),
223            // Custom commands run breadth-first. The distinction shouldn't make a difference in
224            // practice because "garden <custom-cmd> ..." is only able to run a single command, but we
225            // use breadth-first because it retains the original implementation/behavior from before
226            // --breadth-first was added to "garden cmd" and made opt-in.
227            //
228            // On the other hand, we want "garden <cmd> <query>" to paralellize over all of the
229            // resolved TreeContexts, so we use depth-first traversal when running in paralle.
230            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        // Default to "." when no queries have been specified.
244        if params.queries.is_empty() {
245            params.queries.push(constants::DOT.into());
246        }
247
248        params
249    }
250}
251
252impl CmdParams {
253    /// Apply the opt-level MainOptions onto the CmdParams.
254    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
263/// Format an error
264fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
265    let mut cmd = I::command();
266    err.format(&mut cmd)
267}
268
269/// Main entry point for `garden <command> <query>...`.
270pub fn main_custom(app_context: &model::ApplicationContext, arguments: &Vec<String>) -> Result<()> {
271    // Set the command name to "garden <custom>".
272    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    // Add the custom command name to the list of commands. cmds() operates on a vec of commands.
301    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, &params)
306}
307
308/// Run commands across trees.
309///
310/// Resolve the trees queries down to a set of tree indexes paired with
311/// an optional garden context.
312///
313/// If the names resolve to gardens, each garden is processed independently.
314/// Trees that exist in multiple matching gardens will be processed multiple
315/// times.
316///
317/// If the names resolve to trees, each tree is processed independently
318/// with no garden context.
319fn 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
329/// Run commands in parallel. This is the parallel version of fn cmd().
330fn 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
344/// The configured shell state.
345struct ShellParams {
346    /// The shell string is parsed into command line arguments.
347    shell_command: Vec<String>,
348    /// Is this a shell script runner that requires $0 to be passed as the first argument?
349    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        // Does the shell understand "-e" for errexit?
357        let is_shell = path::is_shell(basename);
358        let is_zsh = matches!(basename, constants::SHELL_ZSH);
359        // Does the shell use "-e <string>" or "-c <string>" to evaluate commands?
360        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        // Is the shell a full-blown command with "-c" and everything defined by the user?
369        // If so we won't manage the custom shell options ourselves.
370        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    /// Return ShellParams from a "#!" shebang line string.
397    fn from_str(shell: &str) -> Self {
398        let shell_command = cmd::shlex_split(shell);
399        let basename = path::str_basename(&shell_command[0]);
400        // Does the shell understand "-e" for errexit?
401        let is_shell = path::is_shell(basename);
402
403        Self {
404            shell_command,
405            is_shell,
406        }
407    }
408
409    /// Retrun ShellParams from an ApplicationContext and CmdParams.
410    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
419/// Check whether the TreeContext is relevant to the current CmdParams.
420/// Returns None when the extracted details are not applicable.
421fn 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    // Skip filtered trees.
427    if !params.tree_pattern.matches(&context.tree) {
428        return None;
429    }
430    // Skip symlink trees.
431    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
443/// Prepare state needed for running commands.
444fn 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    // Trees must have a valid path available.
451    let Ok(tree_path) = tree.path_as_ref() else {
452        return None;
453    };
454    // Evaluate the tree environment
455    let env = eval::environment(app_context, config, context);
456    // Sparse gardens/missing trees are ok -> skip these entries.
457    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        // The "--force" option runs commands in a fallback directory when the tree does not exist.
466        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
476// Expand a command to include its pre-commands and post-commands then execute them  in order.
477fn 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    // Create a sequence of the command names to run including pre and post-commands.
488    let command_names = cmd::expand_command_names(app_context, context, name);
489    for command_name in &command_names {
490        // One command maps to multiple command sequences. When the scope is tree, only the tree's
491        // commands are included.  When the scope includes a garden, its matching commands are
492        // appended to the end.
493        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
507/// Run commands breadth-first. Each command is run in all trees before running the next command.
508fn 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    // Loop over each command, evaluate the tree environment,
516    // and run the command in each context.
517    for name in &params.commands {
518        // One invocation runs multiple commands
519        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    // Return the last non-zero exit status.
546    Ok(exit_status)
547}
548
549/// Run multiple commands in parallel over a single tree query.
550/// All command are run in parallel over all of the matching trees.
551/// Each command invocation operates over every resolved tree, serially, within the scope of
552/// the currently running command.
553fn 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    // Loop over each command, evaluate the tree environment, and run the command in each context.
561    params.commands.par_iter().for_each(|name| {
562        // Create a thread-specific ApplicationContext.
563        let app_context_clone = app_context.clone();
564        let app_context = &app_context_clone;
565        // One invocation runs multiple commands
566        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    // Return the last non-zero exit status.
596    Ok(exit_status.load(atomic::Ordering::Acquire))
597}
598
599/// Run commands depth-first. All commands are run on the current tree before visiting the next tree.
600fn 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    // Loop over each context, evaluate the tree environment and run the command.
608    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        // One invocation runs multiple commands
616        for name in &params.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    // Return the last non-zero exit status.
637    Ok(exit_status)
638}
639
640/// Run commands depth-first in parallel.
641/// All trees are visited concurrently in parallel. Commands are run serially within
642/// the scope of a single tree.
643fn 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    // Loop over each context, evaluate the tree environment and run the command.
651    contexts.par_iter().for_each(|context| {
652        // Create a thread-specific ApplicationContext.
653        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        // One invocation runs multiple commands
662        for name in &params.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    // Return any of the non-zero exit statuses. Which value is returned is
686    // undefined due to the parallel nature of this function. Any of the
687    // non-zero exit status values could have ended up recorded in exit_status.
688    Ok(exit_status.load(atomic::Ordering::Acquire))
689}
690
691/// Run a vector of custom commands using the configured shell.
692/// Parameters:
693/// - path: The current working directory for the command.
694/// - shell: The shell that will be used to run the command strings.
695/// - env: Environment variables to set.
696/// - cmd_seq_vec: Vector of vector of command strings to run.
697/// - arguments: Additional command line arguments available in $1, $2, $N.
698fn 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    // Get the current executable name
706    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            // Create a custom ShellParams when "#!" is used.
717            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                // Shells require $0 to be specified when using -c to run commands in order to make $1 and friends
730                // behave intuitively from within the script. The garden executable's location is
731                // provided in $0 for convenience.
732                exec = exec.arg(current_exe.as_str());
733            }
734            exec = exec.args(&params.arguments);
735            // Update the command environment
736            for (k, v) in env {
737                exec = exec.env(k, v);
738            }
739            // When a command list is used then the return code from the final command
740            // is the one that is returned when --no-errexit is in effect.
741            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
759/// Run cmd() over a Vec of tree queries
760fn 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 &params.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    // Return the last non-zero exit status.
781    errors::exit_status_into_result(exit_status.load(atomic::Ordering::Acquire))
782}