use clap::{Arg, Command};
use color_eyre::Result;
#[derive(Default)]
pub(crate) enum EscapeMode {
#[default]
Disabled,
Enabled,
}
pub struct EchoOptions {
pub text: Vec<String>,
pub no_newline: bool,
pub escape_mode: EscapeMode,
}
enum EscapeResult {
Continue,
Stop,
}
pub fn parse_options(matches: &clap::ArgMatches) -> Result<EchoOptions> {
let mut text: Vec<String> = matches.get_many::<String>("text")
.map(|vals| vals.cloned().collect())
.unwrap_or_default();
let mut no_newline = false;
let mut escape_mode = EscapeMode::Disabled;
let mut iter = text.drain(..);
let mut processed_text = Vec::new();
while let Some(arg) = iter.next() {
if arg == "--" {
processed_text.extend(iter);
break;
}
if arg.starts_with('-') && arg.len() > 1 {
let mut chars = arg[1..].chars();
let mut temp_no_newline = false;
let mut temp_escape_mode = None;
let mut all_valid = true;
while let Some(c) = chars.next() {
match c {
'n' => temp_no_newline = true,
'e' => temp_escape_mode = Some(EscapeMode::Enabled),
'E' => temp_escape_mode = Some(EscapeMode::Disabled),
_ => {
all_valid = false;
break;
}
}
}
if all_valid {
no_newline = temp_no_newline;
if let Some(mode) = temp_escape_mode {
escape_mode = mode;
}
} else {
processed_text.push(arg);
processed_text.extend(iter);
break;
}
} else {
processed_text.push(arg);
processed_text.extend(iter);
break;
}
}
Ok(EchoOptions {
text: processed_text,
no_newline,
escape_mode,
})
}
pub fn command() -> Command {
Command::new("echo")
.about("Display a line of text")
.ignore_errors(true)
.allow_hyphen_values(true)
.arg(Arg::new("text")
.num_args(0..)
.allow_hyphen_values(true)
.help("Text to display"))
}
fn handle_octal_escape(chars: &mut std::iter::Peekable<std::str::Chars>, first_digit: char) -> String {
let max_extra = if first_digit == '0' { 3 } else { 2 };
let mut octal = String::new();
octal.push(first_digit);
for _ in 0..max_extra {
match chars.peek() {
Some('0'..='7') => octal.push(chars.next().unwrap()),
_ => break,
}
}
match u32::from_str_radix(&octal, 8) {
Ok(val) if val <= 0xFF => {
char::from(val as u8).to_string()
}
_ => {
format!("\\{}", octal)
}
}
}
fn handle_hex_escape(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
let mut hex = String::new();
for _ in 0..2 {
match chars.peek() {
Some('0'..='9') | Some('a'..='f') | Some('A'..='F') => hex.push(chars.next().unwrap()),
_ => break,
}
}
if hex.is_empty() {
"x".to_string()
} else {
match u32::from_str_radix(&hex, 16) {
Ok(val) if val <= 0xFF => {
char::from(val as u8).to_string()
}
_ => {
format!("x{}", hex)
}
}
}
}
fn handle_escape(
chars: &mut std::iter::Peekable<std::str::Chars>,
result: &mut String,
) -> EscapeResult {
match chars.next() {
Some('\\') => result.push('\\'),
Some('a') => result.push('\x07'),
Some('b') => result.push('\x08'),
Some('c') => return EscapeResult::Stop,
Some('e') => result.push('\x1b'),
Some('f') => result.push('\x0c'),
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some('v') => result.push('\x0b'),
Some(d) if d.is_digit(8) => result.push_str(&handle_octal_escape(chars, d)),
Some('x') => result.push_str(&handle_hex_escape(chars)),
Some(other) => {
result.push('\\');
result.push(other);
}
None => {
result.push('\\');
}
}
EscapeResult::Continue
}
fn process_escapes(s: &str) -> (String, bool) {
let mut result = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c != '\\' {
result.push(c);
continue;
}
if let EscapeResult::Stop = handle_escape(&mut chars, &mut result) {
return (result, true);
}
}
(result, false)
}
pub fn run(options: EchoOptions) -> Result<()> {
let text = options.text.join(" ");
let (output, cancel_newline) = match options.escape_mode {
EscapeMode::Enabled => process_escapes(&text),
EscapeMode::Disabled => (text, false),
};
let no_newline = options.no_newline || cancel_newline;
if no_newline {
print!("{}", output);
} else {
println!("{}", output);
}
Ok(())
}