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