initial commit
This commit is contained in:
commit
dcec7ce541
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/target
|
||||
.vscode
|
||||
.idea
|
||||
Cargo.lock
|
11
Cargo.toml
Normal file
11
Cargo.toml
Normal 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
68
gnu_echo_tests/test.py
Executable 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
65
src/consts.rs
Normal 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
177
src/main.rs
Normal 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!()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user