Skip to main content

coreutils_rs/stdbuf/
core.rs

1/// stdbuf -- run a command with modified buffering for its standard streams
2///
3/// Sets environment variables _STDBUF_I, _STDBUF_O, _STDBUF_E to communicate
4/// the desired buffering modes to the child process. A full implementation would
5/// use LD_PRELOAD with a shared library that reads these variables and calls
6/// setvbuf(). This implementation sets the environment and execs the command.
7use std::io;
8use std::process;
9
10/// Buffering mode specification.
11#[derive(Clone, Debug)]
12pub enum BufferMode {
13    /// Line buffered (mode "L")
14    Line,
15    /// Unbuffered (mode "0")
16    Unbuffered,
17    /// Fully buffered with a specific size in bytes
18    Size(usize),
19}
20
21impl BufferMode {
22    /// Convert to the environment variable value string.
23    pub fn to_env_value(&self) -> String {
24        match self {
25            BufferMode::Line => "L".to_string(),
26            BufferMode::Unbuffered => "0".to_string(),
27            BufferMode::Size(n) => n.to_string(),
28        }
29    }
30}
31
32/// Configuration for the stdbuf command.
33#[derive(Clone, Debug)]
34pub struct StdbufConfig {
35    pub input: Option<BufferMode>,
36    pub output: Option<BufferMode>,
37    pub error: Option<BufferMode>,
38    pub command: String,
39    pub args: Vec<String>,
40}
41
42/// Parse a buffer mode string into a BufferMode.
43///
44/// Accepted formats:
45/// - "L" or "l" -> Line buffered
46/// - "0" -> Unbuffered
47/// - A positive integer (optionally with K, M, G, T suffix) -> Size buffered
48pub fn parse_buffer_mode(s: &str) -> Result<BufferMode, String> {
49    if s.eq_ignore_ascii_case("L") {
50        return Ok(BufferMode::Line);
51    }
52    if s == "0" {
53        return Ok(BufferMode::Unbuffered);
54    }
55
56    // Parse size with optional suffix
57    let (num_str, multiplier) =
58        if let Some(prefix) = s.strip_suffix('K').or_else(|| s.strip_suffix('k')) {
59            (prefix, 1024_usize)
60        } else if let Some(prefix) = s.strip_suffix('M').or_else(|| s.strip_suffix('m')) {
61            (prefix, 1024 * 1024)
62        } else if let Some(prefix) = s.strip_suffix('G').or_else(|| s.strip_suffix('g')) {
63            (prefix, 1024 * 1024 * 1024)
64        } else if let Some(prefix) = s.strip_suffix('T').or_else(|| s.strip_suffix('t')) {
65            (prefix, 1024_usize.wrapping_mul(1024 * 1024 * 1024))
66        } else if let Some(prefix) = s.strip_suffix("KB").or_else(|| s.strip_suffix("kB")) {
67            (prefix, 1000)
68        } else if let Some(prefix) = s.strip_suffix("MB") {
69            (prefix, 1_000_000)
70        } else if let Some(prefix) = s.strip_suffix("GB") {
71            (prefix, 1_000_000_000)
72        } else {
73            (s, 1)
74        };
75
76    let n: usize = num_str
77        .parse()
78        .map_err(|_| format!("invalid mode '{}'", s))?;
79
80    if n == 0 && multiplier == 1 {
81        return Ok(BufferMode::Unbuffered);
82    }
83
84    let size = n
85        .checked_mul(multiplier)
86        .ok_or_else(|| format!("mode size too large: '{}'", s))?;
87
88    if size == 0 {
89        Ok(BufferMode::Unbuffered)
90    } else {
91        Ok(BufferMode::Size(size))
92    }
93}
94
95/// Run the stdbuf command: set environment variables and exec the child process.
96pub fn run_stdbuf(config: &StdbufConfig) -> io::Result<()> {
97    let mut cmd = process::Command::new(&config.command);
98    cmd.args(&config.args);
99
100    if let Some(ref mode) = config.input {
101        cmd.env("_STDBUF_I", mode.to_env_value());
102    }
103    if let Some(ref mode) = config.output {
104        cmd.env("_STDBUF_O", mode.to_env_value());
105    }
106    if let Some(ref mode) = config.error {
107        cmd.env("_STDBUF_E", mode.to_env_value());
108    }
109
110    let status = cmd.status()?;
111    process::exit(status.code().unwrap_or(125));
112}