first working basic prototype with structure

This commit is contained in:
Lu 2023-07-02 15:24:42 +02:00
commit 8aa32c2510
5 changed files with 239 additions and 0 deletions

27
.github/workflows/echo-rs-ci.yml vendored Normal file
View File

@ -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

4
.gitignore vendored Normal file
View File

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

9
Cargo.toml Normal file
View File

@ -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"] }

36
gnu_cat_tests/test.py Executable file
View File

@ -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()

163
src/main.rs Normal file
View File

@ -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<String>,
}
#[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<Input>,
}
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<u8> {
let mut input = Vec::new();
io::stdin().read_to_end(&mut input).unwrap();
input
}
fn read_file(path: PathBuf) -> Vec<u8> {
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<u8>) -> Vec<u8> {
contents
}
fn parse_number(contents: Vec<u8>) -> Vec<u8> {
contents
}
fn parse_ends(contents: Vec<u8>) -> Vec<u8> {
contents
}
fn parse_tabs(contents: Vec<u8>) -> Vec<u8> {
contents
}
fn parse_nonprinting(contents: Vec<u8>) -> Vec<u8> {
contents
}
fn parse_to_stdout(mut contents: Vec<u8>, 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,
)
}
}