initial commit

This commit is contained in:
Lu 2023-07-02 05:00:36 +02:00
commit dcec7ce541
5 changed files with 325 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target
.vscode
.idea
Cargo.lock

11
Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "echo-rs"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ascii_converter = "0.3.0"
lazy_static = "1.4.0"
regex = "1.8.4"

68
gnu_echo_tests/test.py Executable file
View File

@ -0,0 +1,68 @@
#!/usr/bin/env python3
import argparse
import subprocess
from pathlib import Path
def test_params(echo: Path, binary: Path, params: list[str]) -> None:
assert ( # noqa: S101
subprocess.check_output(
[echo, *params]) == subprocess.check_output([binary, *params]) # noqa: S603
)
def test_newline(echo: Path, binary: Path) -> None:
test_params(echo, binary, ["-n", r"test"])
test_params(echo, binary, [r"test"])
def test_no_escapes(echo: Path, binary: Path) -> None:
test_params(echo, binary, [r"\\\a\b\c\e\f\n\r\t\v\00\x0"])
def test_escapes(echo: Path, binary: Path) -> None:
test_params(echo, binary, ["-e", r"test\\test"])
test_params(echo, binary, ["-e", r"test\atest"])
test_params(echo, binary, ["-e", r"test\btest"])
test_params(echo, binary, ["-e", r"test\ctest"])
test_params(echo, binary, ["-e", r"test\etest"])
test_params(echo, binary, ["-e", r"test\ftest"])
test_params(echo, binary, ["-e", r"test\ntest"])
test_params(echo, binary, ["-e", r"test\rtest"])
test_params(echo, binary, ["-e", r"test\ttest"])
test_params(echo, binary, ["-e", r"test\vtest"])
def test_octal(echo: Path, binary: Path) -> None:
test_params(echo, binary, [
"-e", "-n", r"\0\00\0000\777\0777", r"\377\0377\376\0376", r"\00000\1000\01000"])
def test_hex(echo: Path, binary: Path) -> None:
test_params(echo, binary, ["-e", "-n",
r"\x\x0\x00\xFF", r"\xFE\x000\x100"])
def test_octal_hex(echo: Path, binary: Path) -> None:
test_params(echo, binary, ["-e", "-n", r"\0\x\00\x0\0000\x00",
r"\777\0777\377\0377\xFF", r"\376\0376\xFE\00000\x000", r"\1000\01000\x100"])
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("-e", "--echo", help="gnu echo binary", type=Path)
parser.add_argument("-b", "--binary", help="custom echo binary", type=Path)
args = parser.parse_args()
echo = args.echo
binary = args.binary
test_newline(echo, binary)
test_no_escapes(echo, binary)
test_escapes(echo, binary)
test_octal(echo, binary)
test_hex(echo, binary)
test_octal_hex(echo, binary)
if __name__ == "__main__":
main()

65
src/consts.rs Normal file
View File

@ -0,0 +1,65 @@
use std::process::exit;
use lazy_static::lazy_static;
use regex::Regex;
pub const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
pub const HELP_DIALOG: &str = r#"
Usage: echo-rs [SHORT-OPTION]... [STRING]...
or: echo-rs LONG-OPTION
Echo the STRING(s) to standard output. Rust rewrite of GNU echo util.
-n do not output the trailing newline
-e enable interpretation of backslash escapes
-E disable interpretation of backslash escapes (default)
--help display this help and exit
--version output version information and exit
If -e is in effect, the following sequences are recognized:
\\ backslash
\a alert (BEL)
\b backspace
\c produce no further output
\e escape
\f form feed
\n new line
\r carriage return
\t horizontal tab
\v vertical tab
\0NNN byte with octal value NNN (1 to 3 digits)
\xHH byte with hexadecimal value HH (1 to 2 digits)
"#;
pub const SIMPLE_SPECIAL_SEQUENCES: [(&str, &str); 8] = [
(r#"\a"#, "\x07"),
(r#"\b"#, "\x08"),
(r#"\e"#, "\x1b"),
(r#"\f"#, "\x0c"),
(r#"\n"#, "\n"),
(r#"\r"#, "\r"),
(r#"\t"#, "\t"),
(r#"\v"#, "\x0b"),
];
lazy_static! {
pub static ref OCTAL_REGEX: Regex = Regex::new(r#"\\(?:([1-7][0-7]{0,2}|0[0-7]{0,3}))"#)
.map_or_else(
|e| {
eprintln!(
"programming error: cannot compile regex pattern for octal regex match: {}",
e
);
exit(1);
},
|v| v
);
pub static ref HEX_REGEX: Regex = Regex::new(r#"\\x([0-9A-F]{0,2})"#).map_or_else(
|e| {
eprintln!(
"programming error: cannot compile regex pattern for hex regex match: {}",
e
);
exit(1);
},
|v| v
);
}

177
src/main.rs Normal file
View File

@ -0,0 +1,177 @@
use std::{
env::args,
io::{self, Write},
process::exit,
};
mod consts;
use crate::consts::{HELP_DIALOG, HEX_REGEX, OCTAL_REGEX, SIMPLE_SPECIAL_SEQUENCES, VERSION};
struct Settings {
trailing_newline: bool,
interpret_backslash_escapes: bool,
}
impl Default for Settings {
fn default() -> Settings {
Settings {
trailing_newline: true,
interpret_backslash_escapes: false,
}
}
}
fn replace_octal(string: String) -> String {
let mut res = string;
while let Some(captures) = OCTAL_REGEX.captures(
#[allow(clippy::redundant_clone)]
&res.clone(),
) {
if let (Some(entire_match), Some(capture)) = (captures.get(0), captures.get(1)) {
let mut contents = capture.as_str();
if contents.is_empty() {
contents = "0" // GNU echo interprets \0 as \00
}
res = format!(
"{}{}",
&res[..entire_match.start()],
OCTAL_REGEX.replace(
&res[entire_match.start()..],
&u8::from_str_radix(contents, 8)
.map_or_else(|_| 255 as char, |v| v as char)
.to_string(),
)
);
} else {
println!("error matching octal regex. aborting");
exit(1);
}
}
res
}
fn replace_hex(string: String) -> String {
let mut res = string;
let mut search_at = 0;
while let Some(captures) = HEX_REGEX.captures_at(
#[allow(clippy::redundant_clone)]
&res.clone(),
search_at,
) {
if let (Some(entire_match), Some(capture)) = (captures.get(0), captures.get(1)) {
let contents = capture.as_str();
if contents.is_empty() {
search_at = entire_match.end();
continue; // GNU echo does not interpret \x
}
res = format!(
"{}{}",
&res[..entire_match.start()],
HEX_REGEX.replace(
&res[entire_match.start()..],
&u8::from_str_radix(contents, 16)
.map_or_else(|_| 255 as char, |v| v as char)
.to_string(),
)
);
} else {
println!("error matching hex regex. aborting");
exit(1);
}
}
res
}
fn format_arg(arg: String, settings: &mut Settings) -> String {
if settings.interpret_backslash_escapes {
let mut res = arg;
let mut backslash_escaped: Vec<String> =
res.split(r#"\\"#).map(|s| s.to_string()).collect();
let mut found_c = false;
for element in &mut backslash_escaped {
if found_c {
*element = String::new();
continue;
}
for sequence in SIMPLE_SPECIAL_SEQUENCES {
*element = element.replace(sequence.0, sequence.1)
}
if let Some(pos) = element.find(r#"\c"#) {
found_c = true;
settings.trailing_newline = false;
*element = element[..pos].to_string();
continue;
}
*element = replace_octal(element.clone());
*element = replace_hex(element.clone());
}
res = backslash_escaped.join(r#"\"#);
res
} else {
arg
}
}
fn write_as_unicode(string: String) {
let stdout = io::stdout();
stdout
.lock()
.write_all(
string
.chars()
.map(|c| c as u8)
.collect::<Vec<u8>>()
.as_slice(),
)
.map_or_else(|e| eprintln!("error writing to stdout: {e}"), |_| {});
}
fn main() {
let mut settings = Settings::default();
let mut read_flags = true;
for i in 1..args().len() {
if let Some(arg) = args().nth(i) {
if read_flags {
match arg.as_str() {
"-n" => {
settings.trailing_newline = false;
}
"-e" => {
settings.interpret_backslash_escapes = true;
}
"-E" => {
settings.interpret_backslash_escapes = false;
}
"--help" => {
println!("{}", HELP_DIALOG);
exit(0)
}
"--version" => {
match VERSION {
Some(v) => {
println!("echo-rs {}", v)
}
None => {
println!("echo-rs was not compiled with a version")
}
}
exit(0)
}
_ => {
write_as_unicode(format_arg(arg, &mut settings));
read_flags = false;
}
}
} else {
write_as_unicode(format!(" {}", format_arg(arg, &mut settings)));
}
}
}
if settings.trailing_newline {
println!()
}
}