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    /// Enable echo mode by passing "-x" to the shell
60    #[arg(short = 'x', long)]
61    echo: bool,
62    /// Do not pass "-o shwordsplit" to zsh.
63    /// Prevent the "shwordsplit" shell option from being set when using zsh.
64    /// The "-o shwordsplit" option is passed to zsh by default so that unquoted
65    /// $variable expressions are subject to word splitting, just like other shells.
66    /// This option disables this behavior.
67    #[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
68    word_split: bool,
69    /// Tree query for the gardens, groups or trees to execute commands within
70    query: String,
71    /// Custom commands to run over the resolved trees
72    #[arg(required = true, value_terminator = "--")]
73    commands: Vec<String>,
74    /// Arguments to forward to custom commands
75    #[arg(last = true)]
76    arguments: Vec<String>,
77}
78
79/// Run custom garden commands
80#[derive(Parser, Clone, Debug)]
81#[command(bin_name = constants::GARDEN)]
82#[command(styles = clap_cargo::style::CLAP_STYLING)]
83pub struct CustomOptions {
84    /// Set variables using 'name=value' expressions
85    #[arg(long, short = 'D')]
86    define: Vec<String>,
87    /// Perform a trial run without running commands
88    #[arg(long, short = 'N')]
89    dry_run: bool,
90    /// Continue to the next tree when errors occur
91    #[arg(long, short)]
92    keep_going: bool,
93    /// Filter trees by name post-query using a glob pattern
94    #[arg(long, short, default_value = "*")]
95    trees: String,
96    /// Do not pass "-e" to the shell.
97    /// Prevent the "errexit" shell option from being set. By default, the "-e" option
98    /// is passed to the configured shell so that multi-line and multi-statement
99    /// commands halt execution when the first statement with a non-zero exit code is
100    /// encountered. This option has the effect of making multi-line and
101    /// multi-statement commands run all statements even when an earlier statement
102    /// returns a non-zero exit code.
103    #[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
104    exit_on_error: bool,
105    /// Run commands even when the tree does not exist.
106    #[arg(long, short)]
107    force: bool,
108    /// Run commands in parallel using the specified number of jobs.
109    #[arg(
110        long = "jobs",
111        short = 'j',
112        require_equals = false,
113        num_args = 0..=1,
114        default_missing_value = "0",
115        value_name = "JOBS",
116    )]
117    num_jobs: Option<usize>,
118    /// Be quiet
119    #[arg(short, long)]
120    quiet: bool,
121    /// Increase verbosity level (default: 0)
122    #[arg(short, long, action = clap::ArgAction::Count)]
123    verbose: u8,
124    /// Enable echo mode by passing "-x" to the shell
125    #[arg(short = 'x', long)]
126    echo: bool,
127    /// Do not pass "-o shwordsplit" to zsh.
128    /// Prevent the "shwordsplit" shell option from being set when using zsh.
129    /// The "-o shwordsplit" option is passed to zsh by default so that unquoted
130    /// $variable expressions are subject to word splitting, just like other shells.
131    /// This option disables this behavior.
132    #[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
133    word_split: bool,
134    /// Tree queries for the Gardens/Groups/Trees to execute commands within
135    #[arg(value_terminator = "--")]
136    queries: Vec<String>,
137    /// Arguments to forward to custom commands
138    #[arg(last = true)]
139    arguments: Vec<String>,
140}
141
142/// Main entry point for `garden cmd <query> <command>...`.
143pub fn main_cmd(app_context: &model::ApplicationContext, options: &mut CmdOptions) -> Result<()> {
144    app_context
145        .get_root_config_mut()
146        .apply_defines(&options.define);
147    app_context
148        .get_root_config_mut()
149        .update_quiet_and_verbose_variables(options.quiet, options.verbose);
150    if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
151        debug!("jobs: {:?}", options.num_jobs);
152        debug!("query: {}", options.query);
153        debug!("commands: {:?}", options.commands);
154        debug!("arguments: {:?}", options.arguments);
155        debug!("trees: {:?}", options.trees);
156    }
157    if !app_context.get_root_config().shell_exit_on_error {
158        options.exit_on_error = false;
159    }
160    if !app_context.get_root_config().shell_word_split {
161        options.word_split = false;
162    }
163    let mut params: CmdParams = options.clone().into();
164    params.update(&app_context.options)?;
165
166    let exit_status = if options.num_jobs.is_some() {
167        cmd_parallel(app_context, &options.query, &params)?
168    } else {
169        cmd(app_context, &options.query, &params)?
170    };
171
172    errors::exit_status_into_result(exit_status)
173}
174
175/// CmdParams are used to control the execution of run_cmd_vec().
176///
177/// `garden cmd` and `garden <custom-cmd>` parse command line arguments into CmdParams.
178#[derive(Clone, Debug, Default)]
179pub struct CmdParams {
180    commands: Vec<String>,
181    arguments: Vec<String>,
182    queries: Vec<String>,
183    tree_pattern: glob::Pattern,
184    breadth_first: bool,
185    dry_run: bool,
186    force: bool,
187    keep_going: bool,
188    num_jobs: Option<usize>,
189    echo: bool,
190    #[default(true)]
191    exit_on_error: bool,
192    quiet: bool,
193    verbose: u8,
194    #[default(true)]
195    word_split: bool,
196}
197
198/// Build CmdParams from a CmdOptions struct.
199impl From<CmdOptions> for CmdParams {
200    fn from(options: CmdOptions) -> Self {
201        Self {
202            arguments: options.arguments.clone(),
203            breadth_first: options.breadth_first,
204            commands: options.commands.clone(),
205            dry_run: options.dry_run,
206            echo: options.echo,
207            exit_on_error: options.exit_on_error,
208            force: options.force,
209            keep_going: options.keep_going,
210            num_jobs: options.num_jobs,
211            quiet: options.quiet,
212            tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
213            verbose: options.verbose,
214            word_split: options.word_split,
215            ..Default::default()
216        }
217    }
218}
219
220/// Build CmdParams from a CustomOptions struct
221impl From<CustomOptions> for CmdParams {
222    fn from(options: CustomOptions) -> Self {
223        let mut params = Self {
224            // Add the custom command name to the list of commands. cmds() operates on a vec of commands.
225            arguments: options.arguments.clone(),
226            breadth_first: options.num_jobs.is_none(),
227            // Custom commands run breadth-first. The distinction shouldn't make a difference in
228            // practice because "garden <custom-cmd> ..." is only able to run a single command, but we
229            // use breadth-first because it retains the original implementation/behavior from before
230            // --breadth-first was added to "garden cmd" and made opt-in.
231            //
232            // On the other hand, we want "garden <cmd> <query>" to paralellize over all of the
233            // resolved TreeContexts, so we use depth-first traversal when running in paralle.
234            dry_run: options.dry_run,
235            echo: options.echo,
236            exit_on_error: options.exit_on_error,
237            force: options.force,
238            keep_going: options.keep_going,
239            num_jobs: options.num_jobs,
240            queries: options.queries.clone(),
241            quiet: options.quiet,
242            tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
243            verbose: options.verbose,
244            word_split: options.word_split,
245            ..Default::default()
246        };
247
248        // Default to "." when no queries have been specified.
249        if params.queries.is_empty() {
250            params.queries.push(constants::DOT.into());
251        }
252
253        params
254    }
255}
256
257impl CmdParams {
258    /// Apply the opt-level MainOptions onto the CmdParams.
259    fn update(&mut self, options: &cli::MainOptions) -> Result<()> {
260        self.quiet |= options.quiet;
261        self.verbose += options.verbose;
262        cmd::initialize_threads_option(self.num_jobs)?;
263
264        Ok(())
265    }
266}
267
268/// Format an error
269fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
270    let mut cmd = I::command();
271    err.format(&mut cmd)
272}
273
274/// Main entry point for `garden <command> <query>...`.
275pub fn main_custom(app_context: &model::ApplicationContext, arguments: &Vec<String>) -> Result<()> {
276    // Set the command name to "garden <custom>".
277    let name = &arguments[0];
278    let garden_custom = format!("garden {name}");
279    let cli = CustomOptions::command().bin_name(garden_custom);
280    let matches = cli.get_matches_from(arguments);
281
282    let mut options = <CustomOptions as FromArgMatches>::from_arg_matches(&matches)
283        .map_err(format_error::<CustomOptions>)?;
284    app_context
285        .get_root_config_mut()
286        .apply_defines(&options.define);
287    app_context
288        .get_root_config_mut()
289        .update_quiet_and_verbose_variables(options.quiet, options.verbose);
290    if !app_context.get_root_config().shell_exit_on_error {
291        options.exit_on_error = false;
292    }
293    if !app_context.get_root_config().shell_word_split {
294        options.word_split = false;
295    }
296
297    if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
298        debug!("jobs: {:?}", options.num_jobs);
299        debug!("command: {}", name);
300        debug!("queries: {:?}", options.queries);
301        debug!("arguments: {:?}", options.arguments);
302        debug!("trees: {:?}", options.trees);
303    }
304
305    // Add the custom command name to the list of commands. cmds() operates on a vec of commands.
306    let mut params: CmdParams = options.clone().into();
307    params.update(&app_context.options)?;
308    params.commands.push(name.to_string());
309
310    cmds(app_context, &params)
311}
312
313/// Run commands across trees.
314///
315/// Resolve the trees queries down to a set of tree indexes paired with
316/// an optional garden context.
317///
318/// If the names resolve to gardens, each garden is processed independently.
319/// Trees that exist in multiple matching gardens will be processed multiple
320/// times.
321///
322/// If the names resolve to trees, each tree is processed independently
323/// with no garden context.
324fn cmd(app_context: &model::ApplicationContext, query: &str, params: &CmdParams) -> Result<i32> {
325    let config = app_context.get_root_config_mut();
326    let contexts = query::resolve_trees(app_context, config, None, query);
327    if params.breadth_first {
328        run_cmd_breadth_first(app_context, &contexts, params)
329    } else {
330        run_cmd_depth_first(app_context, &contexts, params)
331    }
332}
333
334/// Run commands in parallel. This is the parallel version of fn cmd().
335fn cmd_parallel(
336    app_context: &model::ApplicationContext,
337    query: &str,
338    params: &CmdParams,
339) -> Result<i32> {
340    let config = app_context.get_root_config_mut();
341    let contexts = query::resolve_trees(app_context, config, None, query);
342    if params.breadth_first {
343        run_cmd_breadth_first_parallel(app_context, &contexts, params)
344    } else {
345        run_cmd_depth_first_parallel(app_context, &contexts, params)
346    }
347}
348
349/// The configured shell state.
350struct ShellParams {
351    /// The shell string is parsed into command line arguments.
352    shell_command: Vec<String>,
353    /// Is this a shell script runner that requires $0 to be passed as the first argument?
354    is_shell: bool,
355}
356
357impl ShellParams {
358    fn new(shell: &str, echo: bool, exit_on_error: bool, word_split: bool) -> Self {
359        let mut shell_command = cmd::shlex_split(shell);
360        let basename = path::str_basename(&shell_command[0]);
361        // Does the shell understand "-e" for errexit?
362        let is_shell = path::is_shell(basename);
363        let is_zsh = matches!(basename, constants::SHELL_ZSH);
364        // Does the shell use "-e <string>" or "-c <string>" to evaluate commands?
365        let is_dash_e = matches!(
366            basename,
367            constants::SHELL_BUN
368                | constants::SHELL_NODE
369                | constants::SHELL_NODEJS
370                | constants::SHELL_PERL
371                | constants::SHELL_RUBY
372        );
373        // Is the shell a full-blown command with "-c" and everything defined by the user?
374        // If so we won't manage the custom shell options ourselves.
375        let is_custom = shell_command.len() > 1;
376        if !is_custom {
377            if word_split && is_zsh {
378                shell_command.push(string!("-o"));
379                shell_command.push(string!("shwordsplit"));
380            }
381            if is_zsh {
382                shell_command.push(string!("+o"));
383                shell_command.push(string!("nomatch"));
384            }
385            if echo && is_shell {
386                shell_command.push(string!("-x"));
387            }
388            if exit_on_error && is_shell {
389                shell_command.push(string!("-e"));
390            }
391            if is_dash_e {
392                shell_command.push(string!("-e"));
393            } else {
394                shell_command.push(string!("-c"));
395            }
396        }
397
398        Self {
399            shell_command,
400            is_shell,
401        }
402    }
403
404    /// Return ShellParams from a "#!" shebang line string.
405    fn from_str(shell: &str) -> Self {
406        let shell_command = cmd::shlex_split(shell);
407        let basename = path::str_basename(&shell_command[0]);
408        // Does the shell understand "-e" for errexit?
409        let is_shell = path::is_shell(basename);
410
411        Self {
412            shell_command,
413            is_shell,
414        }
415    }
416
417    /// Retrun ShellParams from an ApplicationContext and CmdParams.
418    fn from_context_and_params(
419        app_context: &model::ApplicationContext,
420        params: &CmdParams,
421    ) -> Self {
422        let shell = app_context.get_root_config().shell.as_str();
423        Self::new(shell, params.echo, params.exit_on_error, params.word_split)
424    }
425}
426
427/// Check whether the TreeContext is relevant to the current CmdParams.
428/// Returns None when the extracted details are not applicable.
429fn get_tree_from_context<'a>(
430    app_context: &'a model::ApplicationContext,
431    context: &model::TreeContext,
432    params: &CmdParams,
433) -> Option<(&'a model::Configuration, &'a model::Tree)> {
434    // Skip filtered trees.
435    if !params.tree_pattern.matches(&context.tree) {
436        return None;
437    }
438    // Skip symlink trees.
439    let config = match context.config {
440        Some(config_id) => app_context.get_config(config_id),
441        None => app_context.get_root_config(),
442    };
443    let tree = config.trees.get(&context.tree)?;
444    if tree.is_symlink {
445        return None;
446    }
447
448    Some((config, tree))
449}
450
451/// Prepare state needed for running commands.
452fn get_command_environment<'a>(
453    app_context: &'a model::ApplicationContext,
454    context: &model::TreeContext,
455    params: &CmdParams,
456) -> Option<(Option<String>, &'a String, model::Environment)> {
457    let (config, tree) = get_tree_from_context(app_context, context, params)?;
458    // Trees must have a valid path available.
459    let Ok(tree_path) = tree.path_as_ref() else {
460        return None;
461    };
462    // Evaluate the tree environment
463    let env = eval::environment(app_context, config, context);
464    // Sparse gardens/missing trees are ok -> skip these entries.
465    let mut fallback_path = None;
466    let display_options = display::DisplayOptions {
467        branches: config.tree_branches,
468        quiet: params.quiet,
469        verbose: params.verbose,
470        ..std::default::Default::default()
471    };
472    if !display::print_tree(tree, &display_options) {
473        // The "--force" option runs commands in a fallback directory when the tree does not exist.
474        if params.force {
475            fallback_path = Some(config.fallback_execdir_string());
476        } else {
477            return None;
478        }
479    }
480
481    Some((fallback_path, tree_path, env))
482}
483
484// Expand a command to include its pre-commands and post-commands then execute them  in order.
485fn expand_and_run_command(
486    app_context: &model::ApplicationContext,
487    context: &model::TreeContext,
488    name: &str,
489    path: &str,
490    shell_params: &ShellParams,
491    params: &CmdParams,
492    env: &model::Environment,
493) -> Result<i32, i32> {
494    let mut exit_status = errors::EX_OK;
495    // Create a sequence of the command names to run including pre and post-commands.
496    let command_names = cmd::expand_command_names(app_context, context, name);
497    for command_name in &command_names {
498        // One command maps to multiple command sequences. When the scope is tree, only the tree's
499        // commands are included.  When the scope includes a garden, its matching commands are
500        // appended to the end.
501        let cmd_seq_vec = eval::command(app_context, context, command_name);
502        app_context.get_root_config_mut().reset();
503
504        if let Err(cmd_status) = run_cmd_vec(path, shell_params, env, &cmd_seq_vec, params) {
505            exit_status = cmd_status;
506            if !params.keep_going {
507                return Err(cmd_status);
508            }
509        }
510    }
511
512    Ok(exit_status)
513}
514
515/// Run commands breadth-first. Each command is run in all trees before running the next command.
516fn run_cmd_breadth_first(
517    app_context: &model::ApplicationContext,
518    contexts: &[model::TreeContext],
519    params: &CmdParams,
520) -> Result<i32> {
521    let mut exit_status: i32 = errors::EX_OK;
522    let shell_params = ShellParams::from_context_and_params(app_context, params);
523    // Loop over each command, evaluate the tree environment,
524    // and run the command in each context.
525    for name in &params.commands {
526        // One invocation runs multiple commands
527        for context in contexts {
528            let Some((fallback_path, tree_path, env)) =
529                get_command_environment(app_context, context, params)
530            else {
531                continue;
532            };
533            let path = fallback_path.as_ref().unwrap_or(tree_path);
534            match expand_and_run_command(
535                app_context,
536                context,
537                name,
538                path,
539                &shell_params,
540                params,
541                &env,
542            ) {
543                Ok(cmd_status) => {
544                    if cmd_status != errors::EX_OK {
545                        exit_status = cmd_status;
546                    }
547                }
548                Err(cmd_status) => return Ok(cmd_status),
549            }
550        }
551    }
552
553    // Return the last non-zero exit status.
554    Ok(exit_status)
555}
556
557/// Run multiple commands in parallel over a single tree query.
558/// All command are run in parallel over all of the matching trees.
559/// Each command invocation operates over every resolved tree, serially, within the scope of
560/// the currently running command.
561fn run_cmd_breadth_first_parallel(
562    app_context: &model::ApplicationContext,
563    contexts: &[model::TreeContext],
564    params: &CmdParams,
565) -> Result<i32> {
566    let exit_status = atomic::AtomicI32::new(errors::EX_OK);
567    let shell_params = ShellParams::from_context_and_params(app_context, params);
568    // Loop over each command, evaluate the tree environment, and run the command in each context.
569    params.commands.par_iter().for_each(|name| {
570        // Create a thread-specific ApplicationContext.
571        let app_context_clone = app_context.clone();
572        let app_context = &app_context_clone;
573        // One invocation runs multiple commands
574        for context in contexts {
575            let Some((fallback_path, tree_path, env)) =
576                get_command_environment(app_context, context, params)
577            else {
578                continue;
579            };
580            let path = fallback_path.as_ref().unwrap_or(tree_path);
581            match expand_and_run_command(
582                app_context,
583                context,
584                name,
585                path,
586                &shell_params,
587                params,
588                &env,
589            ) {
590                Ok(cmd_status) => {
591                    if cmd_status != errors::EX_OK {
592                        exit_status.store(cmd_status, atomic::Ordering::Release);
593                    }
594                }
595                Err(cmd_status) => {
596                    exit_status.store(cmd_status, atomic::Ordering::Release);
597                    break;
598                }
599            }
600        }
601    });
602
603    // Return the last non-zero exit status.
604    Ok(exit_status.load(atomic::Ordering::Acquire))
605}
606
607/// Run commands depth-first. All commands are run on the current tree before visiting the next tree.
608fn run_cmd_depth_first(
609    app_context: &model::ApplicationContext,
610    contexts: &[model::TreeContext],
611    params: &CmdParams,
612) -> Result<i32> {
613    let mut exit_status: i32 = errors::EX_OK;
614    let shell_params = ShellParams::from_context_and_params(app_context, params);
615    // Loop over each context, evaluate the tree environment and run the command.
616    for context in contexts {
617        let Some((fallback_path, tree_path, env)) =
618            get_command_environment(app_context, context, params)
619        else {
620            continue;
621        };
622        let path = fallback_path.as_ref().unwrap_or(tree_path);
623        // One invocation runs multiple commands
624        for name in &params.commands {
625            match expand_and_run_command(
626                app_context,
627                context,
628                name,
629                path,
630                &shell_params,
631                params,
632                &env,
633            ) {
634                Ok(cmd_status) => {
635                    if cmd_status != errors::EX_OK {
636                        exit_status = cmd_status;
637                    }
638                }
639                Err(cmd_status) => return Ok(cmd_status),
640            }
641        }
642    }
643
644    // Return the last non-zero exit status.
645    Ok(exit_status)
646}
647
648/// Run commands depth-first in parallel.
649/// All trees are visited concurrently in parallel. Commands are run serially within
650/// the scope of a single tree.
651fn run_cmd_depth_first_parallel(
652    app_context: &model::ApplicationContext,
653    contexts: &[model::TreeContext],
654    params: &CmdParams,
655) -> Result<i32> {
656    let exit_status = atomic::AtomicI32::new(errors::EX_OK);
657    let shell_params = ShellParams::from_context_and_params(app_context, params);
658    // Loop over each context, evaluate the tree environment and run the command.
659    contexts.par_iter().for_each(|context| {
660        // Create a thread-specific ApplicationContext.
661        let app_context_clone = app_context.clone();
662        let app_context = &app_context_clone;
663        let Some((fallback_path, tree_path, env)) =
664            get_command_environment(app_context, context, params)
665        else {
666            return;
667        };
668        let path = fallback_path.as_ref().unwrap_or(tree_path);
669        // One invocation runs multiple commands
670        for name in &params.commands {
671            match expand_and_run_command(
672                app_context,
673                context,
674                name,
675                path,
676                &shell_params,
677                params,
678                &env,
679            ) {
680                Ok(cmd_status) => {
681                    if cmd_status != errors::EX_OK {
682                        exit_status.store(cmd_status, atomic::Ordering::Release);
683                    }
684                }
685                Err(cmd_status) => {
686                    exit_status.store(cmd_status, atomic::Ordering::Release);
687                    break;
688                }
689            }
690        }
691    });
692
693    // Return any of the non-zero exit statuses. Which value is returned is
694    // undefined due to the parallel nature of this function. Any of the
695    // non-zero exit status values could have ended up recorded in exit_status.
696    Ok(exit_status.load(atomic::Ordering::Acquire))
697}
698
699/// Run a vector of custom commands using the configured shell.
700/// Parameters:
701/// - path: The current working directory for the command.
702/// - shell: The shell that will be used to run the command strings.
703/// - env: Environment variables to set.
704/// - cmd_seq_vec: Vector of vector of command strings to run.
705/// - arguments: Additional command line arguments available in $1, $2, $N.
706fn run_cmd_vec(
707    path: &str,
708    shell_params: &ShellParams,
709    env: &model::Environment,
710    cmd_seq_vec: &[Vec<String>],
711    params: &CmdParams,
712) -> Result<(), i32> {
713    // Get the current executable name
714    let current_exe = cmd::current_exe();
715    let mut exit_status = errors::EX_OK;
716    for cmd_seq in cmd_seq_vec {
717        for cmd_str in cmd_seq {
718            if params.verbose > 1 {
719                eprintln!("{} {}", ":".cyan(), &cmd_str.trim_end().green());
720            }
721            if params.dry_run {
722                continue;
723            }
724            // Create a custom ShellParams when "#!" is used.
725            let cmd_shell_params;
726            let (cmd_str, shell_params) = match syntax::split_shebang(cmd_str) {
727                Some((shell_cmd, cmd_str)) => {
728                    cmd_shell_params = ShellParams::from_str(shell_cmd);
729                    (cmd_str, &cmd_shell_params)
730                }
731                None => (cmd_str.as_str(), shell_params),
732            };
733            let mut exec = subprocess::Exec::cmd(&shell_params.shell_command[0]).cwd(path);
734            exec = exec.args(&shell_params.shell_command[1..]);
735            exec = exec.arg(cmd_str);
736            if shell_params.is_shell {
737                // Shells require $0 to be specified when using -c to run commands in order to make $1 and friends
738                // behave intuitively from within the script. The garden executable's location is
739                // provided in $0 for convenience.
740                exec = exec.arg(current_exe.as_str());
741            }
742            exec = exec.args(&params.arguments);
743            // Update the command environment
744            for (k, v) in env {
745                exec = exec.env(k, v);
746            }
747            // When a command list is used then the return code from the final command
748            // is the one that is returned when --no-errexit is in effect.
749            let status = cmd::status(exec);
750            if status != errors::EX_OK {
751                exit_status = status;
752                if params.exit_on_error {
753                    return Err(status);
754                }
755            } else {
756                exit_status = errors::EX_OK;
757            }
758        }
759        if exit_status != errors::EX_OK {
760            return Err(exit_status);
761        }
762    }
763
764    Ok(())
765}
766
767/// Run cmd() over a Vec of tree queries
768fn cmds(app: &model::ApplicationContext, params: &CmdParams) -> Result<()> {
769    let exit_status = atomic::AtomicI32::new(errors::EX_OK);
770    if params.num_jobs.is_some() {
771        params.queries.par_iter().for_each(|query| {
772            let status = cmd_parallel(&app.clone(), query, params).unwrap_or(errors::EX_IOERR);
773            if status != errors::EX_OK {
774                exit_status.store(status, atomic::Ordering::Release);
775            }
776        });
777    } else {
778        for query in &params.queries {
779            let status = cmd(app, query, params).unwrap_or(errors::EX_IOERR);
780            if status != errors::EX_OK {
781                exit_status.store(status, atomic::Ordering::Release);
782                if !params.keep_going {
783                    break;
784                }
785            }
786        }
787    }
788    // Return the last non-zero exit status.
789    errors::exit_status_into_result(exit_status.load(atomic::Ordering::Acquire))
790}