stem_rs/interpreter/arguments.rs
1//! Command-line argument parsing for the interpreter prompt.
2//!
3//! This module provides argument parsing for the Tor interpreter command-line
4//! interface, handling connection options, execution modes, and display settings.
5//!
6//! # Overview
7//!
8//! The interpreter accepts various command-line arguments to configure:
9//! - Control interface connection (TCP port or Unix socket)
10//! - Tor binary path for auto-starting
11//! - Single command or script execution
12//! - Output formatting options
13//!
14//! # Supported Arguments
15//!
16//! | Short | Long | Description |
17//! |-------|------|-------------|
18//! | `-i` | `--interface` | Control interface `[ADDRESS:]PORT` |
19//! | `-s` | `--socket` | Unix domain socket path |
20//! | | `--tor` | Path to Tor binary |
21//! | | `--run` | Command or script file to execute |
22//! | | `--no-color` | Disable colored output |
23//! | `-h` | `--help` | Show help message |
24//!
25//! # Example
26//!
27//! ```rust
28//! use stem_rs::interpreter::arguments::Arguments;
29//!
30//! // Parse command-line arguments
31//! let args = Arguments::parse(&[
32//! "-i".to_string(),
33//! "9051".to_string(),
34//! "--no-color".to_string(),
35//! ]).unwrap();
36//!
37//! assert_eq!(args.control_port, Some(9051));
38//! assert!(args.disable_color);
39//! ```
40//!
41//! # Python Stem Equivalent
42//!
43//! This module corresponds to Python Stem's `stem.interpreter.arguments` module.
44
45use crate::util::{is_valid_ipv4_address, is_valid_port};
46
47/// Parsed command-line arguments for the interpreter.
48///
49/// This struct holds all configuration options that can be specified
50/// via command-line arguments when launching the interpreter.
51///
52/// # Default Values
53///
54/// | Field | Default |
55/// |-------|---------|
56/// | `control_address` | `"127.0.0.1"` |
57/// | `control_port` | `None` (uses Tor's default) |
58/// | `control_socket` | `"/var/run/tor/control"` |
59/// | `tor_path` | `"tor"` |
60///
61/// # Example
62///
63/// ```rust
64/// use stem_rs::interpreter::arguments::Arguments;
65///
66/// // Use defaults
67/// let defaults = Arguments::default();
68/// assert_eq!(defaults.control_address, "127.0.0.1");
69///
70/// // Parse from command line
71/// let args = Arguments::parse(&["--interface".to_string(), "192.168.1.1:9051".to_string()]).unwrap();
72/// assert_eq!(args.control_address, "192.168.1.1");
73/// assert_eq!(args.control_port, Some(9051));
74/// ```
75#[derive(Debug, Clone)]
76pub struct Arguments {
77 /// IP address for the control interface.
78 ///
79 /// Defaults to `"127.0.0.1"` (localhost).
80 pub control_address: String,
81 /// Port number for the control interface.
82 ///
83 /// If `None`, the default Tor control port is used.
84 pub control_port: Option<u16>,
85 /// Whether the user explicitly specified a port.
86 ///
87 /// Used to determine connection priority when both port and socket
88 /// are available.
89 pub user_provided_port: bool,
90 /// Path to the Unix domain socket for control connection.
91 ///
92 /// Defaults to `"/var/run/tor/control"`.
93 pub control_socket: String,
94 /// Whether the user explicitly specified a socket path.
95 ///
96 /// Used to determine connection priority when both port and socket
97 /// are available.
98 pub user_provided_socket: bool,
99 /// Path to the Tor binary.
100 ///
101 /// Used when Tor needs to be started automatically.
102 /// Defaults to `"tor"` (found via PATH).
103 pub tor_path: String,
104 /// Single command to execute and exit.
105 ///
106 /// If set, the interpreter runs this command and exits instead of
107 /// entering interactive mode.
108 pub run_cmd: Option<String>,
109 /// Path to a script file to execute.
110 ///
111 /// If set, the interpreter runs all commands in the file and exits.
112 /// Takes precedence over `run_cmd` if the path exists.
113 pub run_path: Option<String>,
114 /// Whether to disable colored output.
115 ///
116 /// When `true`, all output is plain text without ANSI color codes.
117 pub disable_color: bool,
118 /// Whether to print help and exit.
119 ///
120 /// When `true`, the interpreter prints usage information and exits
121 /// without connecting to Tor.
122 pub print_help: bool,
123}
124
125impl Default for Arguments {
126 fn default() -> Self {
127 Self {
128 control_address: "127.0.0.1".to_string(),
129 control_port: None,
130 user_provided_port: false,
131 control_socket: "/var/run/tor/control".to_string(),
132 user_provided_socket: false,
133 tor_path: "tor".to_string(),
134 run_cmd: None,
135 run_path: None,
136 disable_color: false,
137 print_help: false,
138 }
139 }
140}
141
142impl Arguments {
143 /// Parses command-line arguments into an `Arguments` struct.
144 ///
145 /// # Arguments
146 ///
147 /// * `argv` - Slice of command-line argument strings (excluding program name)
148 ///
149 /// # Returns
150 ///
151 /// Parsed arguments on success, or an error message on failure.
152 ///
153 /// # Errors
154 ///
155 /// Returns an error string if:
156 /// - An argument requires a value but none is provided
157 /// - An IP address is invalid
158 /// - A port number is invalid (not 1-65535)
159 /// - An unrecognized argument is provided
160 ///
161 /// # Example
162 ///
163 /// ```rust
164 /// use stem_rs::interpreter::arguments::Arguments;
165 ///
166 /// // Parse port only
167 /// let args = Arguments::parse(&["-i".to_string(), "9051".to_string()]).unwrap();
168 /// assert_eq!(args.control_port, Some(9051));
169 ///
170 /// // Parse address and port
171 /// let args = Arguments::parse(&["-i".to_string(), "192.168.1.1:9051".to_string()]).unwrap();
172 /// assert_eq!(args.control_address, "192.168.1.1");
173 /// assert_eq!(args.control_port, Some(9051));
174 ///
175 /// // Invalid port returns error
176 /// let result = Arguments::parse(&["-i".to_string(), "99999".to_string()]);
177 /// assert!(result.is_err());
178 /// ```
179 pub fn parse(argv: &[String]) -> Result<Self, String> {
180 let mut args = Arguments::default();
181 let mut i = 0;
182
183 while i < argv.len() {
184 let arg = &argv[i];
185 match arg.as_str() {
186 "-i" | "--interface" => {
187 i += 1;
188 if i >= argv.len() {
189 return Err("--interface requires an argument".to_string());
190 }
191 let interface = &argv[i];
192 if let Some((addr, port_str)) = interface.rsplit_once(':') {
193 if !is_valid_ipv4_address(addr) {
194 return Err(format!("'{}' isn't a valid IPv4 address", addr));
195 }
196 if !is_valid_port(port_str) {
197 return Err(format!("'{}' isn't a valid port number", port_str));
198 }
199 args.control_address = addr.to_string();
200 args.control_port = Some(port_str.parse().unwrap());
201 } else {
202 if !is_valid_port(interface) {
203 return Err(format!("'{}' isn't a valid port number", interface));
204 }
205 args.control_port = Some(interface.parse().unwrap());
206 }
207 args.user_provided_port = true;
208 }
209 "-s" | "--socket" => {
210 i += 1;
211 if i >= argv.len() {
212 return Err("--socket requires an argument".to_string());
213 }
214 args.control_socket = argv[i].clone();
215 args.user_provided_socket = true;
216 }
217 "--tor" => {
218 i += 1;
219 if i >= argv.len() {
220 return Err("--tor requires an argument".to_string());
221 }
222 args.tor_path = argv[i].clone();
223 }
224 "--run" => {
225 i += 1;
226 if i >= argv.len() {
227 return Err("--run requires an argument".to_string());
228 }
229 let run_arg = &argv[i];
230 if std::path::Path::new(run_arg).exists() {
231 args.run_path = Some(run_arg.clone());
232 } else {
233 args.run_cmd = Some(run_arg.clone());
234 }
235 }
236 "--no-color" => {
237 args.disable_color = true;
238 }
239 "-h" | "--help" => {
240 args.print_help = true;
241 }
242 other => {
243 return Err(format!(
244 "'{}' isn't a recognized argument (for usage provide --help)",
245 other
246 ));
247 }
248 }
249 i += 1;
250 }
251
252 Ok(args)
253 }
254
255 /// Returns the help message for command-line usage.
256 ///
257 /// The help message includes all available options with their
258 /// descriptions and default values.
259 ///
260 /// # Example
261 ///
262 /// ```rust
263 /// use stem_rs::interpreter::arguments::Arguments;
264 ///
265 /// let help = Arguments::get_help();
266 /// assert!(help.contains("--interface"));
267 /// assert!(help.contains("--socket"));
268 /// assert!(help.contains("--help"));
269 /// ```
270 pub fn get_help() -> String {
271 let defaults = Arguments::default();
272 format!(
273 r#"Interactive interpreter for Tor. This provides you with direct access
274to Tor's control interface via either python or direct requests.
275
276 -i, --interface [ADDRESS:]PORT change control interface from {}:{}
277 -s, --socket SOCKET_PATH attach using unix domain socket if present,
278 SOCKET_PATH defaults to: {}
279 --tor PATH tor binary if tor isn't already running
280 --run executes the given command or file of commands
281 --no-color disables colorized output
282 -h, --help presents this help
283"#,
284 defaults.control_address,
285 defaults
286 .control_port
287 .map(|p| p.to_string())
288 .unwrap_or_else(|| "default".to_string()),
289 defaults.control_socket
290 )
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_default_arguments() {
300 let args = Arguments::default();
301 assert_eq!(args.control_address, "127.0.0.1");
302 assert_eq!(args.control_port, None);
303 assert!(!args.user_provided_port);
304 assert_eq!(args.control_socket, "/var/run/tor/control");
305 assert!(!args.user_provided_socket);
306 assert_eq!(args.tor_path, "tor");
307 assert!(args.run_cmd.is_none());
308 assert!(args.run_path.is_none());
309 assert!(!args.disable_color);
310 assert!(!args.print_help);
311 }
312
313 #[test]
314 fn test_parse_help() {
315 let args = Arguments::parse(&["--help".to_string()]).unwrap();
316 assert!(args.print_help);
317 }
318
319 #[test]
320 fn test_parse_help_short() {
321 let args = Arguments::parse(&["-h".to_string()]).unwrap();
322 assert!(args.print_help);
323 }
324
325 #[test]
326 fn test_parse_interface_port_only() {
327 let args = Arguments::parse(&["-i".to_string(), "9051".to_string()]).unwrap();
328 assert_eq!(args.control_port, Some(9051));
329 assert!(args.user_provided_port);
330 }
331
332 #[test]
333 fn test_parse_interface_address_and_port() {
334 let args =
335 Arguments::parse(&["--interface".to_string(), "192.168.1.1:9051".to_string()]).unwrap();
336 assert_eq!(args.control_address, "192.168.1.1");
337 assert_eq!(args.control_port, Some(9051));
338 assert!(args.user_provided_port);
339 }
340
341 #[test]
342 fn test_parse_socket() {
343 let args = Arguments::parse(&["-s".to_string(), "/tmp/tor.sock".to_string()]).unwrap();
344 assert_eq!(args.control_socket, "/tmp/tor.sock");
345 assert!(args.user_provided_socket);
346 }
347
348 #[test]
349 fn test_parse_tor_path() {
350 let args = Arguments::parse(&["--tor".to_string(), "/usr/bin/tor".to_string()]).unwrap();
351 assert_eq!(args.tor_path, "/usr/bin/tor");
352 }
353
354 #[test]
355 fn test_parse_run_cmd() {
356 let args = Arguments::parse(&["--run".to_string(), "GETINFO version".to_string()]).unwrap();
357 assert_eq!(args.run_cmd, Some("GETINFO version".to_string()));
358 assert!(args.run_path.is_none());
359 }
360
361 #[test]
362 fn test_parse_no_color() {
363 let args = Arguments::parse(&["--no-color".to_string()]).unwrap();
364 assert!(args.disable_color);
365 }
366
367 #[test]
368 fn test_parse_invalid_port() {
369 let result = Arguments::parse(&["-i".to_string(), "99999".to_string()]);
370 assert!(result.is_err());
371 }
372
373 #[test]
374 fn test_parse_invalid_address() {
375 let result = Arguments::parse(&["-i".to_string(), "invalid:9051".to_string()]);
376 assert!(result.is_err());
377 }
378
379 #[test]
380 fn test_parse_unrecognized_argument() {
381 let result = Arguments::parse(&["--unknown".to_string()]);
382 assert!(result.is_err());
383 }
384
385 #[test]
386 fn test_parse_multiple_arguments() {
387 let args = Arguments::parse(&[
388 "-i".to_string(),
389 "9051".to_string(),
390 "-s".to_string(),
391 "/tmp/tor.sock".to_string(),
392 "--no-color".to_string(),
393 ])
394 .unwrap();
395 assert_eq!(args.control_port, Some(9051));
396 assert!(args.user_provided_port);
397 assert_eq!(args.control_socket, "/tmp/tor.sock");
398 assert!(args.user_provided_socket);
399 assert!(args.disable_color);
400 }
401
402 #[test]
403 fn test_get_help() {
404 let help = Arguments::get_help();
405 assert!(help.contains("--interface"));
406 assert!(help.contains("--socket"));
407 assert!(help.contains("--tor"));
408 assert!(help.contains("--run"));
409 assert!(help.contains("--no-color"));
410 assert!(help.contains("--help"));
411 }
412}