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}