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