5

How do I get a parent directory for a file?

I want it to be safe on all kind of names:

.
..
path/to/my/file
/absolute/path/to/my/file
'-rf --no-preserve-root whatever'/test.zip
(symbolic links)
`'"`'{(})

I am more interested getting the canonical location on the file system than in traversing the path stated in the filename.

Note that there are similar questions to this one, but none of them focuses on correctness, relative/absolute paths and "unsafe" names:

[1] bash get the parent directory of current directory

[2] Retrieve parent directory of script

[3] bash filepath to parent directory of file

VasiliNovikov
  • 9,681
  • 4
  • 44
  • 62
  • 1
    Are you looking for the full pathname of the directory that contains a file, or the parent of the directory containing the file? Are you more interested in traversing the path stated in the filename, or getting the canonical location on your filesystem? – ghoti Nov 20 '16 at 02:46
  • The second case for both of the questions. – VasiliNovikov Nov 20 '16 at 02:55
  • Suppose your current directory is `/home/user/bin` and the 'file name' is `..` (the parent current directory). Presumably, the directory containing that entry is `/home/user/bin` so the parent directory is `/home/user`, or is there some other interpretation required (the directory `..` is `/home/user` so the parent directory is `/home`, perhaps). I think the 'presumed' variant is correct, and similarly with `.`, but maybe it is as well to check. – Jonathan Leffler Nov 20 '16 at 03:38
  • What is the approved interpretation of the `-rf…/test.zip` name? Is that a directory name with blanks, dashes and so on in it? Also, what should be done if the file name doesn't exist? Is that an error, or should the code make a textual analysis of the file name to try and deduce what would be the name if the file existed? – Jonathan Leffler Nov 20 '16 at 03:39
  • @JonathanLeffler yes, I think your interpretation of `.` and mine are similar. For a path `/home/user/bin` I would expect the "parent directory" to be `/home/user`. Since my and everyone's usage will probably be very specific, it's a case that should be verified manually. – VasiliNovikov Nov 20 '16 at 03:50
  • @JonathanLeffler yes, `-rf --no-preserve-root whatever` should be treated like a single path part. I should've probably put it in quotes, should I? I've edited the question. – VasiliNovikov Nov 20 '16 at 03:50
  • 1
    @JonathanLeffler As for "what if file does not exist" - I honestly don't know. SO questions tend to overgrow the original author-s intent, at least for the worthwile questions. So my interpretation and yours may vary. I personally _know_ a file exists and these cases are indistinguishable for me. – VasiliNovikov Nov 20 '16 at 03:50

2 Answers2

9

Get parent directory of your current directory:

parent_dir="$(dirname -- "$(realpath -- "$PWD")")"

Get the directory of the script you're running:

parent_dir="$(dirname -- "$(realpath -- "$0")")"

Get parent directory of anything:

parent_dir="$(dirname -- "$(realpath -- "$file_or_dir_name")")"

If your system does not have realpath but does have readlink, this should work:

parent_dir="$(dirname -- "$(readlink -f -- "$file_name")")"

Enjoy!

VasiliNovikov
  • 9,681
  • 4
  • 44
  • 62
  • This was a bash question. I use bash on OS X, FreeBSD and other operating systems that are not Linux which do not have the `realpath` command you're suggesting, which is not part of bash. Heck, even on the Linux systems I administer, `realpath` is available but not installed by default. If you DO feel the need to suggest things that only work on one particular platform, at least mention the platform. – ghoti Nov 20 '16 at 02:42
  • 1
    @ghoti FreeBSD has a [`realpath(1)`](https://www.freebsd.org/cgi/man.cgi?query=realpath) utility. "The realpath utility first appeared in FreeBSD 4.3." – kdhp Nov 20 '16 at 03:17
  • 2
    @kdhp: Even though macOS Sierra (and Mac OS X before it) are derived from BSD, they do not include a `realpath` command by default. On my machine, I have my implementation of the command and the GNU implementation of the command. And, given that `realpath` is not standardized (e.g. by POSIX), it is unlikely that the FreeBSD and GNU implementations are fully compatible. With luck, they have a common subset of functionality, though. There is a `readlink` command on macOS, but it has far fewer options than GNU's version does (it has the `-n` option, which appears to be the same on both). – Jonathan Leffler Nov 20 '16 at 03:41
  • 2
    What is the benefit of `printf '%s\n' "$(dirname …)"` compared with simply using `dirname …`? – Jonathan Leffler Nov 20 '16 at 03:57
  • @JonathanLeffler good question! In my actual script I've needed a variable, so the code was `parent_dir="$(...)"`. I guess I should either use a variable or avoid the `printf` layer. Will do the first. Thanks again! – VasiliNovikov Nov 20 '16 at 04:00
  • @JonathanLeffler The FreeBSD `realpath(1)` utility dates back to 2002 (after the initial OS X release) and is not found outside of FreeBSD and its forks. FreeBSD and GNU `realpath(1)` should be compatible, apart from options, because the [`realpath(3)` function](http://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html) is standardized. However `readlink(1)` will be preset in any GNU or BSD userland (apart from a few legacy edge cases). – kdhp Nov 20 '16 at 04:05
  • @kdhp, my goodness, right you are. I must have mistyped when I checked earlier. Thanks for pointing that out. Nevertheless, it's not part of bash. – ghoti Nov 20 '16 at 04:10
  • 1
    @kdhp: This is not really an argument, but an FYI … Interesting history! Also, the macOS `readlink` has precisely one option (`-n` for no newline); GNU `readlink` has 8 options (each with a long name and a short single-character name) plus `--help` and `--version`. Of these, the `-n` (`--no-newline`) option is the same between the two. I'm not sure whether the 7 options are the 'legacy edge cases' you mention. I agree that the core functionality of `realpath` the command is based on `realpath` the function. Maybe I'll delete this after the movie...it's not dreadfully important. – Jonathan Leffler Nov 20 '16 at 04:15
  • @JonathanLeffler You are right, the `-f` option is fairly new (2010) and macOS (OS X) tends to use aged BSD utilities. As for edge-cases I was referring to the case where `realpath(1)` exists while `readlink(1)` does not. – kdhp Nov 20 '16 at 04:18
2

Bash's cd command has a couple of interesting but little-used options, -P and -L.

   cd [-L|[-P [-e]] [-@]] [dir]
      ...    The  -P  option  causes  cd to use the physical directory
      structure by resolving symbolic links while traversing  dir  and
      before processing instances of .. in dir (see also the -P option
      to the set builtin command); the -L option forces symbolic links
      to  be followed by resolving the link after processing instances
      of .. in dir. ...

So ... if you're looking for the physical location in the filesystem of your current working directory, you could use something like this:

realwd="$(cd -P .; pwd)"

In your comments, you mentioned that you're looking for the parent directory of the directory containing a file -- so, if a path is /foo/bar/baz/filename, you'd be looking for /foo/bar.

To get this, I would suggest a combination of cd -P and parameter expansion. Since you know that the / character can never exist as part of a filename, the following might work for you:

grandparent() {
    local realdir="$(cd -P "${1%/*}"; pwd)"
    echo "${realdir%/*}"
}

This works by using cd -P to "get" the physical location of the file, then parameter expansion to strip off the last item in the path.

$ mkdir -p one/two/three
$ touch one/two/three/foo
$ ln -s one/two/three bar
$ ls -l bar
lrwxr-xr-x  1 ghoti  wheel  13 Nov 19 23:05 bar -> one/two/three
$ grandparent bar/foo
/usr/home/ghoti/tmp6/one/two
ghoti
  • 45,319
  • 8
  • 65
  • 104
  • 1
    `-L` and `-P` are [not `bash` specific](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/cd.html). – kdhp Nov 20 '16 at 04:13
  • 1
    @kdhp - true, but they're also not universal -- there are still tcsh users out there. I mentioned that they're bash options because the OP tagged his question [tag:bash]. The function in my answer should work in any POSIX shell .. and also in bash. :) – ghoti Nov 20 '16 at 14:37