diff --git a/src/bin/rbcat.rs b/src/bin/rbcat.rs index 078e9ce..53fc18f 100644 --- a/src/bin/rbcat.rs +++ b/src/bin/rbcat.rs @@ -1,5 +1,5 @@ use std::{env, fs}; -use std::io::{self, BufRead}; +use std::io::{self, BufRead, ErrorKind::*}; fn main() -> Result<(), io::Error> { if env::args().len() < 2 { @@ -15,8 +15,8 @@ fn main() -> Result<(), io::Error> { Ok(contents) => print!("{}", contents), Err(error) => { let msg = match error.kind() { - io::ErrorKind::PermissionDenied => "Permission denied", - io::ErrorKind::NotFound => "No such file or directory", + PermissionDenied => "Permission denied", + NotFound => "No such file or directory", _ => "Unexpected I/O error" }; eprintln!("rbcat: {}: {}", file_name, msg); diff --git a/src/bin/rbls.rs b/src/bin/rbls.rs index 0bc017a..24c1be9 100644 --- a/src/bin/rbls.rs +++ b/src/bin/rbls.rs @@ -1,27 +1,57 @@ use clap::Parser; -use console::Term; -use std::io::{self, Write}; +use std::io::{self, ErrorKind::*}; use std::fs::{self, DirEntry, Metadata}; -fn main() -> Result<(), io::Error> { +// TODO: implement more options (-l, -i, etc.) +fn main() { let cli = Cli::parse(); - let file_infos = get_files_metadata(&cli.files[0])?; - let file_names = format_file_names(file_infos, cli.all, cli.almost_all); - let term = Term::stdout(); - let output = if term.is_term() { format_term(file_names, term.size().1) } - else { file_names.join(" ") }; - let mut stdout = io::stdout().lock(); - write!(stdout, "{}", output)?; - writeln!(stdout)?; - return Ok(()); + let mut non_dirs = Vec::::new(); + let mut dirs = Vec::::new(); + for file_name in &cli.files { + let file_infos = match get_file_infos(&file_name) { + Ok(file_infos) => file_infos, + Err(err) => { + // NotADirectory is unstable feature, look at error code instead + if err.raw_os_error().unwrap() == 20 { + let metadata = fs::metadata(&file_name).unwrap(); + non_dirs.push(FileInfo::new(file_name, metadata)); + continue; + } + let msg = match err.kind() { + PermissionDenied => "Permission denied", + NotFound => "No such file or directory", + _ => "Unexpected I/O error" + }; + eprintln!("rbls: {}: {}", file_name, msg); + continue; + } + }; + dirs.push(DirInfo::new(file_name, file_infos)); + } + if !non_dirs.is_empty() { + let non_dirs_filtered = transform_file_infos(non_dirs, &cli); + let non_dirs_output = format_output(non_dirs_filtered, &cli); + println!("{}", non_dirs_output); + } + let dirs_output = if dirs.len() > 1 { + dirs.into_iter() + .map(|dir| { + let files_filtered = transform_file_infos(dir.file_infos, &cli); + return dir.name + ":\n" + &format_output(files_filtered, &cli); + }) + .collect::>() + .join("\n\n") + } else { + let dir = dirs.remove(0); + let files_filtered = transform_file_infos(dir.file_infos, &cli); + format_output(files_filtered, &cli) + }; + println!("{}", dirs_output); } -fn get_files_metadata(file_name: &str) -> Result, io::Error> { - let Ok(contents) = fs::read_dir(file_name) - else { - let file_info = FileInfo::new(file_name.to_string(), fs::metadata(file_name)?); - return Ok(vec![file_info]); - }; +fn get_file_infos(file_name: impl AsRef) -> io::Result> { + let file_name = file_name.as_ref(); + let contents = fs::read_dir(file_name)?; let mut file_infos = vec![ FileInfo::new(".".to_string(), fs::metadata(".")?), FileInfo::new("..".to_string(), fs::metadata("..")?), @@ -32,15 +62,19 @@ fn get_files_metadata(file_name: &str) -> Result, io::Error> { return Ok(file_infos); } -fn format_file_names(file_infos: Vec, show_all: bool, show_almost_all: bool) -> Vec { - return file_infos.into_iter() +fn transform_file_infos(file_infos: Vec, options: &Cli) -> Vec { + let mut filtered_file_infos = file_infos.into_iter() .filter(|file_info| { - if show_all { return true; } - if show_almost_all { + if options.all { return true; } + if options.almost_all { return file_info.name != "." && file_info.name != ".."; } return !file_info.name.starts_with("."); }) + .collect::>(); + // sort alphabetically + filtered_file_infos.sort_by(|fi1, fi2| fi1.name.to_lowercase().cmp(&fi2.name.to_lowercase())); + return filtered_file_infos.into_iter() .map(|file_info| { let mut file_name = file_info.name; if file_info.metadata.is_dir() { file_name.push('/') } @@ -49,7 +83,13 @@ fn format_file_names(file_infos: Vec, show_all: bool, show_almost_all: .collect(); } -fn format_term(mut file_names: Vec, term_width: u16) -> String { +fn format_output(file_names: Vec, options: &Cli) -> String { + let term = console::Term::stdout(); + if term.is_term() { return format_tabular(file_names, options, term.size().1); } + else { return file_names.join(" "); } +} + +fn format_tabular(mut file_names: Vec, options: &Cli, term_width: u16) -> String { let max_file_name_len = file_names.iter().map(|n| n.len()).max().unwrap(); let elem_width = max_file_name_len + 2; let row_elems_amount = (term_width as usize / elem_width).max(1); @@ -60,12 +100,20 @@ fn format_term(mut file_names: Vec, term_width: u16) -> String { return file_names.join("").trim_end().to_string(); } +struct DirInfo { + name: String, file_infos: Vec +} + +impl DirInfo { + fn new(name: impl Into, file_infos: Vec) -> Self { Self { name: name.into(), file_infos } } +} + struct FileInfo { name: String, metadata: Metadata } impl FileInfo { - fn new(name: String, metadata: Metadata) -> Self { Self { name, metadata } } + fn new(name: impl Into, metadata: Metadata) -> Self { Self { name: name.into(), metadata } } } impl TryFrom for FileInfo { @@ -77,7 +125,7 @@ impl TryFrom for FileInfo { } } -impl TryFrom> for FileInfo { +impl TryFrom> for FileInfo { type Error = io::Error; fn try_from(de: Result) -> Result { Self::try_from(de?) } @@ -93,5 +141,11 @@ struct Cli { all: bool, #[arg(short = 'A', long, help = "Same as --all, but doesn't list implied . and ..")] - almost_all: bool + almost_all: bool, + + #[arg(short, long, help = "Use a long listing format")] + long: bool, + + #[arg(short, long, help = "Print the index number of each file")] + inode: bool }