Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
6 changes: 4 additions & 2 deletions dirname/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
154 changes: 122 additions & 32 deletions dirname/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}
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<String>,
});

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(),
}
}
Expand All @@ -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(".."), ".");
}
}
Loading