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" -> Line buffered (only uppercase; lowercase "l" is rejected per GNU stdbuf)
46/// - "0" -> Unbuffered
47/// - A positive integer (optionally with K, M, G, T, KB, kB, MB, GB suffix) -> Size buffered
48/// - A bare suffix like "K" is treated as "1K" (= 1024) per GNU stdbuf
49pub fn parse_buffer_mode(s: &str) -> Result<BufferMode, String> {
50    // Only uppercase L is accepted for line buffering (GNU stdbuf rejects lowercase)
51    if s == "L" {
52        return Ok(BufferMode::Line);
53    }
54    if s == "0" {
55        return Ok(BufferMode::Unbuffered);
56    }
57
58    // Parse size with optional suffix
59    // Check multi-char suffixes first to avoid partial matches
60    let (num_str, multiplier) =
61        if let Some(prefix) = s.strip_suffix("KB").or_else(|| s.strip_suffix("kB")) {
62            (prefix, 1000_usize)
63        } else if let Some(prefix) = s.strip_suffix("MB") {
64            (prefix, 1_000_000)
65        } else if let Some(prefix) = s.strip_suffix("GB") {
66            (prefix, 1_000_000_000)
67        } else if let Some(prefix) = s.strip_suffix('K').or_else(|| s.strip_suffix('k')) {
68            (prefix, 1024_usize)
69        } else if let Some(prefix) = s.strip_suffix('M').or_else(|| s.strip_suffix('m')) {
70            (prefix, 1024 * 1024)
71        } else if let Some(prefix) = s.strip_suffix('G').or_else(|| s.strip_suffix('g')) {
72            (prefix, 1024 * 1024 * 1024)
73        } else if let Some(prefix) = s.strip_suffix('T').or_else(|| s.strip_suffix('t')) {
74            (prefix, 1024_usize.wrapping_mul(1024 * 1024 * 1024))
75        } else {
76            (s, 1)
77        };
78
79    // A bare suffix with no number prefix is treated as 1 * multiplier (e.g. "K" = 1024)
80    let n: usize = if num_str.is_empty() {
81        1
82    } else {
83        num_str
84            .parse()
85            .map_err(|_| format!("invalid mode '{}'", s))?
86    };
87
88    if n == 0 && multiplier == 1 {
89        return Ok(BufferMode::Unbuffered);
90    }
91
92    let size = n
93        .checked_mul(multiplier)
94        .ok_or_else(|| format!("mode size too large: '{}'", s))?;
95
96    if size == 0 {
97        Ok(BufferMode::Unbuffered)
98    } else {
99        Ok(BufferMode::Size(size))
100    }
101}
102
103/// Run the stdbuf command: set environment variables and exec the child process.
104pub fn run_stdbuf(config: &StdbufConfig) -> io::Result<()> {
105    let mut cmd = process::Command::new(&config.command);
106    cmd.args(&config.args);
107
108    if let Some(ref mode) = config.input {
109        cmd.env("_STDBUF_I", mode.to_env_value());
110    }
111    if let Some(ref mode) = config.output {
112        cmd.env("_STDBUF_O", mode.to_env_value());
113    }
114    if let Some(ref mode) = config.error {
115        cmd.env("_STDBUF_E", mode.to_env_value());
116    }
117
118    let status = cmd.status()?;
119    process::exit(status.code().unwrap_or(125));
120}