Skip to main content

garden/
cmd.rs

1use crate::{constants, display, errors, eval, model, syntax};
2
3/// Return an exit status code from a subprocess::Exec instance.
4pub fn status(exec: subprocess::Exec) -> i32 {
5    if let Err(status) = subprocess_result(exec.join()) {
6        status
7    } else {
8        errors::EX_OK
9    }
10}
11
12/// Flatten a subprocess::Result into a Result<(), i32>.
13pub fn subprocess_result(result: subprocess::Result<subprocess::ExitStatus>) -> Result<(), i32> {
14    match result {
15        Ok(subprocess::ExitStatus::Exited(status)) => {
16            if status == 0 {
17                Ok(())
18            } else {
19                Err(status as i32)
20            }
21        }
22        Ok(subprocess::ExitStatus::Signaled(status)) => Err(status as i32),
23        Ok(subprocess::ExitStatus::Other(status)) => Err(status),
24        Ok(subprocess::ExitStatus::Undetermined) => Err(errors::EX_ERROR),
25        Err(subprocess::PopenError::IoError(err)) => {
26            if err.kind() == std::io::ErrorKind::NotFound {
27                Err(errors::EX_UNAVAILABLE)
28            } else {
29                Err(errors::EX_IOERR)
30            }
31        }
32        Err(_) => Err(errors::EX_ERROR),
33    }
34}
35
36/// Take a subprocess capture and return a string without trailing whitespace.
37fn stdout(capture: &subprocess::CaptureData) -> String {
38    capture.stdout_str().trim_end().to_string()
39}
40
41/// Convert a PopenError into a garden::errors::CommandError.
42fn command_error_from_popen_error(
43    command: String,
44    popen_err: subprocess::PopenError,
45) -> errors::CommandError {
46    let status = match popen_err {
47        subprocess::PopenError::IoError(err) => err.raw_os_error().unwrap_or(1),
48        _ => 1,
49    };
50    errors::CommandError::ExitStatus { command, status }
51}
52
53/// Return a CaptureData result for a subprocess's stdout.
54pub(crate) fn capture_stdout(
55    exec: subprocess::Exec,
56) -> Result<subprocess::CaptureData, errors::CommandError> {
57    let command = exec.to_cmdline_lossy();
58    let capture = exec
59        .stdout(subprocess::Redirection::Pipe)
60        .stderr(subprocess::NullFile {}) // Redirect stderr to /dev/null
61        .capture();
62
63    match capture {
64        Ok(result) => {
65            let status = exit_status(result.exit_status);
66            if status == 0 {
67                Ok(result)
68            } else {
69                Err(errors::CommandError::ExitStatus { command, status })
70            }
71        }
72        Err(err) => Err(command_error_from_popen_error(command, err)),
73    }
74}
75
76/// Convert subprocess::ExitStatus into a CommandError
77pub(crate) fn exit_status(status: subprocess::ExitStatus) -> i32 {
78    match status {
79        subprocess::ExitStatus::Exited(status) => status as i32,
80        subprocess::ExitStatus::Signaled(status) => status as i32,
81        subprocess::ExitStatus::Other(status) => status,
82        subprocess::ExitStatus::Undetermined => errors::EX_ERROR,
83    }
84}
85
86/// Return a trimmed stdout string for an subprocess::Exec instance.
87pub fn stdout_to_string(exec: subprocess::Exec) -> Result<String, errors::CommandError> {
88    Ok(stdout(&capture_stdout(exec)?))
89}
90
91/// Return a `subprocess::Exec` for a command.
92pub fn exec_cmd<S>(command: &[S]) -> subprocess::Exec
93where
94    S: AsRef<std::ffi::OsStr>,
95{
96    if command.len() > 1 {
97        subprocess::Exec::cmd(&command[0]).args(&command[1..])
98    } else {
99        subprocess::Exec::cmd(&command[0])
100    }
101}
102
103/// Return a `subprocess::Exec` that runs a command in the specified directory.
104pub fn exec_in_dir<P, S>(command: &[S], path: &P) -> subprocess::Exec
105where
106    P: AsRef<std::path::Path> + std::convert::AsRef<std::ffi::OsStr> + ?Sized,
107    S: AsRef<std::ffi::OsStr>,
108{
109    exec_cmd(command).cwd(path).env(constants::ENV_PWD, path)
110}
111
112/// Return the exit status from running a command using `subprocess::Exec`
113/// in the specified directory.
114pub(crate) fn run_command<P, S>(command: &[S], path: &P) -> i32
115where
116    P: AsRef<std::path::Path> + std::convert::AsRef<std::ffi::OsStr> + ?Sized,
117    S: AsRef<std::ffi::OsStr>,
118{
119    status(exec_in_dir(command, path))
120}
121
122/// Run a command in the specified tree context.
123/// Parameters:
124/// - config: Mutable reference to a Configuration.
125/// - context: Reference to the TreeContext to evaluate.
126/// - quiet: Suppress messages when set true.
127/// - verbose: increase verbosity of messages.
128/// - command: String vector of the command to run.
129pub(crate) fn exec_in_context<S>(
130    app_context: &model::ApplicationContext,
131    config: &model::Configuration,
132    context: &model::TreeContext,
133    quiet: bool,
134    verbose: u8,
135    dry_run: bool,
136    command: &[S],
137) -> Result<(), errors::GardenError>
138where
139    S: AsRef<std::ffi::OsStr>,
140{
141    let display_options = display::DisplayOptions {
142        branches: config.tree_branches,
143        verbose,
144        quiet,
145        ..std::default::Default::default()
146    };
147    let graft_config = context
148        .config
149        .map(|graft_id| app_context.get_config(graft_id));
150
151    let path;
152    if let Some(graft_cfg) = graft_config {
153        if let Some(tree) = graft_cfg.trees.get(&context.tree) {
154            path = tree.path_as_ref()?;
155
156            // Sparse gardens/missing trees are okay -> skip these entries.
157            if !display::print_tree(tree, &display_options) {
158                return Ok(());
159            }
160        } else {
161            return Ok(());
162        }
163    } else if let Some(tree) = config.trees.get(&context.tree) {
164        path = tree.path_as_ref()?;
165
166        // Sparse gardens/missing trees are okay -> skip these entries.
167        if !display::print_tree(tree, &display_options) {
168            return Ok(());
169        }
170    } else {
171        return Ok(());
172    }
173    // Evaluate the tree environment and run the command.
174    let env = eval::environment(app_context, config, context);
175    let command_vec = resolve_command(command, &env);
176    if verbose > 1 || dry_run {
177        display::print_command_string_vec(&command_vec);
178    }
179    if dry_run {
180        return Ok(());
181    }
182
183    // Create an Exec object.
184    let mut exec = exec_in_dir(&command_vec, &path);
185
186    //  Update the command environment
187    for (name, value) in &env {
188        exec = exec.env(name, value);
189    }
190
191    errors::result_from_exit_status(status(exec))
192}
193
194/// The command might be a path that only exists inside the resolved
195/// environment.  Resolve the path by looking for the presence of PATH
196/// and updating the command when it exists.
197fn resolve_command<S>(command: &[S], env: &[(String, String)]) -> Vec<String>
198where
199    S: AsRef<std::ffi::OsStr>,
200{
201    let mut cmd_path = std::path::PathBuf::from(&command[0]);
202    // Transform cmd_path into an absolute path.
203    if !cmd_path.is_absolute() {
204        for (name, value) in env {
205            // Loop until we find PATH.
206            if name == constants::ENV_PATH {
207                if let Some(path_buf) = std::env::split_paths(&value).find_map(|dir| {
208                    let full_path = dir.join(&cmd_path);
209                    if full_path.is_file() {
210                        Some(full_path)
211                    } else {
212                        None
213                    }
214                }) {
215                    cmd_path = path_buf;
216                }
217                // Once we've seen $PATH we're done.
218                break;
219            }
220        }
221    }
222
223    // Create a copy of the command so where the first entry has been replaced
224    // with a $PATH-resolved absolute path.
225    let mut command_vec = Vec::with_capacity(command.len());
226    command_vec.push(cmd_path.to_string_lossy().to_string());
227    for arg in &command[1..] {
228        let curpath = std::path::PathBuf::from(arg);
229        command_vec.push(curpath.to_string_lossy().into());
230    }
231
232    command_vec
233}
234
235/// Return the current executable path.
236pub(crate) fn current_exe() -> String {
237    match std::env::current_exe() {
238        Err(_) => constants::GARDEN.into(),
239        Ok(path) => path.to_string_lossy().into(),
240    }
241}
242
243/// Given a command name, eg. "custom>", collect all of the custom command values
244/// configured using the specified name. This function is used to gather
245/// pre and post-commands associated with a command.
246pub(crate) fn get_command_values(
247    app_context: &model::ApplicationContext,
248    context: &model::TreeContext,
249    name: &str,
250) -> Vec<String> {
251    let config = match context.config {
252        Some(config_id) => app_context.get_config(config_id),
253        None => app_context.get_root_config(),
254    };
255    let mut vec_variables = Vec::new();
256
257    // Global commands
258    for (command_name, var) in &config.commands {
259        if name == command_name {
260            vec_variables.push(var.clone());
261        }
262    }
263
264    // Tree commands
265    if let Some(tree) = config.trees.get(&context.tree) {
266        for (command_name, var) in &tree.commands {
267            if name == command_name {
268                vec_variables.push(var.clone());
269            }
270        }
271    }
272
273    // Optional garden command scope
274    if let Some(garden_name) = &context.garden {
275        if let Some(garden) = &config.gardens.get(garden_name) {
276            for (command_name, var) in &garden.commands {
277                if name == command_name {
278                    vec_variables.push(var.clone());
279                }
280            }
281        }
282    }
283
284    let mut commands = Vec::with_capacity(vec_variables.len() * 2);
285    for variables in vec_variables.iter_mut() {
286        let values = eval::variables_for_shell(app_context, config, variables, context);
287        commands.extend(values);
288    }
289
290    commands
291}
292
293/// Recursively expand a command name to include its pre-commands and post-commands.
294/// Self-referential loops are avoided. Duplicate commands are retained.
295pub(crate) fn expand_command_names(
296    app_context: &model::ApplicationContext,
297    context: &model::TreeContext,
298    name: &str,
299) -> Vec<String> {
300    let pre_name = syntax::pre_command(name);
301    let post_name = syntax::post_command(name);
302    let pre_commands = get_command_values(app_context, context, &pre_name);
303    let post_commands = get_command_values(app_context, context, &post_name);
304
305    let mut command_names = Vec::with_capacity(pre_commands.len() + 1 + post_commands.len());
306    // Recursively expand pre-commands.
307    for cmd_name in pre_commands.iter() {
308        if cmd_name != name {
309            // Avoid self-referential loops.
310            command_names.extend(expand_command_names(app_context, context, cmd_name));
311        }
312    }
313    command_names.push(name.to_string());
314    // Recursively expand post-commands.
315    for cmd_name in post_commands.iter() {
316        if cmd_name != name {
317            // Avoid self-referential loops.
318            command_names.extend(expand_command_names(app_context, context, cmd_name));
319        }
320    }
321
322    command_names
323}
324
325/// Shell quote a single command argument. Intended for or display purposes only.
326/// Failure to quote will pass the argument through as-is.
327pub(crate) fn shell_quote(arg: &str) -> String {
328    shlex::try_quote(arg)
329        .map(|quoted_arg| quoted_arg.to_string())
330        .unwrap_or_else(|_| arg.to_string())
331}
332
333/// Split a shell string into command-line arguments.
334pub fn shlex_split(shell: &str) -> Vec<String> {
335    if shell.is_empty() {
336        return Vec::new();
337    }
338    match shlex::split(shell) {
339        Some(shell_command) if !shell_command.is_empty() => shell_command,
340        _ => {
341            vec![shell.to_string()]
342        }
343    }
344}
345
346/// Get the default number of jobs to run in parallel
347pub(crate) fn default_num_jobs() -> usize {
348    match std::thread::available_parallelism() {
349        Ok(value) => std::cmp::max(value.get(), 3), // "prune" requires at minimum three threads.
350        Err(_) => 4,
351    }
352}
353
354/// Initialize the global thread pool.
355pub(crate) fn initialize_threads(num_jobs: usize) -> anyhow::Result<()> {
356    let num_jobs = if num_jobs == 0 {
357        default_num_jobs()
358    } else {
359        num_jobs
360    };
361    rayon::ThreadPoolBuilder::new()
362        .num_threads(num_jobs)
363        .build_global()?;
364
365    Ok(())
366}
367
368/// Initialize the global thread pool when the num_jobs option is provided.
369pub fn initialize_threads_option(num_jobs: Option<usize>) -> anyhow::Result<()> {
370    let Some(num_jobs_value) = num_jobs else {
371        return Ok(());
372    };
373
374    initialize_threads(num_jobs_value)
375}