Skip to main content

uu_realpath/
realpath.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5
6// spell-checker:ignore (ToDO) retcode
7
8use clap::{
9    Arg, ArgAction, ArgMatches, Command,
10    builder::{TypedValueParser, ValueParserFactory},
11};
12use std::{
13    ffi::{OsStr, OsString},
14    io::{Write, stdout},
15    path::{Path, PathBuf},
16};
17use uucore::fs::make_path_relative_to;
18use uucore::translate;
19use uucore::{
20    display::{Quotable, print_verbatim},
21    error::{FromIo, UResult},
22    format_usage,
23    fs::{MissingHandling, ResolveMode, canonicalize},
24    line_ending::LineEnding,
25    show_if_err,
26};
27
28const OPT_QUIET: &str = "quiet";
29const OPT_STRIP: &str = "strip";
30const OPT_ZERO: &str = "zero";
31const OPT_PHYSICAL: &str = "physical";
32const OPT_LOGICAL: &str = "logical";
33const OPT_CANONICALIZE_MISSING: &str = "canonicalize-missing";
34const OPT_CANONICALIZE: &str = "canonicalize";
35const OPT_CANONICALIZE_EXISTING: &str = "canonicalize-existing";
36const OPT_RELATIVE_TO: &str = "relative-to";
37const OPT_RELATIVE_BASE: &str = "relative-base";
38
39const ARG_FILES: &str = "files";
40
41/// Custom parser that validates `OsString` is not empty
42#[derive(Clone, Debug)]
43struct NonEmptyOsStringParser;
44
45impl TypedValueParser for NonEmptyOsStringParser {
46    type Value = OsString;
47
48    fn parse_ref(
49        &self,
50        _cmd: &Command,
51        _arg: Option<&Arg>,
52        value: &OsStr,
53    ) -> Result<Self::Value, clap::Error> {
54        if value.is_empty() {
55            let mut err = clap::Error::new(clap::error::ErrorKind::ValueValidation);
56            err.insert(
57                clap::error::ContextKind::Custom,
58                clap::error::ContextValue::String(translate!("realpath-invalid-empty-operand")),
59            );
60            return Err(err);
61        }
62        Ok(value.to_os_string())
63    }
64}
65
66impl ValueParserFactory for NonEmptyOsStringParser {
67    type Parser = Self;
68
69    fn value_parser() -> Self::Parser {
70        Self
71    }
72}
73
74#[uucore::main(no_signals)]
75pub fn uumain(args: impl uucore::Args) -> UResult<()> {
76    let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
77
78    /*  the list of files */
79
80    let paths: Vec<PathBuf> = matches
81        .get_many::<OsString>(ARG_FILES)
82        .unwrap()
83        .map(PathBuf::from)
84        .collect();
85
86    let strip = matches.get_flag(OPT_STRIP);
87    let line_ending = LineEnding::from_zero_flag(matches.get_flag(OPT_ZERO));
88    let quiet = matches.get_flag(OPT_QUIET);
89    let logical = matches.get_flag(OPT_LOGICAL);
90    let can_mode = if matches.get_flag(OPT_CANONICALIZE_MISSING) {
91        MissingHandling::Missing
92    } else if matches.get_flag(OPT_CANONICALIZE_EXISTING) {
93        // -e: all components must exist
94        // Despite the name, MissingHandling::Existing requires all components to exist
95        MissingHandling::Existing
96    } else {
97        // Default behavior (same as -E): all but last component must exist
98        // MissingHandling::Normal allows the final component to not exist
99        MissingHandling::Normal
100    };
101    let resolve_mode = if strip {
102        ResolveMode::None
103    } else if logical {
104        ResolveMode::Logical
105    } else {
106        ResolveMode::Physical
107    };
108    let (relative_to, relative_base) = prepare_relative_options(&matches, can_mode, resolve_mode)?;
109    for path in &paths {
110        let result = resolve_path(
111            path,
112            line_ending,
113            resolve_mode,
114            can_mode,
115            relative_to.as_deref(),
116            relative_base.as_deref(),
117        );
118        if !quiet {
119            show_if_err!(result.map_err_context(|| path.maybe_quote().to_string()));
120        }
121    }
122    // Although we return `Ok`, it is possible that a call to
123    // `show!()` above has set the exit code for the program to a
124    // non-zero integer.
125    Ok(())
126}
127
128pub fn uu_app() -> Command {
129    Command::new("realpath")
130        .version(uucore::crate_version!())
131        .help_template(uucore::localized_help_template("realpath"))
132        .about(translate!("realpath-about"))
133        .override_usage(format_usage(&translate!("realpath-usage")))
134        .infer_long_args(true)
135        .arg(
136            Arg::new(OPT_QUIET)
137                .short('q')
138                .long(OPT_QUIET)
139                .help(translate!("realpath-help-quiet"))
140                .action(ArgAction::SetTrue),
141        )
142        .arg(
143            Arg::new(OPT_STRIP)
144                .short('s')
145                .long(OPT_STRIP)
146                .visible_alias("no-symlinks")
147                .help(translate!("realpath-help-strip"))
148                .action(ArgAction::SetTrue),
149        )
150        .arg(
151            Arg::new(OPT_ZERO)
152                .short('z')
153                .long(OPT_ZERO)
154                .help(translate!("realpath-help-zero"))
155                .action(ArgAction::SetTrue),
156        )
157        .arg(
158            Arg::new(OPT_LOGICAL)
159                .short('L')
160                .long(OPT_LOGICAL)
161                .help(translate!("realpath-help-logical"))
162                .action(ArgAction::SetTrue),
163        )
164        .arg(
165            Arg::new(OPT_PHYSICAL)
166                .short('P')
167                .long(OPT_PHYSICAL)
168                .overrides_with_all([OPT_STRIP, OPT_LOGICAL])
169                .help(translate!("realpath-help-physical"))
170                .action(ArgAction::SetTrue),
171        )
172        .arg(
173            Arg::new(OPT_CANONICALIZE)
174                .short('E')
175                .long(OPT_CANONICALIZE)
176                .overrides_with_all([OPT_CANONICALIZE_EXISTING, OPT_CANONICALIZE_MISSING])
177                .help(translate!("realpath-help-canonicalize"))
178                .action(ArgAction::SetTrue),
179        )
180        .arg(
181            Arg::new(OPT_CANONICALIZE_EXISTING)
182                .short('e')
183                .long(OPT_CANONICALIZE_EXISTING)
184                .overrides_with_all([OPT_CANONICALIZE, OPT_CANONICALIZE_MISSING])
185                .help(translate!("realpath-help-canonicalize-existing"))
186                .action(ArgAction::SetTrue),
187        )
188        .arg(
189            Arg::new(OPT_CANONICALIZE_MISSING)
190                .short('m')
191                .long(OPT_CANONICALIZE_MISSING)
192                .overrides_with_all([OPT_CANONICALIZE, OPT_CANONICALIZE_EXISTING])
193                .help(translate!("realpath-help-canonicalize-missing"))
194                .action(ArgAction::SetTrue),
195        )
196        .arg(
197            Arg::new(OPT_RELATIVE_TO)
198                .long(OPT_RELATIVE_TO)
199                .value_name("DIR")
200                .value_parser(NonEmptyOsStringParser)
201                .help(translate!("realpath-help-relative-to")),
202        )
203        .arg(
204            Arg::new(OPT_RELATIVE_BASE)
205                .long(OPT_RELATIVE_BASE)
206                .value_name("DIR")
207                .value_parser(NonEmptyOsStringParser)
208                .help(translate!("realpath-help-relative-base")),
209        )
210        .arg(
211            Arg::new(ARG_FILES)
212                .action(ArgAction::Append)
213                .required(true)
214                .value_parser(NonEmptyOsStringParser)
215                .value_hint(clap::ValueHint::AnyPath),
216        )
217}
218
219/// Prepare `--relative-to` and `--relative-base` options.
220/// Convert them to their absolute values.
221/// Check if `--relative-to` is a descendant of `--relative-base`,
222/// otherwise nullify their value.
223fn prepare_relative_options(
224    matches: &ArgMatches,
225    can_mode: MissingHandling,
226    resolve_mode: ResolveMode,
227) -> UResult<(Option<PathBuf>, Option<PathBuf>)> {
228    let relative_to = matches
229        .get_one::<OsString>(OPT_RELATIVE_TO)
230        .map(PathBuf::from);
231    let relative_base = matches
232        .get_one::<OsString>(OPT_RELATIVE_BASE)
233        .map(PathBuf::from);
234    let relative_to = canonicalize_relative_option(relative_to, can_mode, resolve_mode)?;
235    let relative_base = canonicalize_relative_option(relative_base, can_mode, resolve_mode)?;
236    match (relative_base.as_deref(), relative_to.as_deref()) {
237        (Some(b), Some(t)) if !t.starts_with(b) => Ok((None, None)),
238        _ => Ok((relative_to, relative_base)),
239    }
240}
241
242/// Prepare single `relative-*` option.
243fn canonicalize_relative_option(
244    relative: Option<PathBuf>,
245    can_mode: MissingHandling,
246    resolve_mode: ResolveMode,
247) -> UResult<Option<PathBuf>> {
248    Ok(match relative {
249        None => None,
250        Some(p) => Some(
251            canonicalize_relative(&p, can_mode, resolve_mode)
252                .map_err_context(|| p.maybe_quote().to_string())?,
253        ),
254    })
255}
256
257/// Make `relative-to` or `relative-base` path values absolute.
258///
259/// # Errors
260///
261/// If the given path is not a directory the function returns an error.
262/// If some parts of the file don't exist, or symlinks make loops, or
263/// some other IO error happens, the function returns error, too.
264fn canonicalize_relative(
265    r: &Path,
266    can_mode: MissingHandling,
267    resolve: ResolveMode,
268) -> std::io::Result<PathBuf> {
269    let abs = canonicalize(r, can_mode, resolve)?;
270    if can_mode == MissingHandling::Existing && !abs.is_dir() {
271        abs.read_dir()?; // raise not a directory error
272    }
273    Ok(abs)
274}
275
276/// Resolve a path to an absolute form and print it.
277///
278/// If `relative_to` and/or `relative_base` is given
279/// the path is printed in a relative form to one of this options.
280/// See the details in `process_relative` function.
281/// If `zero` is `true`, then this function
282/// prints the path followed by the null byte (`'\0'`) instead of a
283/// newline character (`'\n'`).
284///
285/// # Errors
286///
287/// This function returns an error if there is a problem resolving
288/// symbolic links.
289fn resolve_path(
290    p: &Path,
291    line_ending: LineEnding,
292    resolve: ResolveMode,
293    can_mode: MissingHandling,
294    relative_to: Option<&Path>,
295    relative_base: Option<&Path>,
296) -> std::io::Result<()> {
297    let abs = canonicalize(p, can_mode, resolve)?;
298
299    let abs = process_relative(abs, relative_base, relative_to);
300
301    print_verbatim(abs)?;
302    stdout().write_all(&[line_ending.into()])?;
303    Ok(())
304}
305
306/// Conditionally converts an absolute path to a relative form,
307/// according to the rules:
308/// 1. if only `relative_to` is given, the result is relative to `relative_to`
309/// 2. if only `relative_base` is given, it checks whether given `path` is a descendant
310///    of `relative_base`, on success the result is relative to `relative_base`, otherwise
311///    the result is the given `path`
312/// 3. if both `relative_to` and `relative_base` are given, the result is relative to `relative_to`
313///    if `path` is a descendant of `relative_base`, otherwise the result is `path`
314///
315/// For more information see
316/// <https://www.gnu.org/software/coreutils/manual/html_node/Realpath-usage-examples.html>
317fn process_relative(
318    path: PathBuf,
319    relative_base: Option<&Path>,
320    relative_to: Option<&Path>,
321) -> PathBuf {
322    if let Some(base) = relative_base {
323        if path.starts_with(base) {
324            make_path_relative_to(path, relative_to.unwrap_or(base))
325        } else {
326            path
327        }
328    } else if let Some(to) = relative_to {
329        make_path_relative_to(path, to)
330    } else {
331        path
332    }
333}