1use crate::error::{CommonError, Result};
4use shell_words;
5use std::fmt;
6
7#[derive(Debug, Clone, PartialEq)]
9pub struct ShellCommand {
10 pub command: String,
12 pub args: Vec<String>,
14}
15
16impl ShellCommand {
17 pub fn new(command: impl Into<String>) -> Self {
19 Self {
20 command: command.into(),
21 args: Vec::new(),
22 }
23 }
24
25 pub fn arg(mut self, arg: impl Into<String>) -> Self {
27 self.args.push(arg.into());
28 self
29 }
30
31 pub fn args<I, S>(mut self, args: I) -> Self
33 where
34 I: IntoIterator<Item = S>,
35 S: Into<String>,
36 {
37 self.args.extend(args.into_iter().map(Into::into));
38 self
39 }
40
41 pub fn parse(input: &str) -> Result<Self> {
43 let parts =
44 shell_words::split(input).map_err(|e| CommonError::ShellParse(e.to_string()))?;
45
46 if parts.is_empty() {
47 return Err(CommonError::ShellParse("Empty command".to_string()));
48 }
49
50 Ok(Self {
51 command: parts[0].clone(),
52 args: parts[1..].to_vec(),
53 })
54 }
55
56 pub fn to_shell_string(&self) -> String {
58 let mut parts = vec![self.command.clone()];
59 parts.extend(self.args.clone());
60 shell_words::join(&parts)
61 }
62
63 pub fn parts(&self) -> Vec<&str> {
65 let mut parts = vec![self.command.as_str()];
66 parts.extend(self.args.iter().map(|s| s.as_str()));
67 parts
68 }
69}
70
71impl fmt::Display for ShellCommand {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(f, "{}", self.to_shell_string())
74 }
75}
76
77pub fn parse_env_vars(input: &str) -> Result<Vec<(String, String)>> {
79 let mut env_vars = Vec::new();
80 let parts = shell_words::split(input).map_err(|e| CommonError::ShellParse(e.to_string()))?;
81
82 for part in parts {
83 if let Some((key, value)) = part.split_once('=') {
84 if is_valid_env_var_name(key) {
85 env_vars.push((key.to_string(), value.to_string()));
86 }
87 }
88 }
89
90 Ok(env_vars)
91}
92
93pub fn is_valid_env_var_name(name: &str) -> bool {
95 !name.is_empty()
96 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
97 && name
98 .chars()
99 .next()
100 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn test_shell_command_parse() {
109 let cmd = ShellCommand::parse("cargo test --features foo").unwrap();
110 assert_eq!(cmd.command, "cargo");
111 assert_eq!(cmd.args, vec!["test", "--features", "foo"]);
112 }
113
114 #[test]
115 fn test_shell_command_display() {
116 let cmd = ShellCommand::new("echo").arg("hello world").arg("test");
117 assert_eq!(cmd.to_string(), "echo 'hello world' test");
118 assert_eq!(cmd.to_shell_string(), "echo 'hello world' test");
119 }
120
121 #[test]
122 fn test_parse_env_vars() {
123 let vars = parse_env_vars("FOO=bar BAZ='quoted value'").unwrap();
124 assert_eq!(
125 vars,
126 vec![
127 ("FOO".to_string(), "bar".to_string()),
128 ("BAZ".to_string(), "quoted value".to_string()),
129 ]
130 );
131 }
132}