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:
parent
6b00aec48e
commit
de65d4d649
2 changed files with 256 additions and 16 deletions
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue