Fabian Henze
Fabian Henze

Reputation: 1007

How to get absolute path of file or directory, that does *not* exist?

How can I determine the absolute path of a file or directory from a given relative path in C/C++ on GNU/Linux?
I know about realpath(), but it does not work on non-existing files.

Let's say the user enters ../non-existant-directory/file.txt, and the programs working directory is /home/user/.
What I need is a function that returns /home/non-existant-directory/file.txt.

I need this function to check if a given path is in a certain subdirectory or not.

Upvotes: 20

Views: 14084

Answers (4)

le-penseur
le-penseur

Reputation: 31

I know that this is a very old topic, but in case someone comes here looking for an answer to if realpath will work when one or more path components in its operand is missing:

The answer, at least as of coreutils v. 8+, is "yes" on most Linux variants, if you supply the "-m" option to it. Here's the relevant help text:

$ realpath --help
Usage: realpath [OPTION]... FILE...
Print the resolved absolute file name;
all but the last component must exist

  -e, --canonicalize-existing  all components of the path must exist
  -m, --canonicalize-missing   no components of the path need exist
  -L, --logical                resolve '..' components before symlinks
  -P, --physical               resolve symlinks as encountered (default)
  -q, --quiet                  suppress most error messages
      --relative-to=FILE       print the resolved path relative to FILE
      --relative-base=FILE     print absolute paths unless paths below FILE
  -s, --strip, --no-symlinks   don't expand symlinks
  -z, --zero                   separate output with NUL rather than newline

      --help     display this help and exit
      --version  output version information and exit

Here's an example on a machine with coreutils v. 8.22 installed:

$ pwd
/home/alice/foo/bar
$ ls /home/alice/foo/bar/baz/quux/splat
ls: cannot access /home/alice/foo/bar/baz/quux/splat: No such file or directory
$ realpath -e ../../foo/bar/baz/quux/splat
realpath: ‘../../foo/bar/baz/quux/splat’: No such file or directory
$ realpath -m ../../foo/bar/baz/quux/splat
/home/alice/foo/bar/baz/quux/splat

Upvotes: 3

Jagadeesh Kotra
Jagadeesh Kotra

Reputation: 194

you can try to do something like this:

let pbuf = std::path::PathBuf::from_str("somewhere/something.json").unwrap();
let parent = pbuf.parent().unwrap().canonicalize().unwrap_or(std::env::current_dir().unwrap());
let file = pbuf.file_name().unwrap().to_str().unwrap();
let final_path = parent.join(file).to_str().unwrap().to_owned();

if parent path does not exist (or fails for whatever reason), it will attach current directory in it's place.

Upvotes: 0

Patryk Lech
Patryk Lech

Reputation: 1

As noted by @R.. GitHub, you can build this functionality on realpath(). Here is an example function, which uses realpath() to determine the canonical form of the portion of the path which exists, and appends the non-existent portion of the path to it.

Since realpath() operates on C-style strings, I decided to use them here too. But the function can easily be re-written to use std::string (just don't forget to free canonical_file_path after copying it to an std::string!).

Please note that duplicate "/" entries are not removed from the portion of the path which does not exist; it is simply appended to canonical form of the portion which does exist.

////////////////////////////////////////////////////////////////////////////////
// Return the input path in a canonical form. This is achieved by expanding all
// symbolic links, resolving references to "." and "..", and removing duplicate
// "/" characters.
//
// If the file exists, its path is canonicalized and returned. If the file,
// or parts of the containing directory, do not exist, path components are
// removed from the end until an existing path is found. The remainder of the
// path is then appended to the canonical form of the existing path,
// and returned. Consequently, the returned path may not exist. The portion
// of the path which exists, however, is represented in canonical form.
//
// If successful, this function returns a C-string, which needs to be freed by
// the caller using free().
//
// ARGUMENTS:
//   file_path
//   File path, whose canonical form to return.
//
// RETURNS:
//   On success, returns the canonical path to the file, which needs to be freed
//   by the caller.
//
//   On failure, returns NULL.
////////////////////////////////////////////////////////////////////////////////
char *make_file_name_canonical(char const *file_path)
{
  char *canonical_file_path  = NULL;
  unsigned int file_path_len = strlen(file_path);

  if (file_path_len > 0)
  {
    canonical_file_path = realpath(file_path, NULL);
    if (canonical_file_path == NULL && errno == ENOENT)
    {
      // The file was not found. Back up to a segment which exists,
      // and append the remainder of the path to it.
      char *file_path_copy = NULL;
      if (file_path[0] == '/'                ||
          (strncmp(file_path, "./", 2) == 0) ||
          (strncmp(file_path, "../", 3) == 0))
      {
        // Absolute path, or path starts with "./" or "../"
        file_path_copy = strdup(file_path);
      }
      else
      {
        // Relative path
        file_path_copy = (char*)malloc(strlen(file_path) + 3);
        strcpy(file_path_copy, "./");
        strcat(file_path_copy, file_path);
      }

      // Remove path components from the end, until an existing path is found
      for (int char_idx = strlen(file_path_copy) - 1;
           char_idx >= 0 && canonical_file_path == NULL;
           --char_idx)
      {
        if (file_path_copy[char_idx] == '/')
        {
          // Remove the slash character
          file_path_copy[char_idx] = '\0';

          canonical_file_path = realpath(file_path_copy, NULL);
          if (canonical_file_path != NULL)
          {
            // An existing path was found. Append the remainder of the path
            // to a canonical form of the existing path.
            char *combined_file_path = (char*)malloc(strlen(canonical_file_path) + strlen(file_path_copy + char_idx + 1) + 2);
            strcpy(combined_file_path, canonical_file_path);
            strcat(combined_file_path, "/");
            strcat(combined_file_path, file_path_copy + char_idx + 1);
            free(canonical_file_path);
            canonical_file_path = combined_file_path;
          }
          else
          {
            // The path segment does not exist. Replace the slash character
            // and keep trying by removing the previous path component.
            file_path_copy[char_idx] = '/';
          }
        }
      }

      free(file_path_copy);
    }
  }

  return canonical_file_path;
}

Upvotes: 0

R.. GitHub STOP HELPING ICE
R.. GitHub STOP HELPING ICE

Reputation: 215221

Try realpath. If it fails, start removing path components from the end one at a time and retrying realpath until it succeeds. Then append the components you removed back onto the result of the successful realpath call.

If you're sure the containing directory exists and you just want to make a file there, you only have to remove at most one component.

Another approach would be to just create the file first, then call realpath.

Upvotes: 13

Related Questions