use std::io::Write;
use std::process;
const TOOL_NAME: &str = "dirname";
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() {
coreutils_rs::common::reset_sigpipe();
let mut zero = false;
let mut names: Vec<String> = Vec::new();
let mut saw_dashdash = false;
for arg in std::env::args().skip(1) {
if saw_dashdash {
names.push(arg);
continue;
}
match arg.as_str() {
"--help" => {
println!("Usage: {} [OPTION] NAME...", TOOL_NAME);
println!("Output each NAME with its last non-slash component and trailing slashes");
println!("removed; if NAME contains no /'s, output '.' (meaning the current");
println!("directory).");
println!();
println!(" -z, --zero end each output line with NUL, not newline");
println!(" --help display this help and exit");
println!(" --version output version information and exit");
return;
}
"--version" => {
println!("{} (fcoreutils) {}", TOOL_NAME, VERSION);
return;
}
"--zero" | "-z" => zero = true,
"--" => saw_dashdash = true,
s if s.starts_with('-') && s.len() > 1 && !s.starts_with("--") => {
for ch in s[1..].chars() {
match ch {
'z' => zero = true,
c => {
eprintln!("{}: invalid option -- '{}'", TOOL_NAME, c);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
}
}
}
_ => names.push(arg),
}
}
if names.is_empty() {
eprintln!("{}: missing operand", TOOL_NAME);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
let terminator = if zero { '\0' } else { '\n' };
let stdout = std::io::stdout();
let mut out = stdout.lock();
for name in &names {
let result = dirname(name);
let _ = write!(out, "{}{}", result, terminator);
}
}
fn dirname(name: &str) -> &str {
if name.is_empty() {
return ".";
}
let bytes = name.as_bytes();
let len = bytes.len();
let mut end = len;
while end > 0 && bytes[end - 1] == b'/' {
end -= 1;
}
if end == 0 {
return "/";
}
while end > 0 && bytes[end - 1] != b'/' {
end -= 1;
}
if end == 0 {
return ".";
}
while end > 1 && bytes[end - 1] == b'/' {
end -= 1;
}
&name[..end]
}
#[cfg(test)]
mod tests {
use std::process::Command;
fn cmd() -> Command {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("fdirname");
Command::new(path)
}
#[test]
fn test_dirname_simple() {
let output = cmd().arg("/usr/bin").output().unwrap();
assert_eq!(output.status.code(), Some(0));
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim_end_matches('\n'),
"/usr"
);
}
#[test]
fn test_dirname_root() {
let output = cmd().arg("/").output().unwrap();
assert_eq!(output.status.code(), Some(0));
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim_end_matches('\n'),
"/"
);
}
#[test]
fn test_dirname_no_slash() {
let output = cmd().arg("hello").output().unwrap();
assert_eq!(output.status.code(), Some(0));
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim_end_matches('\n'),
"."
);
}
#[test]
fn test_dirname_trailing_slash() {
let output = cmd().arg("/usr/bin/").output().unwrap();
assert_eq!(output.status.code(), Some(0));
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim_end_matches('\n'),
"/usr"
);
}
#[test]
fn test_dirname_multiple_args() {
let output = cmd()
.args(["/usr/bin", "/foo/bar/baz", "hello", "/"])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["/usr", "/foo/bar", ".", "/"]);
}
#[test]
fn test_dirname_zero() {
let output = cmd().args(["-z", "/usr/bin"]).output().unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = &output.stdout;
assert!(stdout.ends_with(&[0u8]), "Should end with NUL byte");
assert!(!stdout.ends_with(b"\n"), "Should not end with newline");
let text = String::from_utf8_lossy(&stdout[..stdout.len() - 1]);
assert_eq!(text, "/usr");
}
#[test]
fn test_dirname_dot() {
let output = cmd().arg(".").output().unwrap();
assert_eq!(output.status.code(), Some(0));
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim_end_matches('\n'),
"."
);
}
#[test]
fn test_dirname_dotdot() {
let output = cmd().arg("..").output().unwrap();
assert_eq!(output.status.code(), Some(0));
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim_end_matches('\n'),
"."
);
}
#[test]
fn test_dirname_double_slash() {
let output = cmd().arg("//").output().unwrap();
assert_eq!(output.status.code(), Some(0));
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim_end_matches('\n'),
"/"
);
}
#[test]
fn test_dirname_no_args() {
let output = cmd().output().unwrap();
assert!(!output.status.success());
}
#[test]
fn test_dirname_empty_string() {
let output = cmd().arg("").output().unwrap();
assert!(output.status.success());
}
#[test]
fn test_dirname_deep_path() {
let output = cmd().arg("/a/b/c/d/e/f").output().unwrap();
assert_eq!(output.status.code(), Some(0));
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim_end_matches('\n'),
"/a/b/c/d/e"
);
}
#[test]
fn test_dirname_multiple_zero() {
let output = cmd().args(["-z", "/a/b", "/c/d"]).output().unwrap();
assert!(output.status.success());
let stdout = &output.stdout;
let parts: Vec<&[u8]> = stdout
.split(|&b| b == 0)
.filter(|s| !s.is_empty())
.collect();
assert_eq!(parts.len(), 2);
}
#[test]
#[cfg(unix)]
fn test_dirname_matches_gnu() {
let test_cases = vec![
vec!["/usr/bin"],
vec!["/"],
vec!["hello"],
vec!["/usr/bin/"],
vec!["."],
vec![".."],
vec!["//"],
vec!["/a/b/c/d"],
];
for args in &test_cases {
let gnu = Command::new("dirname").args(args).output();
if let Ok(gnu) = gnu {
let ours = cmd().args(args).output().unwrap();
assert_eq!(
String::from_utf8_lossy(&ours.stdout),
String::from_utf8_lossy(&gnu.stdout),
"Output mismatch for args {:?}",
args
);
assert_eq!(
ours.status.code(),
gnu.status.code(),
"Exit code mismatch for args {:?}",
args
);
}
}
}
}