From cbccc9a456222e8fb6396199ee51180f7ea7a52d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 28 Sep 2025 10:04:38 +0200 Subject: [PATCH] realpath: implement -E --- src/uu/realpath/locales/en-US.ftl | 1 + src/uu/realpath/locales/fr-FR.ftl | 1 + src/uu/realpath/src/realpath.rs | 21 +++++++++-- tests/by-util/test_realpath.rs | 62 +++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/uu/realpath/locales/en-US.ftl b/src/uu/realpath/locales/en-US.ftl index 9da567cdc..256a6c751 100644 --- a/src/uu/realpath/locales/en-US.ftl +++ b/src/uu/realpath/locales/en-US.ftl @@ -7,6 +7,7 @@ realpath-help-strip = Only strip '.' and '..' components, but don't resolve symb realpath-help-zero = Separate output filenames with \0 rather than newline realpath-help-logical = resolve '..' components before symlinks realpath-help-physical = resolve symlinks as encountered (default) +realpath-help-canonicalize = all but the last component must exist (default) realpath-help-canonicalize-existing = canonicalize by following every symlink in every component of the given name recursively, all components must exist realpath-help-canonicalize-missing = canonicalize by following every symlink in every component of the given name recursively, without requirements on components existence realpath-help-relative-to = print the resolved path relative to DIR diff --git a/src/uu/realpath/locales/fr-FR.ftl b/src/uu/realpath/locales/fr-FR.ftl index 16b522ee9..6908b83f2 100644 --- a/src/uu/realpath/locales/fr-FR.ftl +++ b/src/uu/realpath/locales/fr-FR.ftl @@ -7,6 +7,7 @@ realpath-help-strip = Supprimer uniquement les composants '.' et '..', mais ne p realpath-help-zero = Séparer les noms de fichiers de sortie avec \0 plutôt qu'avec un saut de ligne realpath-help-logical = résoudre les composants '..' avant les liens symboliques realpath-help-physical = résoudre les liens symboliques rencontrés (par défaut) +realpath-help-canonicalize = tous les composants sauf le dernier doivent exister (par défaut) realpath-help-canonicalize-existing = canonicaliser en suivant récursivement chaque lien symbolique dans chaque composant du nom donné, tous les composants doivent exister realpath-help-canonicalize-missing = canonicaliser en suivant récursivement chaque lien symbolique dans chaque composant du nom donné, sans exigences sur l'existence des composants realpath-help-relative-to = afficher le chemin résolu relativement à RÉP diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 6b57acefb..43bfa78ad 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -31,6 +31,7 @@ const OPT_ZERO: &str = "zero"; const OPT_PHYSICAL: &str = "physical"; const OPT_LOGICAL: &str = "logical"; const OPT_CANONICALIZE_MISSING: &str = "canonicalize-missing"; +const OPT_CANONICALIZE: &str = "canonicalize"; const OPT_CANONICALIZE_EXISTING: &str = "canonicalize-existing"; const OPT_RELATIVE_TO: &str = "relative-to"; const OPT_RELATIVE_BASE: &str = "relative-base"; @@ -86,11 +87,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let line_ending = LineEnding::from_zero_flag(matches.get_flag(OPT_ZERO)); let quiet = matches.get_flag(OPT_QUIET); let logical = matches.get_flag(OPT_LOGICAL); - let can_mode = if matches.get_flag(OPT_CANONICALIZE_EXISTING) { - MissingHandling::Existing - } else if matches.get_flag(OPT_CANONICALIZE_MISSING) { + let can_mode = if matches.get_flag(OPT_CANONICALIZE_MISSING) { MissingHandling::Missing + } else if matches.get_flag(OPT_CANONICALIZE_EXISTING) { + // -e: all components must exist + // Despite the name, MissingHandling::Existing requires all components to exist + MissingHandling::Existing } else { + // Default behavior (same as -E): all but last component must exist + // MissingHandling::Normal allows the final component to not exist MissingHandling::Normal }; let resolve_mode = if strip { @@ -164,10 +169,19 @@ pub fn uu_app() -> Command { .help(translate!("realpath-help-physical")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(OPT_CANONICALIZE) + .short('E') + .long(OPT_CANONICALIZE) + .overrides_with_all([OPT_CANONICALIZE_EXISTING, OPT_CANONICALIZE_MISSING]) + .help(translate!("realpath-help-canonicalize")) + .action(ArgAction::SetTrue), + ) .arg( Arg::new(OPT_CANONICALIZE_EXISTING) .short('e') .long(OPT_CANONICALIZE_EXISTING) + .overrides_with_all([OPT_CANONICALIZE, OPT_CANONICALIZE_MISSING]) .help(translate!("realpath-help-canonicalize-existing")) .action(ArgAction::SetTrue), ) @@ -175,6 +189,7 @@ pub fn uu_app() -> Command { Arg::new(OPT_CANONICALIZE_MISSING) .short('m') .long(OPT_CANONICALIZE_MISSING) + .overrides_with_all([OPT_CANONICALIZE, OPT_CANONICALIZE_EXISTING]) .help(translate!("realpath-help-canonicalize-missing")) .action(ArgAction::SetTrue), ) diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index 249614bf4..a2e3b4311 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -505,3 +505,65 @@ fn test_realpath_empty_string() { .fails() .code_is(1); } + +#[test] +fn test_realpath_canonicalize_options() { + // Test that default, -E, and --canonicalize all allow nonexistent final component + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir("existing_dir"); + + let test_cases = [ + vec![], // default behavior + vec!["-E"], // explicit -E flag + vec!["--canonicalize"], // --canonicalize long form + ]; + + #[cfg(windows)] + let expected_path = "existing_dir\\nonexistent"; + #[cfg(not(windows))] + let expected_path = "existing_dir/nonexistent"; + + for args in test_cases { + let mut ucmd = scene.ucmd(); + for arg in args { + ucmd.arg(arg); + } + ucmd.arg("existing_dir/nonexistent") + .succeeds() + .stdout_contains(expected_path); + } +} + +#[test] +fn test_realpath_canonicalize_vs_existing() { + // Test difference between -E and -e, and option overrides + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir("existing_dir"); + + let test_cases = [ + (vec!["-E"], true), // -E should succeed with nonexistent final component + (vec!["-e"], false), // -e should fail with nonexistent final component + (vec!["-e", "-E"], true), // -E should override -e + ]; + + #[cfg(windows)] + let expected_path = "existing_dir\\nonexistent"; + #[cfg(not(windows))] + let expected_path = "existing_dir/nonexistent"; + + for (args, should_succeed) in test_cases { + let mut ucmd = scene.ucmd(); + for arg in args { + ucmd.arg(arg); + } + ucmd.arg("existing_dir/nonexistent"); + + if should_succeed { + ucmd.succeeds().stdout_contains(expected_path); + } else { + ucmd.fails(); + } + } +}