1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

echo: rewrite for readability and tests

This commit is contained in:
Christopher Brown 2019-04-05 18:55:48 -04:00
parent 8cadaa4664
commit 1e223b0b58
2 changed files with 92 additions and 103 deletions

View file

@ -4,6 +4,7 @@
* This file is part of the uutils coreutils package. * This file is part of the uutils coreutils package.
* *
* (c) Derek Chiang <derekchiang93@gmail.com> * (c) Derek Chiang <derekchiang93@gmail.com>
* (c) Christopher Brown <ccbrown112@gmail.com>
* *
* For the full copyright and license information, please view the LICENSE * For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code. * file that was distributed with this source code.
@ -12,13 +13,12 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use std::io::{stdout, Write}; use std::iter::Peekable;
use std::str::from_utf8; use std::str::Chars;
#[allow(dead_code)] const SYNTAX: &str = "[OPTIONS]... [STRING]...";
static SYNTAX: &str = "[OPTIONS]... [STRING]..."; const SUMMARY: &str = "display a line of text";
static SUMMARY: &str = "display a line of text"; const HELP: &str = r#"
static HELP: &str = r#"
Echo the STRING(s) to standard output. Echo the STRING(s) to standard output.
If -e is in effect, the following sequences are recognized: If -e is in effect, the following sequences are recognized:
@ -36,43 +36,53 @@ static HELP: &str = r#"
\\xHH byte with hexadecimal value HH (1 to 2 digits) \\xHH byte with hexadecimal value HH (1 to 2 digits)
"#; "#;
enum Base { fn parse_code(input: &mut Peekable<Chars>, base: u32, max_digits: u32, bits_per_digit: u32) -> Option<char> {
B8 = 8, let mut ret = 0x80000000;
B16 = 16, for _ in 0..max_digits {
} match input.peek().and_then(|c| c.to_digit(base)) {
Some(n) => ret = (ret << bits_per_digit) | n,
struct Opts { None => break,
newline: bool,
escape: bool,
}
fn convert_str(string: &[u8], index: usize, base: Base) -> (char, usize) {
let (max_digits, is_legal_digit): (usize, fn(u8) -> bool) = match base {
Base::B8 => (3, |c| (c as char).is_digit(8)),
Base::B16 => (2, |c| (c as char).is_digit(16)),
};
let mut bytes = vec![];
for offset in 0..max_digits {
if string.len() <= index + offset as usize {
break;
}
let c = string[index + offset as usize];
if is_legal_digit(c) {
bytes.push(c as u8);
} else {
break;
} }
input.next();
} }
std::char::from_u32(ret)
}
if bytes.is_empty() { fn print_escaped(input: &str, should_stop: &mut bool) {
(' ', 0) let mut iter = input.chars().peekable();
} else { while let Some(mut c) = iter.next() {
( if c == '\\' {
usize::from_str_radix(from_utf8(bytes.as_ref()).unwrap(), base as u32).unwrap() as u8 if let Some(next) = iter.next() {
as char, c = match next {
bytes.len(), '\\' => '\\',
) 'a' => '\x07',
'b' => '\x08',
'c' => {
*should_stop = true;
break
},
'e' => '\x1b',
'f' => '\x0c',
'n' => '\n',
'r' => '\r',
't' => '\t',
'v' => '\x0b',
'x' => parse_code(&mut iter, 16, 2, 4).unwrap_or_else(|| {
print!("\\");
next
}),
'0' => parse_code(&mut iter, 8, 3, 3).unwrap_or_else(|| {
print!("\\");
next
}),
_ => {
print!("\\");
next
},
};
}
}
print!("{}", c);
} }
} }
@ -80,75 +90,29 @@ pub fn uumain(args: Vec<String>) -> i32 {
let matches = new_coreopts!(SYNTAX, SUMMARY, HELP) let matches = new_coreopts!(SYNTAX, SUMMARY, HELP)
.optflag("n", "", "do not output the trailing newline") .optflag("n", "", "do not output the trailing newline")
.optflag("e", "", "enable interpretation of backslash escapes") .optflag("e", "", "enable interpretation of backslash escapes")
.optflag( .optflag("E", "", "disable interpretation of backslash escapes (default)")
"E",
"",
"disable interpretation of backslash escapes (default)",
)
.parse(args); .parse(args);
let options = Opts { let no_newline = matches.opt_present("n");
newline: matches.opt_present("n"), let escaped = matches.opt_present("e");
escape: matches.opt_present("e"),
}; for (i, input) in matches.free.iter().enumerate() {
let free = matches.free; if i > 0 {
if !free.is_empty() { print!(" ");
let string = free.join(" "); }
if options.escape { if escaped {
let mut prev_was_slash = false; let mut should_stop = false;
let mut iter = string.chars().enumerate(); print_escaped(&input, &mut should_stop);
while let Some((mut idx, c)) = iter.next() { if should_stop {
prev_was_slash = if !prev_was_slash { break;
if c != '\\' {
print!("{}", c);
false
} else {
true
}
} else {
match c {
'\\' => print!("\\"),
'n' => print!("\n"),
'r' => print!("\r"),
't' => print!("\t"),
'v' => print!("\x0B"),
'a' => print!("\x07"),
'b' => print!("\x08"),
'c' => break,
'e' => print!("\x1B"),
'f' => print!("\x0C"),
ch => {
// 'x' or '0' or _
idx = if ch == 'x' || ch == '0' { idx + 1 } else { idx };
let base = if ch == 'x' { Base::B16 } else { Base::B8 };
match convert_str(string.as_bytes(), idx, base) {
(_, 0) => match ch {
'x' => print!("\\x"),
'0' => print!("\0"),
_ => print!("\\{}", c),
},
(c, num_char_used) => {
print!("{}", c);
let beg = if ch == 'x' || ch == '0' { 0 } else { 1 };
for _ in beg..num_char_used {
iter.next(); // consume used characters
}
}
}
}
}
false
}
} }
} else { } else {
print!("{}", string); print!("{}", input);
} }
} }
if options.newline { if !no_newline {
return_if_err!(1, stdout().flush()) println!();
} else {
println!()
} }
0 0

View file

@ -48,6 +48,21 @@ fn test_escape_hex() {
new_ucmd!().args(&["-e", "\\x41"]).succeeds().stdout_only("A"); new_ucmd!().args(&["-e", "\\x41"]).succeeds().stdout_only("A");
} }
#[test]
fn test_escape_short_hex() {
new_ucmd!().args(&["-e", "foo\\xa bar"]).succeeds().stdout_only("foo\n bar");
}
#[test]
fn test_escape_no_hex() {
new_ucmd!().args(&["-e", "foo\\x bar"]).succeeds().stdout_only("foo\\x bar");
}
#[test]
fn test_escape_one_slash() {
new_ucmd!().args(&["-e", "foo\\ bar"]).succeeds().stdout_only("foo\\ bar");
}
#[test] #[test]
fn test_escape_newline() { fn test_escape_newline() {
new_ucmd!().args(&["-e", "\\na"]).succeeds().stdout_only("\na"); new_ucmd!().args(&["-e", "\\na"]).succeeds().stdout_only("\na");
@ -55,7 +70,7 @@ fn test_escape_newline() {
#[test] #[test]
fn test_escape_no_further_output() { fn test_escape_no_further_output() {
new_ucmd!().args(&["-e", "a\\cb"]).succeeds().stdout_only("a\n"); new_ucmd!().args(&["-e", "a\\cb", "c"]).succeeds().stdout_only("a\n");
} }
#[test] #[test]
@ -63,6 +78,16 @@ fn test_escape_octal() {
new_ucmd!().args(&["-e", "\\0100"]).succeeds().stdout_only("@"); new_ucmd!().args(&["-e", "\\0100"]).succeeds().stdout_only("@");
} }
#[test]
fn test_escape_short_octal() {
new_ucmd!().args(&["-e", "foo\\040bar"]).succeeds().stdout_only("foo bar");
}
#[test]
fn test_escape_no_octal() {
new_ucmd!().args(&["-e", "foo\\0 bar"]).succeeds().stdout_only("foo\\0 bar");
}
#[test] #[test]
fn test_escape_tab() { fn test_escape_tab() {
new_ucmd!().args(&["-e", "\\t"]).succeeds().stdout_only("\t\n"); new_ucmd!().args(&["-e", "\\t"]).succeeds().stdout_only("\t\n");