first working basic prototype with structure
This commit is contained in:
commit
8aa32c2510
27
.github/workflows/echo-rs-ci.yml
vendored
Normal file
27
.github/workflows/echo-rs-ci.yml
vendored
Normal 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
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/target
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
Cargo.lock
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal 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
36
gnu_cat_tests/test.py
Executable 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
163
src/main.rs
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user