diff --git a/README.md b/README.md index fe6525e..33da00c 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ This began as, and continues to be, a learning exercise to better understand the | df | :white_large_square: | | dir | :white_large_square: | | dircolors | :white_large_square: | -| dirname | :white_large_square: | +| dirname | :white_check_mark: | | du | :white_large_square: | | echo | :white_large_square: | | env | :white_large_square: | diff --git a/dirname/Cargo.toml b/dirname/Cargo.toml index 3703bdb..dd10aa2 100644 --- a/dirname/Cargo.toml +++ b/dirname/Cargo.toml @@ -1,10 +1,12 @@ [package] name = "dirname" version = "0.1.0" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] clap = { workspace = true } -shellexpand = "2.1.2" +coreutils = { workspace = true } +tabled = { workspace = true } +serde_json = { workspace = true } diff --git a/dirname/src/main.rs b/dirname/src/main.rs index b5aa6c6..79bbb23 100644 --- a/dirname/src/main.rs +++ b/dirname/src/main.rs @@ -1,43 +1,86 @@ use std::path::MAIN_SEPARATOR; -use clap::Parser; - -/// A rust implementation of dirname -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - /// The path to return the directory of - paths: Vec, -} +use clap::{Arg, ArgAction}; +use tabled::{builder::Builder, settings::Style}; + +use coreutils::{clap_args, clap_base_command}; + +clap_args!(Args { + flag zero: bool, + multi paths: Vec, +}); fn main() { - let args = Args::parse(); + let matches = clap_base_command() + .arg( + Arg::new("zero") + .short('z') + .long("zero") + .action(ArgAction::SetTrue) + .help("output a null-delimited list of dirnames (plain output only)"), + ) + .arg( + Arg::new("paths") + .action(ArgAction::Append) + .help("the path(s) to return the directory of") + .required(true), + ) + .get_matches(); - for arg in &args.paths { - let path = shellexpand::tilde(&arg); + let args = Args::from_matches(&matches); - let dirname = get_dirname(&path); - println!("{}", dirname); + let mut dirnames = Vec::new(); + for path in &args.paths { + let dirname = get_dirname(path); + dirnames.push(dirname); } -} -fn get_dirname(path: &str) -> String { - match path.rfind(MAIN_SEPARATOR) { - Some(idx) => { - if idx == 0 { - // The last separator is the first character, making it the dir - MAIN_SEPARATOR.to_string() - } else if !path.starts_with(MAIN_SEPARATOR) { - /* - if the string doesn't start with the separator, i.e., "foo/" - then the dirname is always '.' - */ - '.'.to_string() - } else { - let dirname = &path[..idx]; - dirname.to_string() + if let Some(output) = &args.output { + match output.as_str() { + "table" => { + let mut builder = Builder::new(); + builder.push_column(["Dirname(s)"]); + + for dirname in dirnames { + builder.push_record([dirname]); + } + let mut table = builder.build(); + println!("{}", table.with(Style::rounded())); + } + "json" => { + println!("{}", serde_json::to_string(&dirnames).unwrap()); + } + "yaml" => { + println!("dirnames:"); + for dirname in dirnames { + println!(" - dirname: \"{}\"", dirname); + } + } + _ => { + for dirname in &dirnames { + if args.zero { + print!("{}\0", dirname); + } else { + println!("{}", dirname); + } + } } } + } +} + +fn get_dirname(path: &str) -> String { + // Strip trailing separators (but preserve root "/") + let trimmed = path.trim_end_matches(MAIN_SEPARATOR); + let trimmed = if trimmed.is_empty() { + &path[..1] + } else { + trimmed + }; + + match trimmed.rfind(MAIN_SEPARATOR) { + Some(0) => MAIN_SEPARATOR.to_string(), + Some(idx) => trimmed[..idx].to_string(), None => '.'.to_string(), } } @@ -47,13 +90,60 @@ mod tests { use super::*; #[test] - fn test_dirname() { - // Assert that we got the stats we were expecting + fn test_root() { assert_eq!(get_dirname("/"), "/"); + } + + #[test] + fn test_absolute_single_component() { assert_eq!(get_dirname("/foo"), "/"); + } + + #[test] + fn test_bare_filename() { assert_eq!(get_dirname("foo"), "."); + } + + #[test] + fn test_relative_with_trailing_slash() { assert_eq!(get_dirname("foo/"), "."); + } + + #[test] + fn test_absolute_two_components() { assert_eq!(get_dirname("/home/stone"), "/home"); + } + + #[test] + fn test_absolute_three_components() { assert_eq!(get_dirname("/home/stone/bin"), "/home/stone"); } + + #[test] + fn test_relative_nested() { + assert_eq!(get_dirname("foo/bar"), "foo"); + assert_eq!(get_dirname("foo/bar/baz"), "foo/bar"); + } + + #[test] + fn test_trailing_slashes_absolute() { + assert_eq!(get_dirname("/home/stone/"), "/home"); + assert_eq!(get_dirname("/home/stone///"), "/home"); + } + + #[test] + fn test_trailing_slashes_root() { + assert_eq!(get_dirname("///"), "/"); + } + + #[test] + fn test_absolute_single_component_trailing_slash() { + assert_eq!(get_dirname("/foo/"), "/"); + } + + #[test] + fn test_dot_and_dotdot() { + assert_eq!(get_dirname("."), "."); + assert_eq!(get_dirname(".."), "."); + } }