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.
25pub fn parse_echo_args(args: &[String]) -> (EchoConfig, &[String]) {
26    let mut config = EchoConfig::default();
27    let mut idx = 0;
28
29    for arg in args {
30        let bytes = arg.as_bytes();
31        // Must start with '-' and have at least one flag character
32        if bytes.len() < 2 || bytes[0] != b'-' {
33            break;
34        }
35        // Every character after '-' must be n, e, or E
36        let all_flags = bytes[1..]
37            .iter()
38            .all(|&b| b == b'n' || b == b'e' || b == b'E');
39        if !all_flags {
40            break;
41        }
42        // Apply flags
43        for &b in &bytes[1..] {
44            match b {
45                b'n' => config.trailing_newline = false,
46                b'e' => config.interpret_escapes = true,
47                b'E' => config.interpret_escapes = false,
48                _ => unreachable!(),
49            }
50        }
51        idx += 1;
52    }
53
54    (config, &args[idx..])
55}
56
57/// Produce the output bytes for an echo invocation.
58///
59/// The returned `Vec<u8>` contains exactly the bytes that should be written to
60/// stdout (including or excluding the trailing newline, and with escape
61/// sequences expanded when `config.interpret_escapes` is true).
62pub fn echo_output(args: &[String], config: &EchoConfig) -> Vec<u8> {
63    let mut out: Vec<u8> = Vec::new();
64
65    for (i, arg) in args.iter().enumerate() {
66        if i > 0 {
67            out.push(b' ');
68        }
69        if config.interpret_escapes {
70            if !expand_escapes(arg.as_bytes(), &mut out) {
71                // \c encountered — stop all output immediately
72                return out;
73            }
74        } else {
75            out.extend_from_slice(arg.as_bytes());
76        }
77    }
78
79    if config.trailing_newline {
80        out.push(b'\n');
81    }
82
83    out
84}
85
86/// Expand backslash escape sequences in `src`, appending the result to `out`.
87///
88/// Returns `true` if all bytes were processed normally, or `false` if `\c`
89/// was encountered (meaning output should stop immediately).
90fn expand_escapes(src: &[u8], out: &mut Vec<u8>) -> bool {
91    let len = src.len();
92    let mut i = 0;
93
94    while i < len {
95        if src[i] != b'\\' {
96            out.push(src[i]);
97            i += 1;
98            continue;
99        }
100
101        // We have a backslash — look at the next character
102        i += 1;
103        if i >= len {
104            // Trailing backslash with nothing after — output it literally
105            out.push(b'\\');
106            break;
107        }
108
109        match src[i] {
110            b'\\' => out.push(b'\\'),
111            b'a' => out.push(0x07),
112            b'b' => out.push(0x08),
113            b'c' => return false, // stop output
114            b'e' => out.push(0x1B),
115            b'f' => out.push(0x0C),
116            b'n' => out.push(b'\n'),
117            b'r' => out.push(b'\r'),
118            b't' => out.push(b'\t'),
119            b'v' => out.push(0x0B),
120            b'0' => {
121                // \0NNN — octal (up to 3 octal digits after the '0')
122                let start = i + 1;
123                let mut end = start;
124                while end < len && end < start + 3 && src[end] >= b'0' && src[end] <= b'7' {
125                    end += 1;
126                }
127                let val = if start == end {
128                    0u8 // \0 with no digits = NUL
129                } else {
130                    parse_octal(&src[start..end])
131                };
132                out.push(val);
133                i = end - 1; // will be incremented at end of loop
134            }
135            b'x' => {
136                // \xHH — hexadecimal (up to 2 hex digits)
137                let start = i + 1;
138                let mut end = start;
139                while end < len && end < start + 2 && is_hex_digit(src[end]) {
140                    end += 1;
141                }
142                if start == end {
143                    // No hex digits: output \x literally
144                    out.push(b'\\');
145                    out.push(b'x');
146                } else {
147                    out.push(parse_hex(&src[start..end]));
148                    i = end - 1; // will be incremented at end of loop
149                }
150            }
151            other => {
152                // Unknown escape — output the backslash and the character literally
153                out.push(b'\\');
154                out.push(other);
155            }
156        }
157        i += 1;
158    }
159
160    true
161}
162
163#[inline]
164fn is_hex_digit(b: u8) -> bool {
165    b.is_ascii_hexdigit()
166}
167
168fn parse_octal(digits: &[u8]) -> u8 {
169    let mut val: u16 = 0;
170    for &d in digits {
171        val = val * 8 + (d - b'0') as u16;
172    }
173    val as u8
174}
175
176fn parse_hex(digits: &[u8]) -> u8 {
177    let mut val: u8 = 0;
178    for &d in digits {
179        let nibble = match d {
180            b'0'..=b'9' => d - b'0',
181            b'a'..=b'f' => d - b'a' + 10,
182            b'A'..=b'F' => d - b'A' + 10,
183            _ => unreachable!(),
184        };
185        val = val * 16 + nibble;
186    }
187    val
188}