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