1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

Realpath relative options (#3710)

* realpath: introduce relative options, make correct exit codes, make pass
GNU test mist/realpath.sh
This commit is contained in:
Niyaz Nigmatullin 2022-07-12 09:29:20 +03:00 committed by GitHub
parent 6b00aec48e
commit de65d4d649
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 256 additions and 16 deletions

View file

@ -10,11 +10,13 @@
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use clap::{crate_version, Arg, Command}; use clap::{crate_version, Arg, ArgMatches, Command};
use std::path::Component;
use std::{ use std::{
io::{stdout, Write}, io::{stdout, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use uucore::error::UClapError;
use uucore::{ use uucore::{
display::{print_verbatim, Quotable}, display::{print_verbatim, Quotable},
error::{FromIo, UResult}, error::{FromIo, UResult},
@ -32,12 +34,14 @@ static OPT_PHYSICAL: &str = "physical";
static OPT_LOGICAL: &str = "logical"; static OPT_LOGICAL: &str = "logical";
const OPT_CANONICALIZE_MISSING: &str = "canonicalize-missing"; const OPT_CANONICALIZE_MISSING: &str = "canonicalize-missing";
const OPT_CANONICALIZE_EXISTING: &str = "canonicalize-existing"; const OPT_CANONICALIZE_EXISTING: &str = "canonicalize-existing";
const OPT_RELATIVE_TO: &str = "relative-to";
const OPT_RELATIVE_BASE: &str = "relative-base";
static ARG_FILES: &str = "files"; static ARG_FILES: &str = "files";
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().get_matches_from(args); let matches = uu_app().try_get_matches_from(args).with_exit_code(1)?;
/* the list of files */ /* the list of files */
@ -58,8 +62,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
} else { } else {
MissingHandling::Normal MissingHandling::Normal
}; };
let resolve_mode = if strip {
ResolveMode::None
} else if logical {
ResolveMode::Logical
} else {
ResolveMode::Physical
};
let (relative_to, relative_base) = prepare_relative_options(&matches, can_mode, resolve_mode)?;
for path in &paths { for path in &paths {
let result = resolve_path(path, strip, zero, logical, can_mode); let result = resolve_path(
path,
zero,
resolve_mode,
can_mode,
relative_to.as_deref(),
relative_base.as_deref(),
);
if !quiet { if !quiet {
show_if_err!(result.map_err_context(|| path.maybe_quote().to_string())); show_if_err!(result.map_err_context(|| path.maybe_quote().to_string()));
} }
@ -126,20 +145,92 @@ pub fn uu_app<'a>() -> Command<'a> {
given name recursively, without requirements on components existence", given name recursively, without requirements on components existence",
), ),
) )
.arg(
Arg::new(OPT_RELATIVE_TO)
.long(OPT_RELATIVE_TO)
.takes_value(true)
.value_name("DIR")
.forbid_empty_values(true)
.help("print the resolved path relative to DIR"),
)
.arg(
Arg::new(OPT_RELATIVE_BASE)
.long(OPT_RELATIVE_BASE)
.takes_value(true)
.value_name("DIR")
.forbid_empty_values(true)
.help("print absolute paths unless paths below DIR"),
)
.arg( .arg(
Arg::new(ARG_FILES) Arg::new(ARG_FILES)
.multiple_occurrences(true) .multiple_occurrences(true)
.takes_value(true)
.required(true) .required(true)
.min_values(1) .forbid_empty_values(true)
.value_hint(clap::ValueHint::AnyPath), .value_hint(clap::ValueHint::AnyPath),
) )
} }
/// Prepare `--relative-to` and `--relative-base` options.
/// Convert them to their absolute values.
/// Check if `--relative-to` is a descendant of `--relative-base`,
/// otherwise nullify their value.
fn prepare_relative_options(
matches: &ArgMatches,
can_mode: MissingHandling,
resolve_mode: ResolveMode,
) -> UResult<(Option<PathBuf>, Option<PathBuf>)> {
let relative_to = matches.value_of(OPT_RELATIVE_TO).map(PathBuf::from);
let relative_base = matches.value_of(OPT_RELATIVE_BASE).map(PathBuf::from);
let relative_to = canonicalize_relative_option(relative_to, can_mode, resolve_mode)?;
let relative_base = canonicalize_relative_option(relative_base, can_mode, resolve_mode)?;
if let (Some(base), Some(to)) = (relative_base.as_deref(), relative_to.as_deref()) {
if !to.starts_with(base) {
return Ok((None, None));
}
}
Ok((relative_to, relative_base))
}
/// Prepare single `relative-*` option.
fn canonicalize_relative_option(
relative: Option<PathBuf>,
can_mode: MissingHandling,
resolve_mode: ResolveMode,
) -> UResult<Option<PathBuf>> {
Ok(match relative {
None => None,
Some(p) => Some(
canonicalize_relative(&p, can_mode, resolve_mode)
.map_err_context(|| p.maybe_quote().to_string())?,
),
})
}
/// Make `relative-to` or `relative-base` path values absolute.
///
/// # Errors
///
/// If the given path is not a directory the function returns an error.
/// If some parts of the file don't exist, or symlinks make loops, or
/// some other IO error happens, the function returns error, too.
fn canonicalize_relative(
r: &Path,
can_mode: MissingHandling,
resolve: ResolveMode,
) -> std::io::Result<PathBuf> {
let abs = canonicalize(r, can_mode, resolve)?;
if can_mode == MissingHandling::Existing && !abs.is_dir() {
abs.read_dir()?; // raise not a directory error
}
Ok(abs)
}
/// Resolve a path to an absolute form and print it. /// Resolve a path to an absolute form and print it.
/// ///
/// If `strip` is `true`, then this function does not attempt to resolve /// If `relative_to` and/or `relative_base` is given
/// symbolic links in the path. If `zero` is `true`, then this function /// the path is printed in a relative form to one of this options.
/// See the details in `process_relative` function.
/// If `zero` is `true`, then this function
/// prints the path followed by the null byte (`'\0'`) instead of a /// prints the path followed by the null byte (`'\0'`) instead of a
/// newline character (`'\n'`). /// newline character (`'\n'`).
/// ///
@ -149,22 +240,70 @@ pub fn uu_app<'a>() -> Command<'a> {
/// symbolic links. /// symbolic links.
fn resolve_path( fn resolve_path(
p: &Path, p: &Path,
strip: bool,
zero: bool, zero: bool,
logical: bool, resolve: ResolveMode,
can_mode: MissingHandling, can_mode: MissingHandling,
relative_to: Option<&Path>,
relative_base: Option<&Path>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let resolve = if strip {
ResolveMode::None
} else if logical {
ResolveMode::Logical
} else {
ResolveMode::Physical
};
let abs = canonicalize(p, can_mode, resolve)?; let abs = canonicalize(p, can_mode, resolve)?;
let line_ending = if zero { b'\0' } else { b'\n' }; let line_ending = if zero { b'\0' } else { b'\n' };
let abs = process_relative(abs, relative_base, relative_to);
print_verbatim(&abs)?; print_verbatim(&abs)?;
stdout().write_all(&[line_ending])?; stdout().write_all(&[line_ending])?;
Ok(()) Ok(())
} }
/// Conditionally converts an absolute path to a relative form,
/// according to the rules:
/// 1. if only `relative_to` is given, the result is relative to `relative_to`
/// 1. if only `relative_base` is given, it checks whether given `path` is a descendant
/// of `relative_base`, on success the result is relative to `relative_base`, otherwise
/// the result is the given `path`
/// 1. if both `relative_to` and `relative_base` are given, the result is relative to `relative_to`
/// if `path` is a descendant of `relative_base`, otherwise the result is `path`
///
/// For more information see
/// <https://www.gnu.org/software/coreutils/manual/html_node/Realpath-usage-examples.html>
fn process_relative(
path: PathBuf,
relative_base: Option<&Path>,
relative_to: Option<&Path>,
) -> PathBuf {
if let Some(base) = relative_base {
if path.starts_with(base) {
make_path_relative_to(&path, relative_to.unwrap_or(base))
} else {
path
}
} else if let Some(to) = relative_to {
make_path_relative_to(&path, to)
} else {
path
}
}
/// Converts absolute `path` to be relative to absolute `to` path.
fn make_path_relative_to(path: &Path, to: &Path) -> PathBuf {
let common_prefix_size = path
.components()
.zip(to.components())
.take_while(|(first, second)| first == second)
.count();
let path_suffix = path
.components()
.skip(common_prefix_size)
.map(|x| x.as_os_str());
let mut components: Vec<_> = to
.components()
.skip(common_prefix_size)
.map(|_| Component::ParentDir.as_os_str())
.chain(path_suffix)
.collect();
if components.is_empty() {
components.push(Component::CurDir.as_os_str());
}
components.iter().collect()
}

View file

@ -1,5 +1,7 @@
use crate::common::util::*; use crate::common::util::*;
#[cfg(windows)]
use regex::Regex;
use std::path::{Path, MAIN_SEPARATOR}; use std::path::{Path, MAIN_SEPARATOR};
static GIBBERISH: &str = "supercalifragilisticexpialidocious"; static GIBBERISH: &str = "supercalifragilisticexpialidocious";
@ -263,3 +265,102 @@ fn test_realpath_when_symlink_part_is_missing() {
.stderr_contains("realpath: dir1/foo2: No such file or directory\n") .stderr_contains("realpath: dir1/foo2: No such file or directory\n")
.stderr_contains("realpath: dir1/foo4: No such file or directory\n"); .stderr_contains("realpath: dir1/foo4: No such file or directory\n");
} }
#[test]
fn test_relative_existing_require_directories() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("dir1");
at.touch("dir1/f");
ucmd.args(&["-e", "--relative-base=.", "--relative-to=dir1/f", "."])
.fails()
.code_is(1)
.stderr_contains("directory");
}
#[test]
fn test_relative_existing_require_directories_2() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("dir1");
at.touch("dir1/f");
ucmd.args(&["-e", "--relative-base=.", "--relative-to=dir1", "."])
.succeeds()
.stdout_is("..\n");
}
#[test]
fn test_relative_base_not_prefix_of_relative_to() {
let result = new_ucmd!()
.args(&[
"-sm",
"--relative-base=/usr/local",
"--relative-to=/usr",
"/usr",
"/usr/local",
])
.succeeds();
#[cfg(windows)]
result.stdout_matches(&Regex::new(r"^.*:\\usr\n.*:\\usr\\local$").unwrap());
#[cfg(not(windows))]
result.stdout_is("/usr\n/usr/local\n");
}
#[test]
fn test_relative_string_handling() {
let result = new_ucmd!()
.args(&["-m", "--relative-to=prefix", "prefixed/1"])
.succeeds();
#[cfg(not(windows))]
result.stdout_is("../prefixed/1\n");
#[cfg(windows)]
result.stdout_is("..\\prefixed\\1\n");
let result = new_ucmd!()
.args(&["-m", "--relative-to=prefixed", "prefix/1"])
.succeeds();
#[cfg(not(windows))]
result.stdout_is("../prefix/1\n");
#[cfg(windows)]
result.stdout_is("..\\prefix\\1\n");
new_ucmd!()
.args(&["-m", "--relative-to=prefixed", "prefixed/1"])
.succeeds()
.stdout_is("1\n");
}
#[test]
fn test_relative() {
let result = new_ucmd!()
.args(&[
"-sm",
"--relative-base=/usr",
"--relative-to=/usr",
"/tmp",
"/usr",
])
.succeeds();
#[cfg(not(windows))]
result.stdout_is("/tmp\n.\n");
#[cfg(windows)]
result.stdout_matches(&Regex::new(r"^.*:\\tmp\n\.$").unwrap());
new_ucmd!()
.args(&["-sm", "--relative-base=/", "--relative-to=/", "/", "/usr"])
.succeeds()
.stdout_is(".\nusr\n"); // spell-checker:disable-line
let result = new_ucmd!()
.args(&["-sm", "--relative-base=/usr", "/tmp", "/usr"])
.succeeds();
#[cfg(not(windows))]
result.stdout_is("/tmp\n.\n");
#[cfg(windows)]
result.stdout_matches(&Regex::new(r"^.*:\\tmp\n\.$").unwrap());
new_ucmd!()
.args(&["-sm", "--relative-base=/", "/", "/usr"])
.succeeds()
.stdout_is(".\nusr\n"); // spell-checker:disable-line
}