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

4

19

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.

Crag answered 14/6, 2012 at 13:22 Comment(1)
I do not think anything like this exists built in. you are going to have to write the code yourself.Superego
M
13

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.

Munshi answered 14/6, 2012 at 13:42 Comment(3)
This won't work if there are .. or .s in the nonexistent part of the filename. E.g. /foo/bar/this_dir_doesn't_exist/../../baz should be normalised to /foo/baz but with your method it will be unchanged. I think you need to resolve .. and . first.Hairpiece
@Timmmm: You can't resolve them before calling realpath (or reimplementing it yourself) since you don't know if the component prior to a .. is a symlink. You just need to apply .. components in the tail after following the procedure in my answer if they might be present.Munshi
Hmm very good point - so, do your method and then resolve .. afterwards. I wish Linux had a syscall for this.Hairpiece
T
2

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
Territerrible answered 3/11, 2023 at 3:3 Comment(1)
Thanks for the answer, however this question was about the C programming language, not bash.Crag
C
0

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;
}
Curtice answered 30/3, 2020 at 2:54 Comment(1)
This doesn't work for multiple ../ in the fictitious part of the path. I would ideally want to get rid of all ./ and ../ in the string regardless if the path exists. _fullpath() on Windows does it. I don't really understand the use case of realpath() failing on a non-existent path. You can check a path with stat().Seersucker
C
0

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.

Collie answered 3/1, 2023 at 8:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.