standout_pipe/pipe.rs
1use crate::shell::{run_piped, ShellError};
2use std::time::Duration;
3
4#[derive(Debug, thiserror::Error)]
5pub enum PipeError {
6 #[error("Shell error: {0}")]
7 Shell(#[from] ShellError),
8}
9
10/// A target that can receive piped output
11pub trait PipeTarget: Send + Sync {
12 /// Pipe the input to the target and return the resulting output.
13 ///
14 /// If the target is configured to 'capture', the returned string is the command's stdout.
15 /// If the target is 'passthrough', the returned string is the original input.
16 /// If the target is 'consume', the returned string is empty (or filtered out by caller).
17 fn pipe(&self, input: &str) -> Result<String, PipeError>;
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum PipeMode {
22 /// Pipe to command, but ignore its output and return the original input.
23 /// Used for side-effects like logging or clipboard where we still want to see the output.
24 Passthrough,
25 /// Pipe to command and use its output as the new result.
26 /// Used for filters like `jq` or `sort`.
27 Capture,
28 /// Pipe to command and suppress further output.
29 /// Used when the pipe destination is the final consumer (e.g. strict clipboard only).
30 Consume,
31}
32
33/// A simple pipe that executes a shell command with input on stdin.
34///
35/// # Security Warning
36///
37/// The command string is passed directly to the shell (`sh -c` on Unix, `cmd /C` on Windows).
38/// If you construct the command from untrusted input, you risk shell injection attacks.
39///
40/// ```ignore
41/// // DANGEROUS if `user_input` is untrusted:
42/// SimplePipe::new(format!("grep {}", user_input))
43///
44/// // SAFE alternatives:
45/// // 1. Use a fixed command string
46/// SimplePipe::new("grep pattern")
47///
48/// // 2. Validate/sanitize user input before interpolation
49/// let sanitized = sanitize_for_shell(user_input);
50/// SimplePipe::new(format!("grep {}", sanitized))
51/// ```
52pub struct SimplePipe {
53 command: String,
54 mode: PipeMode,
55 timeout: Duration,
56}
57
58impl SimplePipe {
59 /// Create a new pipe that executes the given shell command.
60 ///
61 /// The default mode is [`PipeMode::Passthrough`] with a 30-second timeout.
62 ///
63 /// # Security
64 ///
65 /// See the struct-level documentation for shell injection warnings.
66 pub fn new(command: impl Into<String>) -> Self {
67 Self {
68 command: command.into(),
69 mode: PipeMode::Passthrough,
70 timeout: Duration::from_secs(30),
71 }
72 }
73
74 /// Use the command's stdout as the new output.
75 pub fn capture(mut self) -> Self {
76 self.mode = PipeMode::Capture;
77 self
78 }
79
80 /// Don't print anything to the terminal after piping.
81 pub fn consume(mut self) -> Self {
82 self.mode = PipeMode::Consume;
83 self
84 }
85
86 pub fn with_timeout(mut self, timeout: Duration) -> Self {
87 self.timeout = timeout;
88 self
89 }
90}
91
92impl PipeTarget for SimplePipe {
93 fn pipe(&self, input: &str) -> Result<String, PipeError> {
94 let cmd_output = run_piped(&self.command, input, Some(self.timeout))?;
95
96 match self.mode {
97 PipeMode::Passthrough => Ok(input.to_string()),
98 PipeMode::Capture => Ok(cmd_output),
99 PipeMode::Consume => Ok(String::new()),
100 }
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn test_simple_pipe_passthrough() {
110 let pipe = SimplePipe::new(if cfg!(windows) {
111 "findstr foo"
112 } else {
113 "grep foo"
114 });
115 // Passthrough should return ORIGINAL input, but the command is executed.
116 let input = "foo\nbar";
117 let output = pipe.pipe(input).unwrap();
118 assert_eq!(output, "foo\nbar");
119 }
120
121 #[test]
122 fn test_simple_pipe_capture() {
123 let pipe = SimplePipe::new(if cfg!(windows) {
124 "findstr foo"
125 } else {
126 "grep foo"
127 })
128 .capture();
129 let input = "foo\nbar";
130 let output = pipe.pipe(input).unwrap();
131 assert_eq!(output.trim(), "foo");
132 }
133
134 #[test]
135 fn test_simple_pipe_consume() {
136 let pipe = SimplePipe::new(if cfg!(windows) {
137 "findstr foo"
138 } else {
139 "grep foo"
140 })
141 .consume();
142 let input = "foo\nbar";
143 let output = pipe.pipe(input).unwrap();
144 assert_eq!(output, "");
145 }
146}