18

Imagine I have a path that doesn't exist:

$ ls /foo/bar/baz/hello/world
ls: cannot access /foo/bar/baz/hello/world: No such file or directory

But let's say /foo/bar does exist. Is there a quick way for me to determine that baz is the breaking point in the path?

I'm using Bash.

kuzzooroo
  • 283
  • access(2) is not very granular, so the solution typically involves writing something to iterate and test each path element in turn... – thrig Feb 03 '16 at 23:53

5 Answers5

15

One of my favourite utilties is namei, part of util-linux and hence generally present only on Linux:

$ namei /usr/share/foo/bar
f: /usr/share/foo/bar
 d /
 d usr
 d share
   foo - No such file or directory

But its output is not very parseable. So, if you just wish to point out something is missing, namei might be useful.

It's useful for troubleshooting general problems in accessing a path, since you can get it to state whether a component is a link or a mount point, as well as its permissions:

$ ln -sf /usr/foo/bar /tmp/
$ namei -lx /tmp/bar
f: /tmp/bar
Drwxr-xr-x root    root    /
Drwxrwxrwt root    root    tmp
lrwxrwxrwx muru    muru    bar -> /usr/foo/bar
Drwxr-xr-x root    root      /
drwxr-xr-x root    root      usr
                             foo - No such file or directory

The capital D indicates a mount point.

muru
  • 72,889
  • @StéphaneChazelas I hoped equivalents might be present on other systems. Nothing for the BSDs? – muru Feb 04 '16 at 05:47
  • namei works for me as long as a feed it a path that exists, but when I give it one that doesn't I get namei: failed to stat: /usr/share/foo/bar: No such file or directory. – kuzzooroo Feb 06 '16 at 00:36
  • @kuzzooroo which version of namei? – muru Feb 06 '16 at 03:07
  • my version of namei does not give a version in its help nor its man page and does not support a flag giving version info. I am able to determine that it comes from the package linux-utils 2.16-1ubuntu5 but can't figure out what that implies for the version of namei. – kuzzooroo Feb 07 '16 at 17:21
  • @kuzzooroo Since even Ubuntu 12.04 has 2.20.1, you must be on a very old version of Ubuntu indeed. 10.04? – muru Feb 07 '16 at 17:23
  • Yes, ancient indeed: when I'm on Linux, it's 9.10 (this is in an enterprise context in which we're very careful about when we upgrade). Most of the time I'm on a Mac, but as far as I can tell there's no namei port. – kuzzooroo Feb 07 '16 at 17:42
  • I've accepted one of the answers below since they aren't linux-specific, but this is what I would use if I were working on a modern version of Linux. – kuzzooroo Feb 07 '16 at 17:43
5

Given a canonical pathname, such as yours, this will work:

set -f --; IFS=/
for p in $pathname
do    [ -e "$*/$p" ] || break
      set -- "$@" "$p"
done; printf %s\\n "$*"

That prints through the last fully existing/accessible component of $pathname, and puts each of those separately into the arg array. The first nonexistent component is not printed, but it is saved in $p.

You might approach it oppositely:

until cd -- "$path" && cd -
do    case   $path  in
      (*[!/]/*)
              path="${path%/*}"
;;    (*)   ! break
      esac
done  2>/dev/null   && cd -

That will either return appropriately or will pare down $path as needed. It declines to attempt a change to /, but if successful will print both your current working directory and the directory to which it changes to stdout. Your current $PWD will be put in $OLDPWD as well.

mikeserv
  • 58,310
  • Clearly: for p in $pathname will be subject to "word splitting" and "Pathname expansion". –  Feb 04 '16 at 05:53
  • 1
    @BinaryZebra - yes, it is split on $IFS. that's exactly how it works. it is not subject to pathname expansion, except that the variable here called $pathname is expanded to an array of path components as split on $IFS. – mikeserv Feb 04 '16 at 06:03
  • You are avoiding the "Pathname expansion" on var $pathname by using set -f" which is not returned to its default value. –  Feb 04 '16 at 17:45
  • @BinaryZebra - im not avoiding anything. im specifying the environment i need in order to do this robustly. thats all. no - its not returned to its default value and neither will it install itself on the asker's computer or make me breakfast. – mikeserv Feb 04 '16 at 17:49
  • And in the process of setting your environment, change the environment for any code following. –  Feb 04 '16 at 17:54
  • @BinaryZebra - there is no code following. if someone wanted to append some they might or might not want to reset $IFS and globbing before they do. Or they might just run it in a subshell - or as its own script. Should someone want to reset those to defaults they could do it like set +f; IFS='<space><tab><newline>'. Have you any other pressing concerns of which I should be made aware? – mikeserv Feb 04 '16 at 17:59
  • 1
    @BinaryZebra - by the by, if, as any good programmer should, you often find yourself worrying about unnecessarily affecting/recovering your execution environment, you might take an interest in ns, which, if used here, would render this discussion moot. – mikeserv Feb 04 '16 at 18:13
  • For posterity: I had to add pathname="$1" to get this code to run on my command line argument. Just going with for p in $1 didn't work. – kuzzooroo Feb 07 '16 at 17:45
  • This approach will fail if one of the existing folders in our path was "chmod 000 dir" (i.e. no one can read/write/execute). But if you use "sudo namei" then it will correctly indicate that folder and will continue digging. The error of this approach is an assumption that we can "cd" into components. – Dmitry Shevkoplyas Nov 04 '22 at 21:10
  • @DmitryShevkoplyas please dont encourage the use of sudo anything when dealing with path components. of course if your file system is configured to keep your username out of a path, this will not allow you in. you can sudo cd all day, too. – mikeserv Dec 31 '22 at 00:18
1

Something like this (accounting for pathnames with embedded blanks):

#!/bin/sh
explain() {
    if [ -d "$1" ]
    then
        printf "\t%s: is a directory\n" "$1"
    elif [ -e "$1" ]
    then
        printf "\t%s: is not a directory\n" "$1"
    else
        printf "\t%s: does not exist\n" "$1"
    fi
}

for item in "$@"
do
    last=
    test="$item"
    printf "testing: '%s'\n" "$item"
    while [ ! -d "$test" ]
    do
        last="$test"
        test=$(dirname "$test")
        [ -z "$test" ] && break
    done
    if [ -n "$last" ]
    then
        explain "$test"
        explain "$last"
    else
        printf "\t%s: ok\n" "$item"
    fi
done
Thomas Dickey
  • 76,765
  • It assumes the arguments are meant to be directories (or symlinks to directories). – Stéphane Chazelas Feb 04 '16 at 05:52
  • 1
    if you just cd not_a_directory your shell will just write to stderr something like cd:cd:6: no such file or directory: not_a_directory. Actually, the user's shell will do so in a format with which the user is probably already very familiar. Its almost always both easier and better in the end anyway to just do stuff and let the shell handle the reporting as necessary. That kind of philosophy though does require a very strict attention to return values and the promotion/preservation of same. – mikeserv Feb 05 '16 at 02:38
1

Just an alternative solution for bash, assuming the path is an absolute path (starts with /) :

#!/bin/bash

pathname="$1"

IFS='/' read -r -a p <<<"${pathname#/}"

pa=""    max="${#p[@]}"    i=0
while (( i<"$max" )); do
      pa="$pa/${p[i++]}"
      if     [[ ! -e $pa ]]; then
             printf 'failed at: \t"%s"\t"%s"\n' "${pa##*/}" "${pa}"
             break
      fi
done

$ ./script "/foo/ba r/baz/hello/world"
failed at:      "hello"        "/foo/ba r/baz/hello"
  • This worked for me. I needed the last valid path in the path string, so I saved pa as pa_prev as the first line of the while loop (before it gets incremented). When the test for "pa" then fails, pa_prev has the last existing directory in the given path. – Ville Feb 19 '17 at 22:16
  • This approach will fail if one of the existing folders in our path was "chmod 000 dir". But if you use "sudo namei" then it will correctly indicate that folder and will continue digging deeper. The error of this approach is an assumption that "test -e" will be able to look inside "not allowed to look" folders. This can be fixed if instead of "[[ ! -e $pa ]]" one would first: "sudo test -e ${pa}" and then check "if [ $? -ne 0 ];", then it will look deeper. To be fair: if you run "namei" w/o "sudo", then it will give you "permissin denied" on "chmod 000 dir", which is more clear/expected output. – Dmitry Shevkoplyas Nov 04 '22 at 21:24
0
(( dirct=$(echo ${dir}|tr "/" " "|wc -w)+1 ))
i=2
while [ ${i} -le ${dirct} ]
do
  sdir=$(echo ${dir}|cut -d/ -f1,${i})
  if [ ! -d ${sdir} ]
  then
    echo "Path is broken at ${sdir}"
  fi
  (( i++ ))
done

it is not simple but you can put it in a script, make it executable and stick it somewhere in your path, if you are going to use it frequently.

Caveat emptor: If your directory name at any level contains a space character, this will NOT work.

MelBurslan
  • 6,966
  • Is this bash code? I had to swap in ${dir} was coming up blank so I swapped in $1, but now sdir is coming up blank for me. – kuzzooroo Feb 04 '16 at 00:24