use std::path::{Component, Path, PathBuf};
use std::process;
const TOOL_NAME: &str = "realpath";
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
Canonicalize,
CanonicalizeExisting,
CanonicalizeMissing,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum SymlinkMode {
Physical,
Logical,
}
fn main() {
coreutils_rs::common::reset_sigpipe();
let mut mode = Mode::Canonicalize;
let mut no_symlinks = false;
let mut symlink_mode = SymlinkMode::Physical;
let mut zero = false;
let mut quiet = false;
let mut relative_to: Option<String> = None;
let mut relative_base: Option<String> = None;
let mut files: Vec<String> = Vec::new();
let mut saw_dashdash = false;
let args: Vec<String> = std::env::args().skip(1).collect();
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if saw_dashdash {
files.push(arg.clone());
i += 1;
continue;
}
match arg.as_str() {
"--help" => {
print_help();
return;
}
"--version" => {
println!("{} (fcoreutils) {}", TOOL_NAME, VERSION);
return;
}
"-e" | "--canonicalize-existing" => mode = Mode::CanonicalizeExisting,
"-m" | "--canonicalize-missing" => mode = Mode::CanonicalizeMissing,
"-s" | "--strip" | "--no-symlinks" => no_symlinks = true,
"-z" | "--zero" => zero = true,
"-q" | "--quiet" => quiet = true,
"-L" | "--logical" => symlink_mode = SymlinkMode::Logical,
"-P" | "--physical" => symlink_mode = SymlinkMode::Physical,
"--relative-to" => {
i += 1;
if i >= args.len() {
eprintln!("{}: option '--relative-to' requires an argument", TOOL_NAME);
process::exit(1);
}
relative_to = Some(args[i].clone());
}
"--relative-base" => {
i += 1;
if i >= args.len() {
eprintln!(
"{}: option '--relative-base' requires an argument",
TOOL_NAME
);
process::exit(1);
}
relative_base = Some(args[i].clone());
}
s if s.starts_with("--relative-to=") => {
relative_to = Some(s["--relative-to=".len()..].to_string());
}
s if s.starts_with("--relative-base=") => {
relative_base = Some(s["--relative-base=".len()..].to_string());
}
"--" => saw_dashdash = true,
s if s.starts_with('-') && !s.starts_with("--") && s.len() > 1 => {
for ch in s[1..].chars() {
match ch {
'e' => mode = Mode::CanonicalizeExisting,
'm' => mode = Mode::CanonicalizeMissing,
's' => no_symlinks = true,
'z' => zero = true,
'q' => quiet = true,
'L' => symlink_mode = SymlinkMode::Logical,
'P' => symlink_mode = SymlinkMode::Physical,
_ => {
eprintln!("{}: invalid option -- '{}'", TOOL_NAME, ch);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
}
}
}
_ => files.push(arg.clone()),
}
i += 1;
}
if files.is_empty() {
eprintln!("{}: missing operand", TOOL_NAME);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
if let Some(ref val) = relative_to
&& val.is_empty()
{
eprintln!("{}: '': No such file or directory", TOOL_NAME);
process::exit(1);
}
if let Some(ref val) = relative_base
&& val.is_empty()
{
eprintln!("{}: '': No such file or directory", TOOL_NAME);
process::exit(1);
}
let resolved_relative_to = relative_to.as_ref().map(|d| {
resolve_path(d, mode, no_symlinks, symlink_mode)
.unwrap_or_else(|_| make_absolute(Path::new(d)))
});
let resolved_relative_base = relative_base.as_ref().map(|d| {
resolve_path(d, mode, no_symlinks, symlink_mode)
.unwrap_or_else(|_| make_absolute(Path::new(d)))
});
if mode == Mode::CanonicalizeExisting {
if let Some(ref resolved) = resolved_relative_to
&& resolved.exists()
&& !resolved.is_dir()
{
if !quiet {
eprintln!(
"{}: {}: Not a directory",
TOOL_NAME,
relative_to.as_ref().unwrap()
);
}
process::exit(1);
}
if let Some(ref resolved) = resolved_relative_base
&& resolved.exists()
&& !resolved.is_dir()
{
if !quiet {
eprintln!(
"{}: {}: Not a directory",
TOOL_NAME,
relative_base.as_ref().unwrap()
);
}
process::exit(1);
}
}
let terminator = if zero { "\0" } else { "\n" };
let mut exit_code = 0;
for file in &files {
if file.is_empty() {
exit_code = 1;
if !quiet {
eprintln!("{}: '': No such file or directory", TOOL_NAME);
}
continue;
}
match resolve_path(file, mode, no_symlinks, symlink_mode) {
Ok(resolved) => {
let output =
apply_relative(&resolved, &resolved_relative_to, &resolved_relative_base);
print!("{}{}", output.to_string_lossy(), terminator);
}
Err(e) => {
exit_code = 1;
if !quiet {
eprintln!(
"{}: {}: {}",
TOOL_NAME,
file,
coreutils_rs::common::io_error_msg(&e)
);
}
}
}
}
process::exit(exit_code);
}
fn resolve_path(
path: &str,
mode: Mode,
no_symlinks: bool,
symlink_mode: SymlinkMode,
) -> Result<PathBuf, std::io::Error> {
if no_symlinks {
let abs = make_absolute(Path::new(path));
let normalized = normalize_path(&abs);
match mode {
Mode::CanonicalizeExisting | Mode::Canonicalize => {
if !normalized.exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"No such file or directory",
));
}
Ok(normalized)
}
Mode::CanonicalizeMissing => Ok(normalized),
}
} else if symlink_mode == SymlinkMode::Logical {
resolve_logical(path, mode)
} else {
match mode {
Mode::Canonicalize | Mode::CanonicalizeExisting => std::fs::canonicalize(path),
Mode::CanonicalizeMissing => canonicalize_missing(Path::new(path)),
}
}
}
fn resolve_logical(path: &str, mode: Mode) -> Result<PathBuf, std::io::Error> {
let abs = make_absolute(Path::new(path));
let normalized = normalize_path(&abs);
match mode {
Mode::Canonicalize | Mode::CanonicalizeExisting => std::fs::canonicalize(&normalized),
Mode::CanonicalizeMissing => canonicalize_missing(&normalized),
}
}
fn make_absolute(path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("/"))
.join(path)
}
}
fn normalize_path(path: &Path) -> PathBuf {
let mut result = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
result.pop();
}
c => {
result.push(c.as_os_str());
}
}
}
result
}
fn canonicalize_missing(path: &Path) -> Result<PathBuf, std::io::Error> {
let abs = make_absolute(path);
if let Ok(canon) = std::fs::canonicalize(&abs) {
return Ok(canon);
}
let components: Vec<Component<'_>> = abs.components().collect();
let mut resolved = PathBuf::new();
let mut remaining_start = 0;
for i in (0..components.len()).rev() {
let mut prefix = PathBuf::new();
for c in &components[..=i] {
prefix.push(c.as_os_str());
}
if let Ok(canon) = std::fs::canonicalize(&prefix) {
resolved = canon;
remaining_start = i + 1;
break;
}
}
if resolved.as_os_str().is_empty() {
if let Some(Component::RootDir) = components.first() {
resolved.push("/");
remaining_start = 1;
} else {
resolved = std::env::current_dir()?;
}
}
for c in &components[remaining_start..] {
match c {
Component::CurDir => {}
Component::ParentDir => {
resolved.pop();
}
Component::Normal(s) => {
resolved.push(s);
if resolved.symlink_metadata().is_ok()
&& let Ok(canon) = std::fs::canonicalize(&resolved)
{
resolved = canon;
}
}
Component::RootDir | Component::Prefix(_) => {
resolved.push(c.as_os_str());
}
}
}
Ok(resolved)
}
fn relative_path(from: &Path, to: &Path) -> PathBuf {
let from_components: Vec<Component<'_>> = from.components().collect();
let to_components: Vec<Component<'_>> = to.components().collect();
let common_len = from_components
.iter()
.zip(to_components.iter())
.take_while(|(a, b)| a == b)
.count();
let mut result = PathBuf::new();
for _ in common_len..from_components.len() {
result.push("..");
}
for c in &to_components[common_len..] {
result.push(c.as_os_str());
}
if result.as_os_str().is_empty() {
PathBuf::from(".")
} else {
result
}
}
fn apply_relative(
path: &Path,
relative_to: &Option<PathBuf>,
relative_base: &Option<PathBuf>,
) -> PathBuf {
if let Some(base) = relative_base
&& relative_to.is_none()
{
if path.starts_with(base) {
return relative_path(base, path);
}
return path.to_path_buf();
}
if let Some(rel_to) = relative_to {
if let Some(base) = relative_base {
if path.starts_with(base) && rel_to.starts_with(base) {
return relative_path(rel_to, path);
}
return path.to_path_buf();
}
return relative_path(rel_to, path);
}
path.to_path_buf()
}
fn print_help() {
println!("Usage: {} [OPTION]... FILE...", TOOL_NAME);
println!("Print the resolved absolute file name;");
println!("all but the last component must exist");
println!();
println!(" -e, --canonicalize-existing all components of the path must exist");
println!(" -m, --canonicalize-missing no path components need exist or be a directory");
println!(" -L, --logical resolve '..' components before symlinks");
println!(" -P, --physical resolve symlinks as encountered (default)");
println!(" -q, --quiet suppress most error messages");
println!(" -s, --strip, --no-symlinks don't expand symlinks");
println!(" -z, --zero end each output line with NUL, not newline");
println!(" --relative-to=DIR print the resolved path relative to DIR");
println!(" --relative-base=DIR print absolute paths unless paths below DIR");
println!(" --help display this help and exit");
println!(" --version output version information and exit");
}
#[cfg(all(test, unix))]
mod tests {
use std::fs;
use std::process::Command;
fn cmd() -> Command {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("frealpath");
Command::new(path)
}
#[test]
fn test_realpath_absolute() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("file.txt");
fs::write(&file, "hello").unwrap();
let output = cmd().arg(file.to_str().unwrap()).output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let canon = fs::canonicalize(&file).unwrap();
assert_eq!(stdout.trim(), canon.to_str().unwrap());
}
#[test]
fn test_realpath_symlinks() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("real.txt");
let link = dir.path().join("sym.txt");
fs::write(&target, "data").unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
let output = cmd().arg(link.to_str().unwrap()).output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let canon = fs::canonicalize(&target).unwrap();
assert_eq!(stdout.trim(), canon.to_str().unwrap());
}
#[test]
fn test_realpath_no_symlinks() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("real2.txt");
let link = dir.path().join("sym2.txt");
fs::write(&target, "data").unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
let output = cmd().args(["-s", link.to_str().unwrap()]).output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let canon_dir = fs::canonicalize(dir.path()).unwrap();
let abs_link = canon_dir.join("sym2.txt");
let stdout_trimmed = stdout.trim();
assert!(
stdout_trimmed == abs_link.to_str().unwrap() || stdout_trimmed.ends_with("/sym2.txt"),
"Expected path to sym2.txt, got: {}",
stdout_trimmed
);
}
#[test]
fn test_realpath_missing() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("nonexistent").join("deep").join("path.txt");
let output = cmd()
.args(["-m", missing.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("nonexistent"));
assert!(stdout.contains("path.txt"));
}
#[test]
fn test_realpath_existing() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("does_not_exist.txt");
let output = cmd()
.args(["-e", missing.to_str().unwrap()])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("No such file or directory"));
}
#[test]
fn test_realpath_relative_to() {
let dir = tempfile::tempdir().unwrap();
let subdir = dir.path().join("sub");
let file = dir.path().join("file.txt");
fs::create_dir(&subdir).unwrap();
fs::write(&file, "test").unwrap();
let canon_dir = fs::canonicalize(&subdir).unwrap();
let output = cmd()
.args([
&format!("--relative-to={}", canon_dir.to_str().unwrap()),
file.to_str().unwrap(),
])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "../file.txt");
}
#[test]
fn test_realpath_logical_mode() {
let dir = tempfile::tempdir().unwrap();
let canon_dir = fs::canonicalize(dir.path()).unwrap();
let real_dir = canon_dir.join("real_dir");
let link = canon_dir.join("link");
fs::create_dir(&real_dir).unwrap();
std::os::unix::fs::symlink(&real_dir, &link).unwrap();
let link_dotdot = format!("{}/link/..", canon_dir.display());
let output = cmd().args(["-L", &link_dotdot]).output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), canon_dir.to_str().unwrap());
}
#[test]
fn test_realpath_physical_mode() {
let output = cmd().args(["-P", "/tmp"]).output().unwrap();
assert!(output.status.success());
}
#[test]
fn test_realpath_combined_flags() {
let output = cmd().args(["-Pqz", "/tmp"]).output().unwrap();
assert!(output.status.success());
let stdout = output.stdout;
assert!(stdout.ends_with(b"\0"), "Expected NUL terminator");
assert!(!stdout.ends_with(b"\n"), "Should not end with newline");
}
#[test]
fn test_realpath_empty_string_all_modes() {
for flag in &["", "-e", "-m"] {
let mut c = cmd();
if !flag.is_empty() {
c.arg(*flag);
}
c.arg("");
let output = c.output().unwrap();
assert_eq!(
output.status.code(),
Some(1),
"Empty string should fail with flag '{}'",
flag
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("No such file or directory"),
"Expected error message with flag '{}', got: {}",
flag,
stderr
);
}
}
#[test]
fn test_realpath_empty_relative_base() {
let output = cmd().args(["--relative-base=", "/tmp"]).output().unwrap();
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("No such file or directory"));
}
#[test]
fn test_realpath_empty_relative_to() {
let output = cmd().args(["--relative-to=", "/tmp"]).output().unwrap();
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("No such file or directory"));
}
#[test]
fn test_realpath_e_relative_to_file() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("afile");
fs::write(&file, "x").unwrap();
let output = cmd()
.args([
"-e",
&format!("--relative-to={}", file.to_str().unwrap()),
"/tmp",
])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("Not a directory"));
}
#[test]
fn test_realpath_slashes() {
let output = cmd().args(["/", "//", "///"]).output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.trim().lines().collect();
assert_eq!(lines.len(), 3);
for line in &lines {
assert_eq!(*line, "/", "Expected '/' but got '{}'", line);
}
}
#[test]
#[cfg(target_os = "linux")]
fn test_realpath_matches_gnu() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("gnu_test.txt");
fs::write(&file, "hello").unwrap();
let gnu = Command::new("realpath")
.arg(file.to_str().unwrap())
.output();
if let Ok(gnu) = gnu {
let ours = cmd().arg(file.to_str().unwrap()).output().unwrap();
assert_eq!(ours.status.code(), gnu.status.code(), "Exit code mismatch");
let gnu_out = String::from_utf8_lossy(&gnu.stdout);
let our_out = String::from_utf8_lossy(&ours.stdout);
assert_eq!(our_out.trim(), gnu_out.trim(), "Output mismatch");
}
let missing = dir.path().join("missing_gnu");
let gnu_m = Command::new("realpath")
.args(["-m", missing.to_str().unwrap()])
.output();
if let Ok(gnu_m) = gnu_m {
let ours_m = cmd()
.args(["-m", missing.to_str().unwrap()])
.output()
.unwrap();
assert_eq!(
ours_m.status.code(),
gnu_m.status.code(),
"Exit code mismatch for -m"
);
let gnu_out = String::from_utf8_lossy(&gnu_m.stdout);
let our_out = String::from_utf8_lossy(&ours_m.stdout);
assert_eq!(our_out.trim(), gnu_out.trim(), "Output mismatch for -m");
}
}
}