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