From 6b42d59d805604279571d3f98455d012a4dcc093 Mon Sep 17 00:00:00 2001 From: Wojciech Graj Date: Wed, 12 Jul 2023 02:23:44 +0200 Subject: [PATCH 1/3] paste: create paste --- DragonflyBSD.toml | 1 + FreeBSD.toml | 1 + Fuchsia.toml | 1 + Haiku.toml | 1 + Illumos.toml | 1 + Linux.toml | 1 + MacOS.toml | 1 + NetBSD.toml | 1 + OpenBSD.toml | 1 + README.md | 3 +- Unix.toml | 1 + paste/Cargo.toml | 22 ++++++++ paste/build.rs | 36 +++++++++++++ paste/src/cli.rs | 30 +++++++++++ paste/src/main.rs | 130 ++++++++++++++++++++++++++++++++++++++++++++++ 15 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 paste/Cargo.toml create mode 100644 paste/build.rs create mode 100644 paste/src/cli.rs create mode 100644 paste/src/main.rs diff --git a/DragonflyBSD.toml b/DragonflyBSD.toml index c2fe0ea8..81f52a90 100644 --- a/DragonflyBSD.toml +++ b/DragonflyBSD.toml @@ -33,6 +33,7 @@ members = [ "nice", "nl", "nohup", + "paste", "printf", "pwd", "rm", diff --git a/FreeBSD.toml b/FreeBSD.toml index 33595733..b1af314e 100644 --- a/FreeBSD.toml +++ b/FreeBSD.toml @@ -33,6 +33,7 @@ members = [ "nice", "nl", "nohup", + "paste", "printf", "pwd", "rm", diff --git a/Fuchsia.toml b/Fuchsia.toml index 725afb58..afb4671f 100644 --- a/Fuchsia.toml +++ b/Fuchsia.toml @@ -33,6 +33,7 @@ members = [ # "nice", "nl", "nohup", + "paste", "printf", "pwd", "rm", diff --git a/Haiku.toml b/Haiku.toml index cd23fa78..7af1772e 100644 --- a/Haiku.toml +++ b/Haiku.toml @@ -31,6 +31,7 @@ members = [ "nice", "nl", "nohup", + "paste", "printf", "pwd", "rm", diff --git a/Illumos.toml b/Illumos.toml index 33595733..b1af314e 100644 --- a/Illumos.toml +++ b/Illumos.toml @@ -33,6 +33,7 @@ members = [ "nice", "nl", "nohup", + "paste", "printf", "pwd", "rm", diff --git a/Linux.toml b/Linux.toml index 2ad34367..e779e984 100644 --- a/Linux.toml +++ b/Linux.toml @@ -33,6 +33,7 @@ members = [ "nice", "nl", "nohup", + "paste", "printf", "pwd", "rm", diff --git a/MacOS.toml b/MacOS.toml index 33595733..b1af314e 100644 --- a/MacOS.toml +++ b/MacOS.toml @@ -33,6 +33,7 @@ members = [ "nice", "nl", "nohup", + "paste", "printf", "pwd", "rm", diff --git a/NetBSD.toml b/NetBSD.toml index 33595733..b1af314e 100644 --- a/NetBSD.toml +++ b/NetBSD.toml @@ -33,6 +33,7 @@ members = [ "nice", "nl", "nohup", + "paste", "printf", "pwd", "rm", diff --git a/OpenBSD.toml b/OpenBSD.toml index 33595733..b1af314e 100644 --- a/OpenBSD.toml +++ b/OpenBSD.toml @@ -33,6 +33,7 @@ members = [ "nice", "nl", "nohup", + "paste", "printf", "pwd", "rm", diff --git a/README.md b/README.md index c55f35e1..2960938a 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ cargo install --path . | nl | | | X | | nohup | | | X | | od | X | | | -| paste | X | | | +| paste | | | X | | patch | X | | | | printf | | | X | | pwd | | | X | @@ -172,6 +172,7 @@ Without them, this project would not be what it is today. - [@bojan88](https://github.com/bojan88) - _Bojan Đurđević_ - [@Celeo](https://github.com/Celeo) - _Celeo_ - [@FedericoPonzi](https://github.com/FedericoPonzi) - _Federico Ponzi_ +- [@wojciech-graj](https://github.com/wojciech-graj) - _Wojciech Graj_ - [@Larisho](https://github.com/Larisho) - _Gab David_ - [@silverweed](https://github.com/silverweed) - _Giacomo Parolini_ - [@marcospb19](https://github.com/marcospb19) - _João M. Bezerra_ diff --git a/Unix.toml b/Unix.toml index 33595733..b1af314e 100644 --- a/Unix.toml +++ b/Unix.toml @@ -33,6 +33,7 @@ members = [ "nice", "nl", "nohup", + "paste", "printf", "pwd", "rm", diff --git a/paste/Cargo.toml b/paste/Cargo.toml new file mode 100644 index 00000000..b08632d5 --- /dev/null +++ b/paste/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "paste" +version = "0.1.0" +authors = ["Wojciech Graj "] +license = "MPL-2.0-no-copyleft-exception" +build = "build.rs" +edition = "2021" +rust-version = "1.61.0" +description = """ +Concatenate lines from files, delimited by TABs. + +Use - as a FILE to read from standard input. An absence of FILEs will be treated as -. +""" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "3.0.0", features = ["cargo", "wrap_help"] } + +[build-dependencies] +clap = { version = "3.0.0", features = ["cargo"] } +clap_generate = "3.0.0" diff --git a/paste/build.rs b/paste/build.rs new file mode 100644 index 00000000..1be11eda --- /dev/null +++ b/paste/build.rs @@ -0,0 +1,36 @@ +use std::{env, fs::File, io::BufWriter}; + +use clap::crate_name; +use clap_generate::{Generator, Shell}; + +#[path = "src/cli.rs"] +mod cli; + +fn main() { + let mut app = cli::create_app(); + app.set_bin_name(crate_name!()); + + let out_dir = match env::var("OUT_DIR") { + Ok(dir) => dir, + Err(err) => { + eprintln!("No OUT_DIR: {}", err); + return; + }, + }; + + let shells = [Shell::Bash, Shell::Elvish, Shell::Fish, Shell::PowerShell, Shell::Zsh]; + + for shell in shells { + let file_name = format!("{}/{}", out_dir, shell.file_name(app.get_name())); + let mut file = BufWriter::new( + File::options() + .read(true) + .write(true) + .create(true) + .open(file_name) + .expect("Unable to open file"), + ); + + shell.generate(&app, &mut file) + } +} diff --git a/paste/src/cli.rs b/paste/src/cli.rs new file mode 100644 index 00000000..7f6ebfa2 --- /dev/null +++ b/paste/src/cli.rs @@ -0,0 +1,30 @@ +use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; + +pub(crate) fn create_app<'help>() -> App<'help> { + App::new(crate_name!()) + .version(crate_version!()) + .author(crate_authors!()) + .about(crate_description!()) + .mut_arg("help", |help| help.help("Display help information.").short('?')) + .mut_arg("version", |v| v.help("Display version information.")) + .arg(Arg::new("FILE").help("").multiple_occurrences(true)) + .arg( + Arg::new("delimiters") + .help("Loop through characters from LIST instead of using TABs.") + .long("delimiters") + .short('d') + .value_name("LIST"), + ) + .arg( + Arg::new("serial") + .help("Apply paste to lines from each file separately.") + .long("serial") + .short('s'), + ) + .arg( + Arg::new("zero-terminated") + .help("Replace newline with NUL as line delimiter.") + .long("zero-terminated") + .short('z'), + ) +} diff --git a/paste/src/main.rs b/paste/src/main.rs new file mode 100644 index 00000000..82afd1c7 --- /dev/null +++ b/paste/src/main.rs @@ -0,0 +1,130 @@ +use std::{error::Error, fs::File, io}; + +use clap::ArgMatches; + +mod cli; + +fn main() { + let matches = cli::create_app().get_matches(); + + let flags = PasteFlags::from_matches(&matches); + + let mut readers: Vec> = match matches.values_of("FILE") { + None => vec![Box::new(io::BufReader::new(io::stdin()))], + Some(filenames) => filenames + .map(|filename| -> Box { + match filename { + "-" => Box::new(io::BufReader::new(io::stdin())), + filename => match File::open(filename) { + Ok(file) => Box::new(io::BufReader::new(file)), + Err(why) => { + eprintln!("paste: {}: {}", filename, why); + std::process::exit(1); + }, + }, + } + }) + .collect(), + }; + + std::process::exit(match paste(&mut readers, &flags) { + Ok(_) => 0, + Err(why) => { + eprintln!("paste: {}", why); + 1 + }, + }); +} + +fn paste( + readers: &mut Vec>, flags: &PasteFlags, +) -> Result<(), Box> { + let line_terminator = if flags.zero_terminated { b'\0' } else { b'\n' }; + + let mut out_line_no = 0; + loop { + let mut flag = false; + let mut out_buf: Vec = Vec::new(); + let mut inp_line_no = 0; + loop { + let reader_idx = if flags.serial { out_line_no } else { inp_line_no }; + if reader_idx >= readers.len() { + break; + } + + let mut inp_buf: Vec = Vec::new(); + match (*readers[reader_idx]).read_until(line_terminator, &mut inp_buf) { + Ok(0) => { + // EOF + if flags.serial { + break; + } + }, + Ok(_) => { + if pop_if_last(&mut inp_buf, b'\n') { + pop_if_last(&mut inp_buf, b'\t'); + } + flag = true; + }, + Err(why) => { + return Err(Box::new(why)); + }, + } + + if inp_line_no != 0 { + match &flags.delimiters { + None => out_buf.push(b'\t'), + Some(list) => out_buf.extend([list[(inp_line_no - 1) % list.len()] as u8]), + }; + } + + out_buf.extend(inp_buf); + + inp_line_no += 1; + } + if !flag { + break; + } + let s = match String::from_utf8(out_buf) { + Ok(string) => string, + Err(why) => { + return Err(Box::new(why)); + }, + }; + println!("{}", s); + out_line_no += 1; + } + Ok(()) +} + +fn pop_if_last(buf: &mut Vec, tgt: T) -> bool +where + T: Copy + std::cmp::PartialEq, +{ + if let Some(last) = buf.last().copied() { + if last == tgt { + buf.pop(); + return true; + } + } + false +} + +struct PasteFlags { + pub delimiters: Option>, + pub serial: bool, + pub zero_terminated: bool, +} + +impl PasteFlags { + pub fn from_matches(matches: &ArgMatches) -> Self { + PasteFlags { + delimiters: match matches.get_one::("delimiters") { + None => Option::None, + Some(list) => Option::Some(list.chars().collect()), + }, + serial: matches.is_present("serial"), + zero_terminated: matches.is_present("zero-terminated"), + } + } +} From 09fc37c04e2c65a2f0e5a3906f1bf280469c0d80 Mon Sep 17 00:00:00 2001 From: Wojciech Graj Date: Wed, 12 Jul 2023 03:02:09 +0200 Subject: [PATCH 2/3] paste: add comment. --- paste/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/paste/src/main.rs b/paste/src/main.rs index 82afd1c7..be71181d 100644 --- a/paste/src/main.rs +++ b/paste/src/main.rs @@ -97,6 +97,7 @@ fn paste( Ok(()) } +// Pop the last element from `buf` if it equals `tgt` fn pop_if_last(buf: &mut Vec, tgt: T) -> bool where T: Copy + std::cmp::PartialEq, From 3f8c1c719a7fbe52383507d4784272fb1dea6a66 Mon Sep 17 00:00:00 2001 From: Wojciech Graj Date: Wed, 12 Jul 2023 03:06:52 +0200 Subject: [PATCH 3/3] paste: apply clippy suggestion. --- paste/src/main.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/paste/src/main.rs b/paste/src/main.rs index be71181d..646b1891 100644 --- a/paste/src/main.rs +++ b/paste/src/main.rs @@ -120,10 +120,7 @@ struct PasteFlags { impl PasteFlags { pub fn from_matches(matches: &ArgMatches) -> Self { PasteFlags { - delimiters: match matches.get_one::("delimiters") { - None => Option::None, - Some(list) => Option::Some(list.chars().collect()), - }, + delimiters: matches.get_one::("delimiters").map(|list| list.chars().collect()), serial: matches.is_present("serial"), zero_terminated: matches.is_present("zero-terminated"), }