Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add expand util. Improve echo. #6

Merged
merged 2 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ https://github.com/jgarzik/posixutils
- [ ] ed
- [x] env
- [ ] ex
- [ ] expand
- [x] expand
- [ ] expr
- [x] false
- [ ] file
Expand Down
15 changes: 12 additions & 3 deletions display/src/echo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,7 +66,7 @@ fn translate_str(s: &str) -> String {
}
}

if nl {
if nl && !skip_nl {
output.push_str("\n");
}

Expand All @@ -80,7 +80,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args: Vec<String> = 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())?;

Expand Down
3 changes: 3 additions & 0 deletions display/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
4 changes: 4 additions & 0 deletions text/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
179 changes: 179 additions & 0 deletions text/src/expand.rs
Original file line number Diff line number Diff line change
@@ -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<String>,

/// Files to read as input.
files: Vec<String>,
}

enum TabList {
UniStop(usize),
Stops(Vec<usize>),
}

fn parse_tablist(tablist: &str) -> Result<TabList, &'static str> {
let res = tablist.parse::<usize>();
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::<usize>() {
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<dyn Write>) -> 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<dyn Read>;
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<dyn std::error::Error>> {
// 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)
}
15 changes: 15 additions & 0 deletions text/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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");
Expand Down