Skip to main content

shell_tunnel/execution/
command.rs

1//! Command building and representation.
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5use std::time::Duration;
6
7/// A command to be executed in a shell session.
8#[derive(Debug, Clone)]
9pub struct Command {
10    /// The command line to execute.
11    pub command_line: String,
12    /// Working directory override (if any).
13    pub working_dir: Option<PathBuf>,
14    /// Environment variables to set.
15    pub env: HashMap<String, String>,
16    /// Maximum execution time.
17    pub timeout: Option<Duration>,
18    /// Whether to capture output.
19    pub capture_output: bool,
20}
21
22impl Command {
23    /// Create a new command with the given command line.
24    pub fn new(command_line: impl Into<String>) -> Self {
25        Self {
26            command_line: command_line.into(),
27            working_dir: None,
28            env: HashMap::new(),
29            timeout: None,
30            capture_output: true,
31        }
32    }
33
34    /// Set the working directory.
35    pub fn working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
36        self.working_dir = Some(dir.into());
37        self
38    }
39
40    /// Add an environment variable.
41    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
42        self.env.insert(key.into(), value.into());
43        self
44    }
45
46    /// Add multiple environment variables.
47    pub fn envs<I, K, V>(mut self, vars: I) -> Self
48    where
49        I: IntoIterator<Item = (K, V)>,
50        K: Into<String>,
51        V: Into<String>,
52    {
53        for (k, v) in vars {
54            self.env.insert(k.into(), v.into());
55        }
56        self
57    }
58
59    /// Set the execution timeout.
60    pub fn timeout(mut self, duration: Duration) -> Self {
61        self.timeout = Some(duration);
62        self
63    }
64
65    /// Set whether to capture output.
66    pub fn capture_output(mut self, capture: bool) -> Self {
67        self.capture_output = capture;
68        self
69    }
70}
71
72impl Default for Command {
73    fn default() -> Self {
74        Self::new("")
75    }
76}
77
78/// Builder for creating commands with fluent API.
79#[derive(Debug, Default)]
80pub struct CommandBuilder {
81    command_line: Option<String>,
82    working_dir: Option<PathBuf>,
83    env: HashMap<String, String>,
84    timeout: Option<Duration>,
85    capture_output: bool,
86}
87
88impl CommandBuilder {
89    /// Create a new command builder.
90    pub fn new() -> Self {
91        Self {
92            capture_output: true,
93            ..Default::default()
94        }
95    }
96
97    /// Set the command line.
98    pub fn command_line(mut self, cmd: impl Into<String>) -> Self {
99        self.command_line = Some(cmd.into());
100        self
101    }
102
103    /// Set the working directory.
104    pub fn working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
105        self.working_dir = Some(dir.into());
106        self
107    }
108
109    /// Add an environment variable.
110    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
111        self.env.insert(key.into(), value.into());
112        self
113    }
114
115    /// Set the execution timeout.
116    pub fn timeout(mut self, duration: Duration) -> Self {
117        self.timeout = Some(duration);
118        self
119    }
120
121    /// Set whether to capture output.
122    pub fn capture_output(mut self, capture: bool) -> Self {
123        self.capture_output = capture;
124        self
125    }
126
127    /// Build the command.
128    ///
129    /// Returns `None` if no command line was specified.
130    pub fn build(self) -> Option<Command> {
131        self.command_line.map(|cmd| Command {
132            command_line: cmd,
133            working_dir: self.working_dir,
134            env: self.env,
135            timeout: self.timeout,
136            capture_output: self.capture_output,
137        })
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_command_new() {
147        let cmd = Command::new("ls -la");
148        assert_eq!(cmd.command_line, "ls -la");
149        assert!(cmd.working_dir.is_none());
150        assert!(cmd.env.is_empty());
151        assert!(cmd.timeout.is_none());
152        assert!(cmd.capture_output);
153    }
154
155    #[test]
156    fn test_command_builder_chain() {
157        let cmd = Command::new("cargo build")
158            .working_dir("/project")
159            .env("RUST_LOG", "debug")
160            .timeout(Duration::from_secs(60))
161            .capture_output(true);
162
163        assert_eq!(cmd.command_line, "cargo build");
164        assert_eq!(cmd.working_dir, Some(PathBuf::from("/project")));
165        assert_eq!(cmd.env.get("RUST_LOG"), Some(&"debug".to_string()));
166        assert_eq!(cmd.timeout, Some(Duration::from_secs(60)));
167    }
168
169    #[test]
170    fn test_command_envs() {
171        let vars = [("KEY1", "val1"), ("KEY2", "val2")];
172        let cmd = Command::new("echo").envs(vars);
173
174        assert_eq!(cmd.env.len(), 2);
175        assert_eq!(cmd.env.get("KEY1"), Some(&"val1".to_string()));
176        assert_eq!(cmd.env.get("KEY2"), Some(&"val2".to_string()));
177    }
178
179    #[test]
180    fn test_command_builder_build() {
181        let cmd = CommandBuilder::new()
182            .command_line("pwd")
183            .working_dir("/tmp")
184            .build();
185
186        assert!(cmd.is_some());
187        let cmd = cmd.unwrap();
188        assert_eq!(cmd.command_line, "pwd");
189        assert_eq!(cmd.working_dir, Some(PathBuf::from("/tmp")));
190    }
191
192    #[test]
193    fn test_command_builder_empty() {
194        let cmd = CommandBuilder::new().build();
195        assert!(cmd.is_none());
196    }
197}