From 34c92117d631e397be626ea1b0cce6bc78e1a390 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Sun, 10 Mar 2024 13:54:04 -0400 Subject: [PATCH 1/2] echo: support -n Both Linux and BSD support -n, and it does not conflict with POSIX --- display/src/echo.rs | 15 ++++++++++++--- display/tests/integration.rs | 3 +++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/display/src/echo.rs b/display/src/echo.rs index 657b707c..b88deebc 100644 --- a/display/src/echo.rs +++ b/display/src/echo.rs @@ -19,7 +19,7 @@ use gettextrs::{bind_textdomain_codeset, textdomain}; use plib::PROJECT_NAME; use std::io::{self, Write}; -fn translate_str(s: &str) -> String { +fn translate_str(skip_nl: bool, s: &str) -> String { let mut output = String::with_capacity(s.len()); let mut in_bs = false; @@ -66,7 +66,7 @@ fn translate_str(s: &str) -> String { } } - if nl { + if nl && !skip_nl { output.push_str("\n"); } @@ -80,7 +80,16 @@ fn main() -> Result<(), Box> { let mut args: Vec = std::env::args().collect(); args.remove(0); - let echo_str = translate_str(&args.join(" ")); + let skip_nl = { + if args.len() > 0 && (args[0] == "-n") { + args.remove(0); + true + } else { + false + } + }; + + let echo_str = translate_str(skip_nl, &args.join(" ")); io::stdout().write_all(echo_str.as_bytes())?; diff --git a/display/tests/integration.rs b/display/tests/integration.rs index 61fe8d84..94e75329 100644 --- a/display/tests/integration.rs +++ b/display/tests/integration.rs @@ -23,4 +23,7 @@ fn echo_test(args: &[&str], expected_output: &str) { #[test] fn test_echo_basic() { echo_test(&["big", "brown", "bear"], "big brown bear\n"); + + echo_test(&["-n", "foo", "bar"], "foo bar"); + echo_test(&["foo", "bar\\c"], "foo bar"); } From 630531b93327502fb610701220bcfdda0bc42fc0 Mon Sep 17 00:00:00 2001 From: Jeff Garzik Date: Sun, 10 Mar 2024 22:14:32 -0400 Subject: [PATCH 2/2] add util: expand --- README.md | 2 +- text/Cargo.toml | 4 + text/src/expand.rs | 179 ++++++++++++++++++++++++++++++++++++++ text/tests/integration.rs | 15 ++++ 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 text/src/expand.rs diff --git a/README.md b/README.md index 3b2e54d4..824f57b1 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ https://github.com/jgarzik/posixutils - [ ] ed - [x] env - [ ] ex - - [ ] expand + - [x] expand - [ ] expr - [x] false - [ ] file diff --git a/text/Cargo.toml b/text/Cargo.toml index 2ee695de..894edc4d 100644 --- a/text/Cargo.toml +++ b/text/Cargo.toml @@ -16,6 +16,10 @@ topological-sort = "0.2" name = "asa" path = "src/asa.rs" +[[bin]] +name = "expand" +path = "src/expand.rs" + [[bin]] name = "head" path = "src/head.rs" diff --git a/text/src/expand.rs b/text/src/expand.rs new file mode 100644 index 00000000..74d9f992 --- /dev/null +++ b/text/src/expand.rs @@ -0,0 +1,179 @@ +// +// Copyright (c) 2024 Jeff Garzik +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +extern crate clap; +extern crate plib; + +use clap::Parser; +use gettextrs::{bind_textdomain_codeset, textdomain}; +use plib::PROJECT_NAME; +use std::fs; +use std::io::{self, BufWriter, Read, Write}; + +/// expand - convert tabs to spaces +#[derive(Parser, Debug)] +#[command(author, version, about, long_about)] +struct Args { + /// Tab stops, either a single positive decimal integer or a list of tabstops separated by commas. + #[arg(short, long)] + tablist: Option, + + /// Files to read as input. + files: Vec, +} + +enum TabList { + UniStop(usize), + Stops(Vec), +} + +fn parse_tablist(tablist: &str) -> Result { + let res = tablist.parse::(); + if let Ok(tab) = res { + return Ok(TabList::UniStop(tab)); + } + + let mut v = Vec::new(); + for token in tablist.split(&[' ', ','][..]) { + let n = match token.parse::() { + Ok(val) => val, + Err(_e) => return Err("Invalid tab stop in list"), + }; + + if !v.is_empty() { + let last = *v.iter().last().unwrap(); + if n <= last { + return Err("Invalid tab stop order in list"); + } + } + + v.push(n); + } + + Ok(TabList::Stops(v)) +} + +fn space_out(column: &mut usize, writer: &mut BufWriter) -> io::Result<()> { + *column = *column + 1; + + writer.write_all(b" ")?; + + Ok(()) +} + +fn expand_file(tablist: &TabList, filename: &str) -> io::Result<()> { + // open file, or stdin + let mut file: Box; + if filename == "" { + file = Box::new(io::stdin().lock()); + } else { + file = Box::new(fs::File::open(filename)?); + } + + let mut raw_buffer = [0; plib::BUFSZ]; + let mut writer = BufWriter::new(io::stdout()); + let mut column: usize = 1; + let mut cur_stop = 0; + + loop { + // read a chunk of file data + let n_read = file.read(&mut raw_buffer[..])?; + if n_read == 0 { + break; + } + + // slice of buffer containing file data + let buf = &raw_buffer[0..n_read]; + + for byte_ref in buf { + let byte = *byte_ref; + if byte == 0x8 { + // backspace + writer.write_all(&[byte])?; + if column > 1 { + column = column - 1; + } + } else if byte == '\r' as u8 || byte == '\n' as u8 { + writer.write_all(&[byte])?; + column = 1; + } else if byte != '\t' as u8 { + writer.write_all(&[byte])?; + column = column + 1; + } else { + match tablist { + TabList::UniStop(n) => { + while (column % n) != 0 { + space_out(&mut column, &mut writer)?; + } + space_out(&mut column, &mut writer)?; + } + TabList::Stops(tabvec) => { + let last_tab: usize = tabvec[tabvec.len() - 1]; + let next_tab = tabvec[cur_stop]; + + if column >= last_tab { + space_out(&mut column, &mut writer)?; + } else { + while column < next_tab { + space_out(&mut column, &mut writer)?; + } + cur_stop = cur_stop + 1; + space_out(&mut column, &mut writer)?; + } + } + } + } + } + } + + writer.flush()?; + + Ok(()) +} + +fn main() -> Result<(), Box> { + // parse command line arguments + let mut args = Args::parse(); + + textdomain(PROJECT_NAME)?; + bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; + + let tablist = { + if let Some(ref tablist) = args.tablist { + match parse_tablist(&tablist) { + Ok(tl) => tl, + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + } + } else { + TabList::UniStop(8) + } + }; + + // if no files, read from stdin + if args.files.is_empty() { + args.files.push(String::new()); + } + + let mut exit_code = 0; + + for filename in &args.files { + match expand_file(&tablist, filename) { + Ok(()) => {} + Err(e) => { + exit_code = 1; + eprintln!("{}: {}", filename, e); + } + } + } + + std::process::exit(exit_code) +} diff --git a/text/tests/integration.rs b/text/tests/integration.rs index 2f3287e7..032a9ec6 100644 --- a/text/tests/integration.rs +++ b/text/tests/integration.rs @@ -9,6 +9,15 @@ use plib::{run_test, TestPlan}; +fn expand_test_noargs(test_data: &str, expected_output: &str) { + run_test(TestPlan { + cmd: String::from("expand"), + args: Vec::new(), + stdin_data: String::from(test_data), + expected_out: String::from(expected_output), + }); +} + fn head_test(test_data: &str, expected_output: &str) { run_test(TestPlan { cmd: String::from("head"), @@ -29,6 +38,12 @@ fn wc_test(args: &[&str], test_data: &str, expected_output: &str) { }); } +#[test] +fn test_expand_basic() { + expand_test_noargs("", ""); + expand_test_noargs("a\tb\tc\n", "a b c\n"); +} + #[test] fn test_head_basic() { head_test("a\nb\nc\nd\n", "a\nb\nc\nd\n");