use anyhow::{Context as _, Result, anyhow}; use camino::{Utf8Path, Utf8PathBuf}; use std::{ borrow::Cow, fs, io::{Read as _, Write as _}, rc::Rc, }; fn main() -> Result<()> { let mut app = App::try_new()?; app.run() } struct App { is_terminal: bool, lk: Option, paths: Vec>, path_index: usize, } impl App { fn try_new() -> Result { use std::io::IsTerminal as _; let is_terminal = std::io::stdout().is_terminal(); let mut args = std::env::args(); let _exe_name = args.next().context("How do we not have an exe name?")?; let mut paths = vec![]; for arg in args { paths.push(Cow::Owned(Utf8PathBuf::from(arg))); } let paths = if paths.is_empty() { vec![Cow::Borrowed(Utf8Path::new("."))] } else { paths }; Ok(App { is_terminal, lk: None, paths, path_index: 0, }) } fn poll(&mut self) -> Result>> { loop { if self.path_index >= self.paths.len() { return Ok(None); } let path = &self.paths[self.path_index]; match self.lk.as_mut() { Some(lk) => { if let Some(buf) = lk.poll()? { return Ok(Some(buf)); } else { self.lk = None; self.path_index += 1; } } None => { self.lk = Some(Lk::try_new(path)?); } }; } } fn run(&mut self) -> Result<()> { while let Some(buf) = self.poll()? { std::io::stdout().write_all(&buf)?; } Ok(()) } } enum Lk { Cat(Cat), Ls(Ls), } impl Lk { fn try_new(path: &Utf8Path) -> Result { let metadata = fs::metadata(path)?; if metadata.is_dir() { Self::try_new_ls(path) } else { Self::try_new_cat(path) } } fn try_new_cat(path: &Utf8Path) -> Result { Ok(Self::Cat(Cat::try_new(path)?)) } fn try_new_ls(path: &Utf8Path) -> Result { Ok(Self::Ls(Ls::try_new(path)?)) } // I don't understand why, but using `Rc` here instead of `Cow` satisfies the borrow checker. fn poll(&mut self) -> Result>> { match self { Lk::Cat(x) => x.poll(), Lk::Ls(x) => x.poll(), } } } struct Cat { f: fs::File, buf: Vec, } impl Cat { fn try_new(path: &Utf8Path) -> Result { let f = fs::File::open(path)?; let buf = vec![0u8; 4_096]; Ok(Self { f, buf }) } fn poll(&mut self) -> Result>> { let bytes_read = self.f.read(&mut self.buf)?; let buf = &self.buf[0..bytes_read]; if bytes_read == 0 { return Ok(None); } Ok(Some(buf.into())) } } struct Ls { names: Vec, name_index: usize, } impl Ls { fn try_new(path: &Utf8Path) -> Result { let d = fs::read_dir(path)?; let mut names = vec![]; for entry in d { let entry = entry?; names.push( entry .file_name() .into_string() .map_err(|_| anyhow!("name is not UTF-8"))?, ); } names.sort(); Ok(Self { names, name_index: 0, }) } fn poll(&mut self) -> Result>> { if self.name_index >= self.names.len() { return Ok(None); } let name = &self.names[self.name_index]; self.name_index += 1; Ok(Some(format!("{name}\n").into_bytes().into())) } } #[cfg(test)] mod tests { use super::*; use std::path::Path; #[test] fn test() -> Result<()> { let dir = tempfile::tempdir()?; let dir = dir.as_ref(); fs::write(dir.join("alfa.txt"), "alfa\n")?; fs::write(dir.join("bravo.txt"), "bravo\n")?; for (input, expected) in [ (vec![dir], b"alfa.txt\nbravo.txt\n".to_vec()), ( vec![&dir.join("alfa.txt"), &dir.join("bravo.txt")], b"alfa\nbravo\n".to_vec(), ), ] { assert_eq!(lk(input)?, expected); } Ok(()) } fn lk(paths: Vec<&Path>) -> Result> { let paths = paths .iter() .map(|p| Cow::Owned(Utf8PathBuf::from_path_buf(p.to_path_buf()).unwrap())) .collect(); let mut app = App { is_terminal: false, lk: None, paths, path_index: 0, }; let mut out = vec![]; while let Some(buf) = app.poll()? { out.write_all(&buf)?; } Ok(out) } }