Skip to main content

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    #[arg(required = true)]
70    commands: Vec<String>,
71    /// Arguments to forward to custom commands
72    #[arg(last = true)]
73    arguments: Vec<String>,
74}
75
76/// Run custom garden commands
77#[derive(Parser, Clone, Debug)]
78#[command(bin_name = constants::GARDEN)]
79#[command(styles = clap_cargo::style::CLAP_STYLING)]
80pub struct CustomOptions {
81    /// Set variables using 'name=value' expressions
82    #[arg(long, short = 'D')]
83    define: Vec<String>,
84    /// Perform a trial run without running commands
85    #[arg(long, short = 'N')]
86    dry_run: bool,
87    /// Continue to the next tree when errors occur
88    #[arg(long, short)]
89    keep_going: bool,
90    /// Filter trees by name post-query using a glob pattern
91    #[arg(long, short, default_value = "*")]
92    trees: String,
93    /// Do not pass "-e" to the shell.
94    /// Prevent the "errexit" shell option from being set. By default, the "-e" option
95    /// is passed to the configured shell so that multi-line and multi-statement
96    /// commands halt execution when the first statement with a non-zero exit code is
97    /// encountered. This option has the effect of making multi-line and
98    /// multi-statement commands run all statements even when an earlier statement
99    /// returns a non-zero exit code.
100    #[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
101    exit_on_error: bool,
102    /// Run commands even when the tree does not exist.
103    #[arg(long, short)]
104    force: bool,
105    /// Run commands in parallel using the specified number of jobs.
106    #[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    /// Be quiet
116    #[arg(short, long)]
117    quiet: bool,
118    /// Increase verbosity level (default: 0)
119    #[arg(short, long, action = clap::ArgAction::Count)]
120    verbose: u8,
121    /// Do not pass "-o shwordsplit" to zsh.
122    /// Prevent the "shwordsplit" shell option from being set when using zsh.
123    /// The "-o shwordsplit" option is passed to zsh by default so that unquoted
124    /// $variable expressions are subject to word splitting, just like other shells.
125    /// This option disables this behavior.
126    #[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
127    word_split: bool,
128    /// Tree queries for the Gardens/Groups/Trees to execute commands within
129    queries: Vec<String>,
130    /// Arguments to forward to custom commands
131    #[arg(last = true)]
132    arguments: Vec<String>,
133}
134
135/// Main entry point for `garden cmd <query> <command>...`.
136pub 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, &params)?
161    } else {
162        cmd(app_context, &options.query, &params)?
163    };
164
165    errors::exit_status_into_result(exit_status)
166}
167
168/// CmdParams are used to control the execution of run_cmd_vec().
169///
170/// `garden cmd` and `garden <custom-cmd>` parse command line arguments into CmdParams.
171#[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
190/// Build CmdParams from a CmdOptions struct.
191impl 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
211/// Build CmdParams from a CustomOptions struct
212impl From<CustomOptions> for CmdParams {
213    fn from(options: CustomOptions) -> Self {
214        let mut params = Self {
215            // Add the custom command name to the list of commands. cmds() operates on a vec of commands.
216            arguments: options.arguments.clone(),
217            breadth_first: options.num_jobs.is_none(),
218            // Custom commands run breadth-first. The distinction shouldn't make a difference in
219            // practice because "garden <custom-cmd> ..." is only able to run a single command, but we
220            // use breadth-first because it retains the original implementation/behavior from before
221            // --breadth-first was added to "garden cmd" and made opt-in.
222            //
223            // On the other hand, we want "garden <cmd> <query>" to paralellize over all of the
224            // resolved TreeContexts, so we use depth-first traversal when running in paralle.
225            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        // Default to "." when no queries have been specified.
239        if params.queries.is_empty() {
240            params.queries.push(constants::DOT.into());
241        }
242
243        params
244    }
245}
246
247impl CmdParams {
248    /// Apply the opt-level MainOptions onto the CmdParams.
249    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
258/// Format an error
259fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
260    let mut cmd = I::command();
261    err.format(&mut cmd)
262}
263
264/// Main entry point for `garden <command> <query>...`.
265pub fn main_custom(app_context: &model::ApplicationContext, arguments: &Vec<String>) -> Result<()> {
266    // Set the command name to "garden <custom>".
267    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    // Add the custom command name to the list of commands. cmds() operates on a vec of commands.
296    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, &params)
301}
302
303/// Run commands across trees.
304///
305/// Resolve the trees queries down to a set of tree indexes paired with
306/// an optional garden context.
307///
308/// If the names resolve to gardens, each garden is processed independently.
309/// Trees that exist in multiple matching gardens will be processed multiple
310/// times.
311///
312/// If the names resolve to trees, each tree is processed independently
313/// with no garden context.
314fn 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
324/// Run commands in parallel. This is the parallel version of fn cmd().
325fn 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
339/// The configured shell state.
340struct ShellParams {
341    /// The shell string is parsed into command line arguments.
342    shell_command: Vec<String>,
343    /// Is this a shell script runner that requires $0 to be passed as the first argument?
344    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        // Does the shell understand "-e" for errexit?
352        let is_shell = path::is_shell(basename);
353        let is_zsh = matches!(basename, constants::SHELL_ZSH);
354        // Does the shell use "-e <string>" or "-c <string>" to evaluate commands?
355        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        // Is the shell a full-blown command with "-c" and everything defined by the user?
364        // If so we won't manage the custom shell options ourselves.
365        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    /// Return ShellParams from a "#!" shebang line string.
392    fn from_str(shell: &str) -> Self {
393        let shell_command = cmd::shlex_split(shell);
394        let basename = path::str_basename(&shell_command[0]);
395        // Does the shell understand "-e" for errexit?
396        let is_shell = path::is_shell(basename);
397
398        Self {
399            shell_command,
400            is_shell,
401        }
402    }
403
404    /// Retrun ShellParams from an ApplicationContext and CmdParams.
405    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
414/// Check whether the TreeContext is relevant to the current CmdParams.
415/// Returns None when the extracted details are not applicable.
416fn 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    // Skip filtered trees.
422    if !params.tree_pattern.matches(&context.tree) {
423        return None;
424    }
425    // Skip symlink trees.
426    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
438/// Prepare state needed for running commands.
439fn 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    // Trees must have a valid path available.
446    let Ok(tree_path) = tree.path_as_ref() else {
447        return None;
448    };
449    // Evaluate the tree environment
450    let env = eval::environment(app_context, config, context);
451    // Sparse gardens/missing trees are ok -> skip these entries.
452    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        // The "--force" option runs commands in a fallback directory when the tree does not exist.
461        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
471// Expand a command to include its pre-commands and post-commands then execute them  in order.
472fn 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    // Create a sequence of the command names to run including pre and post-commands.
483    let command_names = cmd::expand_command_names(app_context, context, name);
484    for command_name in &command_names {
485        // One command maps to multiple command sequences. When the scope is tree, only the tree's
486        // commands are included.  When the scope includes a garden, its matching commands are
487        // appended to the end.
488        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
502/// Run commands breadth-first. Each command is run in all trees before running the next command.
503fn 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    // Loop over each command, evaluate the tree environment,
511    // and run the command in each context.
512    for name in &params.commands {
513        // One invocation runs multiple commands
514        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    // Return the last non-zero exit status.
541    Ok(exit_status)
542}
543
544/// Run multiple commands in parallel over a single tree query.
545/// All command are run in parallel over all of the matching trees.
546/// Each command invocation operates over every resolved tree, serially, within the scope of
547/// the currently running command.
548fn 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    // Loop over each command, evaluate the tree environment, and run the command in each context.
556    params.commands.par_iter().for_each(|name| {
557        // Create a thread-specific ApplicationContext.
558        let app_context_clone = app_context.clone();
559        let app_context = &app_context_clone;
560        // One invocation runs multiple commands
561        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    // Return the last non-zero exit status.
591    Ok(exit_status.load(atomic::Ordering::Acquire))
592}
593
594/// Run commands depth-first. All commands are run on the current tree before visiting the next tree.
595fn 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    // Loop over each context, evaluate the tree environment and run the command.
603    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        // One invocation runs multiple commands
611        for name in &params.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    // Return the last non-zero exit status.
632    Ok(exit_status)
633}
634
635/// Run commands depth-first in parallel.
636/// All trees are visited concurrently in parallel. Commands are run serially within
637/// the scope of a single tree.
638fn 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    // Loop over each context, evaluate the tree environment and run the command.
646    contexts.par_iter().for_each(|context| {
647        // Create a thread-specific ApplicationContext.
648        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        // One invocation runs multiple commands
657        for name in &params.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    // Return any of the non-zero exit statuses. Which value is returned is
681    // undefined due to the parallel nature of this function. Any of the
682    // non-zero exit status values could have ended up recorded in exit_status.
683    Ok(exit_status.load(atomic::Ordering::Acquire))
684}
685
686/// Run a vector of custom commands using the configured shell.
687/// Parameters:
688/// - path: The current working directory for the command.
689/// - shell: The shell that will be used to run the command strings.
690/// - env: Environment variables to set.
691/// - cmd_seq_vec: Vector of vector of command strings to run.
692/// - arguments: Additional command line arguments available in $1, $2, $N.
693fn 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    // Get the current executable name
701    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            // Create a custom ShellParams when "#!" is used.
712            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                // Shells require $0 to be specified when using -c to run commands in order to make $1 and friends
725                // behave intuitively from within the script. The garden executable's location is
726                // provided in $0 for convenience.
727                exec = exec.arg(current_exe.as_str());
728            }
729            exec = exec.args(&params.arguments);
730            // Update the command environment
731            for (k, v) in env {
732                exec = exec.env(k, v);
733            }
734            // When a command list is used then the return code from the final command
735            // is the one that is returned when --no-errexit is in effect.
736            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
754/// Run cmd() over a Vec of tree queries
755fn 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 &params.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    // Return the last non-zero exit status.
776    errors::exit_status_into_result(exit_status.load(atomic::Ordering::Acquire))
777}