Hello all,

I've encountered two interesting (?) edge-cases with realpath.
Perhaps they warrent a change in documentation (or even error-checking code).

First,
Running 'realpath' in a deleted (unlinked) directory leads to confusing
error messages. Example:

   # In terminal 1
   mkdir /tmp/a
   cd /tmp/a

   # In another terminal
   rmdir /tmp/a

   # back in terminal 1
   $ realpath /foo
   /foo

   $ realpath foo
   realpath: foo: No such file or directory

   $ realpath ../foo
   realpath: ../foo: No such file or directory

   $ realpath -m foo
   realpath: foo: No such file or directory

I'd say this error message is confusing, because naively 'realpath' does
not require the last component to exist, and with '-m' it doesn't
require any component to exist. Yet the error message hints that
something that should exist doesn't.

The reason for the error message is this flow:
  src/realpath.c:process_path()
    src/realpath.c:realpath_canon()
      gnulib/lib/canonicalize.c:canonicalize_filename_mode()
        gnulib/lib/xgetcwd.c:xgetcwd()

Then xgetcwd fails (because the directory was deleted),
but despite the 'x' prefix, it only dies on ENOMEM,
not on ENOENT. It returns NULL, and the rest of the program
fails, but errno remains at ENOENT.

Not sure what would be a good gnulib fix without breaking
existing code that uses 'canonicalize_filename_mode'.

Of course, it is always recommended not to run programs
in a deleted directory... many programs will fail with confusing
errors.


Second,
`realpath --help` shows:
 --relative-to=FILE       print the resolved path relative to FILE
 --relative-base=FILE     print absolute paths unless paths below FILE

This hints (to a naive person like me :) ) that realpath will calculate
relative path to any file. But in fact, it assumes that the FILE
argument is always a directory, and with 'realpath -e' it indeeds
verifies it's a directory.

Example:

   $ mkdir /tmp/b
   $ cd /tmp/b
   $ touch 1
   $ realpath --relative-to=/tmp/b/1 2
   ../2
   $ realpath -e --relative-to=/tmp/b/1 2
   realpath: /tmp/b/1: Not a directory

One could argue it doesn't matter since 'realpath'
is mainly used to construct path strings in script.
But the resulting concatenated string is not a valid path:

   $ touch /tmp/b/1/../2
   touch: cannot touch '/tmp/b/1/../2': Not a directory

That is, if someone writes a shell script like:

   base=/foo/bar/file.txt
   rel=$(realpath --relative-to="$base" another.txt)

There might be assumption that "$base/$rel" should always work,
but it only works correctly if '$base' is a directory, despite
realpath not returning error message.

I assume that checking if the argument to --relative-{to,base}
is actually a directory when not using 'realpath -e' is not
acceptable as it will break existing behaviour.

So perhaps just update the usage text? (patch attached).
If not, then add a new 'gotcha' ?

regards,
- assaf

>From 4531af6d156eb1391a1152a5bd27488159ad2f68 Mon Sep 17 00:00:00 2001
From: Assaf Gordon <[email protected]>
Date: Sat, 24 Jun 2017 19:47:26 -0400
Subject: [PATCH] realpath: improve usage description for --relative-{to,base}

* src/realpath.c (usage): Explicitly say 'DIR' instead of 'FILE' for
--relative-{to,base} parameters, to avoid giving the impression
that regular files can be used as relative base.
---
 src/realpath.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/realpath.c b/src/realpath.c
index 92d88c5..d4c1595 100644
--- a/src/realpath.c
+++ b/src/realpath.c
@@ -81,8 +81,8 @@ all but the last component must exist\n\
   -L, --logical                resolve '..' components before symlinks\n\
   -P, --physical               resolve symlinks as encountered (default)\n\
   -q, --quiet                  suppress most error messages\n\
-      --relative-to=FILE       print the resolved path relative to FILE\n\
-      --relative-base=FILE     print absolute paths unless paths below FILE\n\
+      --relative-to=DIR        print the resolved path relative to DIR\n\
+      --relative-base=DIR      print absolute paths unless paths below DIR\n\
   -s, --strip, --no-symlinks   don't expand symlinks\n\
   -z, --zero                   end each output line with NUL, not newline\n\
 \n\
-- 
2.8.2

Reply via email to