Skip to main content

coreutils_rs/echo/
core.rs

1/// Configuration for the echo command.
2pub struct EchoConfig {
3    /// Whether to append a trailing newline (true by default; `-n` disables it).
4    pub trailing_newline: bool,
5    /// Whether to interpret backslash escape sequences (`-e` enables, `-E` disables).
6    pub interpret_escapes: bool,
7}
8
9impl Default for EchoConfig {
10    fn default() -> Self {
11        Self {
12            trailing_newline: true,
13            interpret_escapes: false,
14        }
15    }
16}
17
18/// Parse the raw command-line arguments (after the program name) into an
19/// `EchoConfig` and the remaining text arguments.
20///
21/// GNU echo uses *manual* flag parsing: a leading argument is only treated as
22/// flags if it starts with `-` and every subsequent character is one of `n`,
23/// `e`, or `E`.  Combined flags like `-neE` are valid.  Anything else (e.g.
24/// `-z`, `--foo`, or even `-`) is treated as a normal text argument.
25///
26/// When `POSIXLY_CORRECT` is set, escapes are always interpreted.
27/// GNU coreutils 9.x: if the FIRST arg is exactly "-n", recognize it and
28/// subsequent option-like args (including combined flags like -nE, -ne).
29/// Only -n has effect (suppress newline); -e/-E are consumed but ignored
30/// (escapes stay on). If the first arg is NOT "-n", no options recognized.
31pub fn parse_echo_args(args: &[String]) -> (EchoConfig, &[String]) {
32    // POSIXLY_CORRECT: escapes always interpreted
33    if std::env::var_os("POSIXLY_CORRECT").is_some() {
34        let mut config = EchoConfig {
35            trailing_newline: true,
36            interpret_escapes: true,
37        };
38        // Only recognize options if first arg is exactly "-n"
39        if args.first().map(|s| s.as_str()) == Some("-n") {
40            config.trailing_newline = false;
41            let mut idx = 1;
42            // Consume subsequent option-like args
43            for arg in &args[1..] {
44                let bytes = arg.as_bytes();
45                if bytes.len() < 2 || bytes[0] != b'-' {
46                    break;
47                }
48                let all_flags = bytes[1..]
49                    .iter()
50                    .all(|&b| b == b'n' || b == b'e' || b == b'E');
51                if !all_flags {
52                    break;
53                }
54                for &b in &bytes[1..] {
55                    if b == b'n' {
56                        config.trailing_newline = false;
57                    }
58                }
59                idx += 1;
60            }
61            return (config, &args[idx..]);
62        }
63        return (config, args);
64    }
65
66    let mut config = EchoConfig::default();
67    let mut idx = 0;
68
69    for arg in args {
70        let bytes = arg.as_bytes();
71        // Must start with '-' and have at least one flag character
72        if bytes.len() < 2 || bytes[0] != b'-' {
73            break;
74        }
75        // Every character after '-' must be n, e, or E
76        let all_flags = bytes[1..]
77            .iter()
78            .all(|&b| b == b'n' || b == b'e' || b == b'E');
79        if !all_flags {
80            break;
81        }
82        // Apply flags
83        for &b in &bytes[1..] {
84            match b {
85                b'n' => config.trailing_newline = false,
86                b'e' => config.interpret_escapes = true,
87                b'E' => config.interpret_escapes = false,
88                _ => unreachable!(),
89            }
90        }
91        idx += 1;
92    }
93
94    (config, &args[idx..])
95}
96
97/// Produce the output bytes for an echo invocation.
98///
99/// The returned `Vec<u8>` contains exactly the bytes that should be written to
100/// stdout (including or excluding the trailing newline, and with escape
101/// sequences expanded when `config.interpret_escapes` is true).
102pub fn echo_output(args: &[String], config: &EchoConfig) -> Vec<u8> {
103    let mut out: Vec<u8> = Vec::new();
104
105    for (i, arg) in args.iter().enumerate() {
106        if i > 0 {
107            out.push(b' ');
108        }
109        if config.interpret_escapes {
110            if !expand_escapes(arg.as_bytes(), &mut out) {
111                // \c encountered — stop all output immediately
112                return out;
113            }
114        } else {
115            out.extend_from_slice(arg.as_bytes());
116        }
117    }
118
119    if config.trailing_newline {
120        out.push(b'\n');
121    }
122
123    out
124}
125
126/// Expand backslash escape sequences in `src`, appending the result to `out`.
127///
128/// Returns `true` if all bytes were processed normally, or `false` if `\c`
129/// was encountered (meaning output should stop immediately).
130fn expand_escapes(src: &[u8], out: &mut Vec<u8>) -> bool {
131    let len = src.len();
132    let mut i = 0;
133
134    while i < len {
135        if src[i] != b'\\' {
136            out.push(src[i]);
137            i += 1;
138            continue;
139        }
140
141        // We have a backslash — look at the next character
142        i += 1;
143        if i >= len {
144            // Trailing backslash with nothing after — output it literally
145            out.push(b'\\');
146            break;
147        }
148
149        match src[i] {
150            b'\\' => out.push(b'\\'),
151            b'a' => out.push(0x07),
152            b'b' => out.push(0x08),
153            b'c' => return false, // stop output
154            b'e' => out.push(0x1B),
155            b'f' => out.push(0x0C),
156            b'n' => out.push(b'\n'),
157            b'r' => out.push(b'\r'),
158            b't' => out.push(b'\t'),
159            b'v' => out.push(0x0B),
160            b'0' => {
161                // \0NNN — octal with \0 prefix (up to 3 more octal digits)
162                // GNU echo treats \0 as a prefix: read up to 3 MORE digits
163                let mut val: u16 = 0;
164                let mut consumed = 0;
165                let mut j = i + 1;
166                while j < len && consumed < 3 && src[j] >= b'0' && src[j] <= b'7' {
167                    val = val * 8 + (src[j] - b'0') as u16;
168                    j += 1;
169                    consumed += 1;
170                }
171                out.push(val as u8);
172                i = j - 1; // will be incremented at end of loop
173            }
174            b'1'..=b'7' => {
175                // \NNN — octal (up to 3 total digits including the first)
176                let first = src[i] - b'0';
177                let mut val = first as u16;
178                let mut consumed = 0;
179                let mut j = i + 1;
180                while j < len && consumed < 2 && src[j] >= b'0' && src[j] <= b'7' {
181                    val = val * 8 + (src[j] - b'0') as u16;
182                    j += 1;
183                    consumed += 1;
184                }
185                out.push(val as u8);
186                i = j - 1; // will be incremented at end of loop
187            }
188            b'x' => {
189                // \xHH — hexadecimal (up to 2 hex digits)
190                let start = i + 1;
191                let mut end = start;
192                while end < len && end < start + 2 && is_hex_digit(src[end]) {
193                    end += 1;
194                }
195                if start == end {
196                    // No hex digits: output \x literally
197                    out.push(b'\\');
198                    out.push(b'x');
199                } else {
200                    out.push(parse_hex(&src[start..end]));
201                    i = end - 1; // will be incremented at end of loop
202                }
203            }
204            other => {
205                // Unknown escape — output the backslash and the character literally
206                out.push(b'\\');
207                out.push(other);
208            }
209        }
210        i += 1;
211    }
212
213    true
214}
215
216#[inline]
217fn is_hex_digit(b: u8) -> bool {
218    b.is_ascii_hexdigit()
219}
220
221fn parse_hex(digits: &[u8]) -> u8 {
222    let mut val: u8 = 0;
223    for &d in digits {
224        let nibble = match d {
225            b'0'..=b'9' => d - b'0',
226            b'a'..=b'f' => d - b'a' + 10,
227            b'A'..=b'F' => d - b'A' + 10,
228            _ => unreachable!(),
229        };
230        val = val * 16 + nibble;
231    }
232    val
233}