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

Merge pull request #1354 from ccbrown/rewrite-echo

echo: rewrite for readability and tests
This commit is contained in:
Alex Lyon 2019-04-05 18:39:18 -07:00 committed by GitHub
commit 100f6df2bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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,
None => break,
}
input.next();
}
std::char::from_u32(ret)
} }
struct Opts { fn print_escaped(input: &str, should_stop: &mut bool) {
newline: bool, let mut iter = input.chars().peekable();
escape: bool, while let Some(mut c) = iter.next() {
} if c == '\\' {
if let Some(next) = iter.next() {
fn convert_str(string: &[u8], index: usize, base: Base) -> (char, usize) { c = match next {
let (max_digits, is_legal_digit): (usize, fn(u8) -> bool) = match base { '\\' => '\\',
Base::B8 => (3, |c| (c as char).is_digit(8)), 'a' => '\x07',
Base::B16 => (2, |c| (c as char).is_digit(16)), '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
},
}; };
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;
} }
} }
print!("{}", c);
if bytes.is_empty() {
(' ', 0)
} else {
(
usize::from_str_radix(from_utf8(bytes.as_ref()).unwrap(), base as u32).unwrap() as u8
as char,
bytes.len(),
)
} }
} }
@ -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 { } else {
match c { print!("{}", input);
'\\' => 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 {
print!("{}", string);
} }
} }
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");