diff --git a/Cargo.lock b/Cargo.lock index b72831b..5dc6203 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,7 +97,7 @@ dependencies = [ [[package]] name = "enumerate-ordered" -version = "1.0.0" +version = "1.1.0" dependencies = [ "color-eyre", "futures", diff --git a/Cargo.toml b/Cargo.toml index 6e43b9b..2264f89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "enumerate-ordered" description = "Order files by their timestamps" authors=["Avril (Flanchan) "] -version = "1.0.0" +version = "1.1.0" edition = "2021" license = "gpl3.0-or-later" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/TODO b/TODO deleted file mode 100644 index ad7017f..0000000 --- a/TODO +++ /dev/null @@ -1 +0,0 @@ -Add `-0`, deliminate standard input reads and standard output writes by the ASCII nul (b'\0', 0u8) instead of a newline (b'\n') diff --git a/src/args.rs b/src/args.rs index 471d0db..356951d 100644 --- a/src/args.rs +++ b/src/args.rs @@ -18,15 +18,32 @@ use futures::{ }; /// Parsed command-line args -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct Args { + pub delim: u8, pub reverse: bool, pub walker: walk::Config, pub worker: work::Config, paths: Option>, } +impl Default for Args +{ + #[inline] + fn default() -> Self + { + Self { + delim: b'\n', + reverse: false, + walker: Default::default(), + worker: Default::default(), + paths: None, + } + } +} + + impl Args { /// Paths as an async stream @@ -39,6 +56,7 @@ impl Args stream::iter(paths.iter().map(|x| Cow::Borrowed(Path::new(x)))).boxed() } else { let (tx, rx) = mpsc::channel(128); + let read_chr = self.delim; tokio::spawn(async move { use tokio::io::{ self, @@ -52,7 +70,7 @@ impl Args { buf.clear(); use std::os::unix::prelude::*; - let n = match stdin.read_until(b'\n', &mut buf).await { + let n = match stdin.read_until(read_chr, &mut buf).await { Ok(n) => n, Err(e) => { error!("paths: Failed to read input line: {}", e); @@ -69,7 +87,7 @@ impl Args trace!("paths: Ignoring empty line. Yielding then continuing."); tokio::task::yield_now().await; continue; - } else if path_bytes[n-1] == b'\n' { + } else if path_bytes[n-1] == read_chr { &path_bytes[.. (path_bytes.len()-1)] } else { path_bytes @@ -298,6 +316,44 @@ where I: Iterator output.worker.by = work::OrderBy::ModifiedTime; } + // -z, -0, --nul, + // -I, --delim |ifs + 'delim: { + output.delim = if input.is_any(args![b"z0", "nul"]) { + 0u8 + } else if input.is_any(args![b'I', "delim"]) { + fn read_ifs_as_byte() -> eyre::Result + { + let Some(ifs) = std::env::var_os("IFS") else { + return Err(eyre!("IFS env-var not set")); + }; + use std::os::unix::prelude::*; + ifs.as_bytes().first().copied().ok_or(eyre!("IFS env-var empty")) + } + + if input.is_long() { + let val = take!(); + match val.as_bytes() { + [] => return Err(eyre!("Line seperator cannot be empty").with_suggestion(|| "--delim ifs|".header("Usage is"))), + b"ifs" | b"IFS" => { + read_ifs_as_byte().wrap_err(eyre!("Failed to read line seperator from IFS env var"))? + } + [de] => *de, + [de, rest @ ..] => { + warn!("Specified more than one byte for line seperator. Ignoring other {} bytes", rest.len()); + *de + }, + } + } else { + // Read IFS + read_ifs_as_byte().wrap_err(eyre!("Failed to read line seperator from IFS env var"))? + } + } else { + // No change + break 'delim; + }; + }; + // -n, --reverse output.reverse = input.is_any(args![b'n', "reverse"]); diff --git a/src/main.rs b/src/main.rs index 6b0ec22..24c9ef0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,6 +81,9 @@ where W: std::io::Write, } write_opt!("-r", "--recursive " => "Recursively sort input files, up to `` (set to 0 for infniite); if limit is not specified, recursion is infinite")?; + write_opt!("-z", "-0", "--nul" => "Seperate lines when reading/writing by the ascii NUL character (0) instead of a newline. This applies to reading input from `stdin` as well as writing output")?; + write_opt!("-I", "--delim ifs" => "Read the first byte of the IFS environment variable as the I/O line seperator.")?; + write_opt!("--delim " => "Use this user-provided byte as the I/O line seperator")?; write_opt!("-a", "--atime" => "Sort by atime")?; write_opt!("-c", "--ctime" => "Sort by ctime (default)")?; write_opt!("-m", "--mtime" => "Sort by mtime")?; @@ -179,25 +182,27 @@ async fn main() -> eyre::Result<()> { trace!("Writing ordered results to stdout... (buffered, sync, rev: {})", args.reverse); #[inline] - fn operate_on(stdout: &mut W, set: I) -> eyre::Result<()> + fn operate_on(stdout: &mut W, set: I, delim: &[u8]) -> eyre::Result<()> where W: Write, I: IntoIterator + ExactSizeIterator + DoubleEndedIterator + std::iter::FusedIterator + 'static { for info in set { stdout.write_all(info.path().as_os_str().as_bytes()) - .and_then(|_| stdout.write_all(&[b'\n'])) + .and_then(|_| stdout.write_all(delim)) .wrap_err("Failed to write raw pathname for entry to stdout") .with_context(|| format!("{:?}", info.path()).header("Pathname was"))?; } Ok(()) } + + let delim = &[args.delim]; if args.reverse { - operate_on(&mut stdout, set.into_iter().rev()) + operate_on(&mut stdout, set.into_iter().rev(), delim) } else { - operate_on(&mut stdout, set.into_iter()) - }.wrap_err("Abandoning output write due to failure")?; + operate_on(&mut stdout, set.into_iter(), delim) + }.wrap_err("Abandoning output write due to intermittent failure")?; stdout.flush().wrap_err("Failed to flush buffered output to stdout")?; Ok(())