commit 8aa32c25109badfa7aefb6dfb283548cbb8ebec3 Author: Lu Date: Sun Jul 2 15:24:42 2023 +0200 first working basic prototype with structure diff --git a/.github/workflows/echo-rs-ci.yml b/.github/workflows/echo-rs-ci.yml new file mode 100644 index 0000000..f26088f --- /dev/null +++ b/.github/workflows/echo-rs-ci.yml @@ -0,0 +1,27 @@ +name: echo-rs CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Set up echo-rs + uses: actions/checkout@v2 + - name: Install cargo-audit + run: cargo install cargo-audit + - name: Build + run: cargo build --verbose + - name: test + run: python3 gnu_cat_tests/test.py -e /bin/cat -b target/debug/cat-rs + - name: Clippy + run: cargo clippy --verbose -- -D warnings + - name: Audit + run: cargo audit \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc2a5e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +.vscode +.idea +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..36070e4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "cat-rs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.3.10", features = ["derive"] } diff --git a/gnu_cat_tests/test.py b/gnu_cat_tests/test.py new file mode 100755 index 0000000..bb6ec0a --- /dev/null +++ b/gnu_cat_tests/test.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import argparse +import subprocess +from pathlib import Path + + +def test_params(cat: Path, binary: Path, params: list[str], stdin: bytes = b"") -> None: + outputs = [] + for executable in (cat, binary): + outputs.append(b"") + p = subprocess.Popen([executable, *params], stdin=subprocess.PIPE, stdout=subprocess.PIPE) # noqa: S603 + outputs[-1] += p.communicate(input=stdin)[0] + + assert(outputs[0] == outputs[1]) # noqa: S101 + +def test_basic_usage(cat: Path, binary: Path) -> None: + test_params(cat, binary, ["-", __file__, __file__], b"test") + test_params(cat, binary, [__file__, "-", __file__], b"test") + test_params(cat, binary, ["-", __file__, "-", __file__], b"test") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("-c", "--cat", help="gnu cat binary", type=Path) + parser.add_argument("-b", "--binary", help="custom echo binary", type=Path) + args = parser.parse_args() + cat = args.cat + binary = args.binary + + test_basic_usage(cat, binary) + + print("tests completed successfully") + + +if __name__ == "__main__": + main() diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9ad221a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,163 @@ +use std::{ + fs::File, + io::{self, Read, Write}, + path::{Path, PathBuf}, + process::exit, +}; + +use clap::Parser; + +#[derive(Parser)] +#[command( + author, + version, + about = "Concatenate file(s) to standard output", + long_about = "Concatenate file(s) to standard output + +Examples: + cat-rs f - g Output f's contents, then standard input, then g's contents. + cat-rs Copy standard input to standard output." +)] +struct Cli { + #[arg(help = "equivalent to -vET")] + #[arg(short = 'A', long = "show-all")] + show_all: bool, + #[arg(help = "number nonempty output lines, overrides -n")] + #[arg(short = 'b', long = "number-nonblank")] + number_nonblank: bool, + #[arg(help = "equivalent to -vE")] + #[arg(short = 'e')] + nonprinting_ends: bool, + #[arg(help = "display $ at end of each line")] + #[arg(short = 'E', long = "show-ends")] + show_ends: bool, + #[arg(help = "number all output lines")] + #[arg(short = 'n', long = "number")] + number: bool, + #[arg(help = "suppress repeated empty output lines")] + #[arg(short = 's', long = "squeeze-blanks")] + squeeze_blanks: bool, + #[arg(help = "equivalent to -vT")] + #[arg(short = 't')] + nonprinting_tabs: bool, + #[arg(help = "display TAB character as ^I")] + #[arg(short = 'T', long = "show-tabs")] + show_tabs: bool, + #[arg(help = "(ignored)")] + #[arg(short = 'u')] + ignored_value: bool, + #[arg(help = "use ^ and M- notation, except for LFD and TAB")] + #[arg(short = 'v', long = "show-nonprinting")] + show_nonprinting: bool, + #[arg(help = "With no file, or when file is -, read standard input")] + files: Vec, +} + +#[derive(Clone)] +enum Input { + File(PathBuf), + Stdio, +} + +impl Input { + fn new(input: String) -> Self { + if &input == "-" { + Self::Stdio + } else { + Self::File(Path::new(&input).to_path_buf()) + } + } +} + +struct Settings { + number_nonblank: bool, + number: bool, + ends: bool, + tabs: bool, + nonprinting: bool, + inputs: Vec, +} + +impl Settings { + fn new(cli: Cli) -> Self { + Self { + number_nonblank: cli.number_nonblank, + number: cli.number || cli.number_nonblank, + ends: cli.show_ends || cli.nonprinting_ends || cli.show_all, + tabs: cli.show_tabs || cli.nonprinting_tabs || cli.show_all, + nonprinting: cli.show_nonprinting + || cli.nonprinting_ends + || cli.nonprinting_tabs + || cli.show_all, + inputs: cli.files.into_iter().map(Input::new).collect(), + } + } +} + +fn read_stdin() -> Vec { + let mut input = Vec::new(); + io::stdin().read_to_end(&mut input).unwrap(); + input +} + +fn read_file(path: PathBuf) -> Vec { + let mut content = Vec::new(); + let mut file = File::open(path).unwrap(); + file.read_to_end(&mut content).unwrap(); + content +} + +fn parse_number_nonblank(contents: Vec) -> Vec { + contents +} + +fn parse_number(contents: Vec) -> Vec { + contents +} + +fn parse_ends(contents: Vec) -> Vec { + contents +} + +fn parse_tabs(contents: Vec) -> Vec { + contents +} + +fn parse_nonprinting(contents: Vec) -> Vec { + contents +} + +fn parse_to_stdout(mut contents: Vec, settings: &Settings) { + let stdout = io::stdout(); + if settings.number_nonblank { + contents = parse_number_nonblank(contents) + } else if settings.number { + contents = parse_number(contents) + } + if settings.ends { + contents = parse_ends(contents) + } + if settings.tabs { + contents = parse_tabs(contents) + } + if settings.nonprinting { + contents = parse_nonprinting(contents) + } + if let Err(e) = stdout.lock().write_all(&contents) { + eprintln!("Error writing to stdout: {e}"); + exit(1) + }; +} + +fn main() { + let settings = Settings::new(Cli::parse()); + for input in settings.inputs.clone() { + parse_to_stdout( + match input { + Input::File(path) => read_file(path), + Input::Stdio => read_stdin(), + }, + &settings, + ) + } +}