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.

Fabian Henze
  • 980
  • 1
  • 10
  • 27
  • I do not think anything like this exists built in. you are going to have to write the code yourself. – Colin D Jun 14 '12 at 13:27

3 Answers3

12

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.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • 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. – Timmmm Nov 08 '21 at 15:19
  • 1
    @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. – R.. GitHub STOP HELPING ICE Nov 08 '21 at 17:13
  • Hmm very good point - so, do your method and then resolve `..` afterwards. I wish Linux had a syscall for this. – Timmmm Nov 09 '21 at 11:24
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;
}
  • 2
    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()`. – Rich Jahn Sep 10 '20 at 19:57
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.

Jagadeesh Kotra
  • 194
  • 1
  • 3
  • 9