1use std::path::{Path, PathBuf};
9
10use indexmap::IndexMap;
11use vantage_core::{Result, error};
12
13#[derive(Clone, Debug)]
15pub struct CmdOutput {
16 pub stdout: String,
17 pub stderr: String,
18 pub exit_code: i32,
19}
20
21fn resolve_program(command: &str, base_dir: Option<&Path>) -> PathBuf {
26 match base_dir {
27 Some(dir) if command.contains('/') && !Path::new(command).is_absolute() => {
28 dir.join(command)
29 }
30 _ => PathBuf::from(command),
31 }
32}
33
34pub fn run_command(
40 command: &str,
41 args: &[String],
42 env: &IndexMap<String, String>,
43 pass_path: bool,
44 base_dir: Option<&Path>,
45) -> Result<CmdOutput> {
46 let mut cmd = std::process::Command::new(resolve_program(command, base_dir));
47 cmd.args(args);
48 cmd.env_clear();
49
50 if let Some(dir) = base_dir {
51 cmd.current_dir(dir);
52 }
53
54 if pass_path {
55 if let Ok(path) = std::env::var("PATH") {
56 cmd.env("PATH", path);
57 }
58 if let Ok(home) = std::env::var("HOME") {
59 cmd.env("HOME", home);
60 }
61 }
62 for (k, v) in env {
63 cmd.env(k, v);
64 }
65
66 let output = cmd.output().map_err(|e| {
67 error!(
68 "failed to execute command",
69 command = command.to_string(),
70 detail = e.to_string()
71 )
72 })?;
73
74 Ok(CmdOutput {
75 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
76 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
77 exit_code: output.status.code().unwrap_or(-1),
78 })
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn bare_name_is_left_for_path_lookup() {
87 assert_eq!(resolve_program("gh", None), PathBuf::from("gh"));
89 assert_eq!(
90 resolve_program("gh", Some(Path::new("/inv"))),
91 PathBuf::from("gh")
92 );
93 }
94
95 #[test]
96 fn relative_path_resolves_against_base_dir() {
97 assert_eq!(
98 resolve_program("./scripts/gh-stats.py", Some(Path::new("/inv"))),
99 PathBuf::from("/inv/./scripts/gh-stats.py")
100 );
101 assert_eq!(
102 resolve_program("scripts/gh-stats.py", Some(Path::new("/inv"))),
103 PathBuf::from("/inv/scripts/gh-stats.py")
104 );
105 }
106
107 #[test]
108 fn relative_path_without_base_dir_is_unchanged() {
109 assert_eq!(
110 resolve_program("./scripts/x.py", None),
111 PathBuf::from("./scripts/x.py")
112 );
113 }
114
115 #[test]
116 fn absolute_path_passes_through() {
117 assert_eq!(
118 resolve_program("/usr/bin/gh", Some(Path::new("/inv"))),
119 PathBuf::from("/usr/bin/gh")
120 );
121 }
122}