use async_trait::async_trait;
use super::{Builtin, Context};
use crate::error::Result;
use crate::interpreter::ExecResult;
pub struct Echo;
#[async_trait]
impl Builtin for Echo {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
if let Some(r) = super::check_help_version(
ctx.args,
"Usage: echo [SHORT-OPTION]... [STRING]...\n or: echo LONG-OPTION\nEcho the STRING(s) to standard output.\n\n -n\tdo not output the trailing newline\n -e\tenable interpretation of backslash escapes\n -E\tdisable interpretation of backslash escapes (default)\n --help\tdisplay this help and exit\n --version\toutput version information and exit\n",
Some("echo (bashkit) 0.1"),
) {
return Ok(r);
}
let mut output = String::new();
let mut add_newline = true;
let mut interpret_escapes = false;
let mut args_iter = ctx.args.iter().peekable();
while let Some(arg) = args_iter.peek() {
let arg_str = arg.as_str();
if arg_str.starts_with('-') && arg_str.len() > 1 && !arg_str.starts_with("--") {
let mut is_valid_option = true;
for c in arg_str[1..].chars() {
if !matches!(c, 'n' | 'e' | 'E') {
is_valid_option = false;
break;
}
}
if is_valid_option {
for c in arg_str[1..].chars() {
match c {
'n' => add_newline = false,
'e' => interpret_escapes = true,
'E' => interpret_escapes = false,
_ => {}
}
}
args_iter.next();
} else {
break;
}
} else {
break;
}
}
let remaining: Vec<&String> = args_iter.collect();
for (i, arg) in remaining.iter().enumerate() {
if i > 0 {
output.push(' ');
}
if interpret_escapes {
output.push_str(&interpret_escape_sequences(arg));
} else {
output.push_str(arg);
}
}
if add_newline {
output.push('\n');
}
Ok(ExecResult::ok(output))
}
}
fn interpret_escape_sequences(s: &str) -> String {
let mut result = String::new();
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('t') => result.push('\t'),
Some('r') => result.push('\r'),
Some('\\') => result.push('\\'),
Some('a') => result.push('\x07'), Some('b') => result.push('\x08'), Some('f') => result.push('\x0c'), Some('v') => result.push('\x0b'), Some('0') => {
let mut value = 0u8;
for _ in 0..3 {
if let Some(&digit) = chars.peek() {
if ('0'..='7').contains(&digit) {
value = value * 8 + (digit as u8 - b'0');
chars.next();
} else {
break;
}
}
}
result.push(value as char);
}
Some('x') => {
let mut value = 0u8;
for _ in 0..2 {
if let Some(&digit) = chars.peek() {
if digit.is_ascii_hexdigit() {
value = value * 16
+ digit.to_digit(16).expect(
"to_digit(16) valid: guarded by is_ascii_hexdigit()",
) as u8;
chars.next();
} else {
break;
}
}
}
result.push(value as char);
}
Some('c') => {
break;
}
Some(other) => {
result.push('\\');
result.push(other);
}
None => result.push('\\'),
}
} else {
result.push(ch);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_sequences() {
assert_eq!(interpret_escape_sequences("hello\\nworld"), "hello\nworld");
assert_eq!(interpret_escape_sequences("tab\\there"), "tab\there");
assert_eq!(interpret_escape_sequences("\\\\backslash"), "\\backslash");
}
}