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}