Add cut applet

This commit is contained in:
Nathan Fisher 2023-01-14 02:08:14 -05:00
parent 5560cf55a9
commit 3ffcdb54ed
3 changed files with 300 additions and 10 deletions

285
src/cmd/cut/mod.rs Normal file
View File

@ -0,0 +1,285 @@
use super::Cmd;
use clap::{parser::ValuesRef, Arg, ArgAction, ArgGroup, Command};
use std::{
error::Error,
fs::File,
io::{self, BufRead, BufReader, BufWriter, Write},
ops::{Range, RangeFrom, RangeTo},
usize, vec,
};
#[derive(Debug, Default)]
pub struct Cut;
impl Cmd for Cut {
fn cli(&self) -> clap::Command {
Command::new("cut")
.about("cut out selected fields of each line of a file")
.author("Nathan Fisher")
.version(env!("CARGO_PKG_VERSION"))
.args([
Arg::new("bytes")
.short('b')
.long("bytes")
.help("select only these bytes")
.num_args(1)
.allow_hyphen_values(true)
.value_name("LIST")
.value_delimiter(','),
Arg::new("characters")
.short('c')
.long("characters")
.help("select only these characters")
.num_args(1)
.allow_hyphen_values(true)
.value_name("LIST")
.value_delimiter(','),
Arg::new("fields")
.short('f')
.long("fields")
.help("select only these fields")
.num_args(1)
.allow_hyphen_values(true)
.value_name("LIST")
.value_delimiter(','),
Arg::new("delimiter")
.short('d')
.long("delimiter")
.help("use DELIM instead of TAB for field delimiter")
.num_args(1)
.value_name("DELIM"),
Arg::new("nosplit").help("ignored").short('n'),
Arg::new("suppress")
.short('s')
.long("only-delimited")
.requires("fields")
.action(ArgAction::SetTrue),
Arg::new("file").value_name("FILE").num_args(0..),
])
.group(
ArgGroup::new("flags")
.args(["bytes", "characters", "fields"])
.required(true),
)
}
fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box<dyn Error>> {
let Some(matches) = matches else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "no input")));
};
let files: Vec<String> = match matches.get_many::<String>("file") {
Some(f) => f.cloned().collect(),
None => vec!["-".to_string()],
};
let (ranges, operator) = if let Some(raw_ranges) = matches.get_many::<String>("bytes") {
(get_ranges(raw_ranges)?, Operator::Bytes)
} else if let Some(raw_ranges) = matches.get_many::<String>("characters") {
(get_ranges(raw_ranges)?, Operator::Characters)
} else if let Some(raw_ranges) = matches.get_many::<String>("fields") {
(get_ranges(raw_ranges)?, Operator::Fields)
} else {
unreachable!()
};
for file in files {
let reader: Box<dyn BufRead> = if file.as_str() == "-" {
Box::new(BufReader::new(io::stdin()))
} else {
let fd = File::open(&file)?;
Box::new(BufReader::new(fd))
};
for line in reader.lines() {
match operator {
Operator::Bytes => print_bytes(&line?, &ranges)?,
Operator::Characters => print_chars(&line?, &ranges)?,
Operator::Fields => print_fields(
&line?,
&ranges,
matches.get_flag("suppress"),
matches.get_one::<String>("delimiter"),
)?,
}
}
}
Ok(())
}
fn path(&self) -> Option<crate::Path> {
Some(crate::Path::UsrBin)
}
}
enum Operator {
Bytes,
Characters,
Fields,
}
fn get_ranges(raw_ranges: ValuesRef<String>) -> Result<Vec<ParsedRange>, Box<dyn Error>> {
let mut ranges = vec![];
for range in raw_ranges {
let parsed_range = parse_range(range)?;
ranges.push(parsed_range);
}
Ok(ranges)
}
fn print_bytes(line: &str, ranges: &[ParsedRange]) -> Result<(), Box<dyn Error>> {
let combined_ranges = combine_ranges(ranges, line.len());
let mut writer = BufWriter::new(std::io::stdout());
for (idx, byte) in line.as_bytes().iter().enumerate() {
if combined_ranges.contains(&idx) {
write!(writer, "{}", *byte as char)?;
}
}
write!(writer, "\n")?;
Ok(())
}
fn print_chars(line: &str, ranges: &[ParsedRange]) -> Result<(), Box<dyn Error>> {
let combined_ranges = combine_ranges(ranges, line.len());
let mut writer = BufWriter::new(std::io::stdout());
for (idx, ch) in line.chars().enumerate() {
if combined_ranges.contains(&idx) {
write!(writer, "{}", &ch)?;
}
}
write!(writer, "\n")?;
Ok(())
}
fn print_fields(
line: &str,
ranges: &[ParsedRange],
suppress: bool,
delimiter: Option<&String>,
) -> Result<(), Box<dyn Error>> {
let combined_ranges = combine_ranges(ranges, line.len());
let mut writer = BufWriter::new(std::io::stdout());
let delimiter = match delimiter {
Some(d) => d,
None => "\t",
};
if suppress {
if !line.contains(delimiter) {
return Ok(());
}
}
let fields = line.split(delimiter);
for (idx, field) in fields.enumerate() {
if combined_ranges.contains(&idx) {
write!(writer, "{field}")?;
}
}
write!(writer, "\n")?;
Ok(())
}
enum ParsedRange {
Bounded(Range<usize>),
LowerBounded(RangeFrom<usize>),
UpperBounded(RangeTo<usize>),
Single(usize),
}
impl From<Range<usize>> for ParsedRange {
fn from(value: Range<usize>) -> Self {
Self::Bounded(value)
}
}
impl From<RangeFrom<usize>> for ParsedRange {
fn from(value: RangeFrom<usize>) -> Self {
Self::LowerBounded(value)
}
}
impl From<RangeTo<usize>> for ParsedRange {
fn from(value: RangeTo<usize>) -> Self {
Self::UpperBounded(value)
}
}
impl From<usize> for ParsedRange {
fn from(value: usize) -> Self {
Self::Single(value)
}
}
fn combine_ranges(ranges: &[ParsedRange], max: usize) -> Vec<usize> {
let mut outrange: Vec<usize> = vec![];
for range in ranges {
match range {
ParsedRange::Bounded(r) => {
for n in r.start..r.end {
outrange.push(n);
}
}
ParsedRange::LowerBounded(r) => {
for n in r.start..=max {
outrange.push(n);
}
}
ParsedRange::UpperBounded(r) => {
for n in 0..=r.end {
outrange.push(n);
}
}
ParsedRange::Single(u) => outrange.push(*u),
}
}
outrange.dedup();
outrange.sort_unstable();
outrange
}
fn parse_range(range: &str) -> Result<ParsedRange, Box<dyn Error>> {
if range.starts_with('-') {
if range.len() > 1 {
let end: usize = range[1..].parse()?;
if end >= 1 {
Ok((..end - 1).into())
} else {
Err(Box::new(io::Error::new(
io::ErrorKind::Other,
"invalid range",
)))
}
} else {
Err(Box::new(io::Error::new(
io::ErrorKind::Other,
"invalid range",
)))
}
} else if range.ends_with('-') {
if range.len() > 1 {
let start: usize = range[0..range.len()].parse()?;
if start >= 1 {
Ok((start - 1..).into())
} else {
Err(Box::new(io::Error::new(
io::ErrorKind::Other,
"invalid range",
)))
}
} else {
Err(Box::new(io::Error::new(
io::ErrorKind::Other,
"invalid range",
)))
}
} else if let Some((start, end)) = range.split_once('-') {
let start: usize = start.parse()?;
let end: usize = end.parse()?;
if start >= 1 && end > start {
Ok((start - 1..end).into())
} else {
Err(Box::new(io::Error::new(
io::ErrorKind::Other,
"invalid range",
)))
}
} else {
let range: usize = range.parse::<usize>()? - 1;
Ok(range.into())
}
}

View File

@ -9,6 +9,7 @@ mod cat;
mod chmod;
pub mod clear;
mod cp;
pub mod cut;
mod date;
mod dd;
pub mod dirname;
@ -41,10 +42,10 @@ pub mod yes;
pub use {
self::hostname::Hostname, base32::Base32, base64::Base64, basename::Basename,
bootstrap::Bootstrap, clear::Clear, dirname::Dirname, echo::Echo, factor::Factor, fold::Fold,
groups::Groups, head::Head, link::Link, mountpoint::Mountpoint, nologin::Nologin, nproc::Nproc,
r#false::False, r#true::True, rev::Rev, shitbox::Shitbox, sleep::Sleep, sync::SYnc,
which::Which, whoami::Whoami, yes::Yes,
bootstrap::Bootstrap, clear::Clear, cut::Cut, dirname::Dirname, echo::Echo, factor::Factor,
fold::Fold, groups::Groups, head::Head, link::Link, mountpoint::Mountpoint, nologin::Nologin,
nproc::Nproc, r#false::False, r#true::True, rev::Rev, shitbox::Shitbox, sleep::Sleep,
sync::Sync as SyncCmd, which::Which, whoami::Whoami, yes::Yes,
};
pub trait Cmd: fmt::Debug + Sync {
@ -61,6 +62,7 @@ pub fn get(name: &str) -> Option<Box<dyn Cmd>> {
"basename" => Some(Box::new(Basename::default())),
"bootstrap" => Some(Box::new(Bootstrap::default())),
"clear" => Some(Box::new(Clear::default())),
"cut" => Some(Box::new(Cut::default())),
"dirname" => Some(Box::new(Dirname::default())),
"echo" => Some(Box::new(Echo::default())),
"factor" => Some(Box::new(Factor::default())),
@ -75,7 +77,7 @@ pub fn get(name: &str) -> Option<Box<dyn Cmd>> {
"rev" => Some(Box::new(Rev::default())),
"shitbox" => Some(Box::new(Shitbox::default())),
"sleep" => Some(Box::new(Sleep::default())),
"sync" => Some(Box::new(SYnc::default())),
"sync" => Some(Box::new(SyncCmd::default())),
"true" => Some(Box::new(True::default())),
"which" => Some(Box::new(Which::default())),
"whoami" => Some(Box::new(Whoami::default())),
@ -84,12 +86,13 @@ pub fn get(name: &str) -> Option<Box<dyn Cmd>> {
}
}
pub static COMMANDS: [&'static str; 25] = [
pub static COMMANDS: [&'static str; 26] = [
"base32",
"base64",
"basename",
"bootstrap",
"clear",
"cut",
"dirname",
"echo",
"false",

View File

@ -1,11 +1,11 @@
use super::Cmd;
use clap::{Arg, ArgAction, Command};
use std::{error::Error, io, ffi::CString};
use std::{error::Error, ffi::CString, io};
#[derive(Debug, Default)]
pub struct SYnc;
pub struct Sync;
impl Cmd for SYnc {
impl Cmd for Sync {
fn cli(&self) -> clap::Command {
Command::new("sync")
.about("force completion of pending disk writes (flush cache)")
@ -66,7 +66,9 @@ impl Cmd for SYnc {
}
}
} else {
unsafe { libc::sync(); }
unsafe {
libc::sync();
}
}
Ok(())
}