Home > Net >  Differentiation on whether directory exists and permission error
Differentiation on whether directory exists and permission error

Time:03-23

Looking for a very simple way to check whether a file/directory exists while evaluating user permissions, returning different (code) errors:

There is command test that checks for permissions but fails to provide a better return code for case where file does not exist:

$ test -r /tmp/; echo $?           # 0
$ test -r /tmp/root/; echo $?      # 1
$ test -r /tmp/missing/; echo $?   # 1

I am looking for something similar to ls where I get a different message for different errors:

$ ls /tmp/root
ls: root: Permission denied
$ ls /tmp/missing
ls: /tmp/missing: No such file or directory

I like the differentiation but the error code is 1 in both. To properly handle each error, I have to parse stderr which is honestly a very inelegant solution.

Isn't there a better and graceful way of doing this?


Something close to a pythonic way looks something like this:

import os

os.listdir("/tmp/root/dir/")  # raises PermissionError
os.listdir("/tmp/foo/")       # raises FileNotFoundError

CodePudding user response:

Read the manual some more. There's also -d to specifically check whether the target is a directory, and a slew of other predicates to check for symlinks, device nodes, etc.

testthing () {
    if ! [[ -e "$1" ]]; then
        echo "$1: not found" >&2
        return 2
    elif ! [[ -d "$1" ]]; then
        echo "$1: not a directory" >&2
        return 4
    elif ! [[ -r "$1" ]]; then
        echo "$1: permission denied" >&2
        return 8
    fi
    return 0
}

Usage:

testthing "/root/no/such/directory"

Notice that [[ is a Bash built-in which is somewhat more robust and versatile than the legacy [ aka test.

It's hard to predict what the priorities should be, but if you want the comparisons in a different order, by all means go for it. It is unavoidable that the shell cannot correctly tell the precise status of a directory entry when it lacks read access to the parent directory. Maybe solve this from the caller by examining the existence and permissions of every entry in the path, starting from the root directory.

CodePudding user response:

The shell and standard utilities do not provide a command that does everything you seem to want:

  • with a single command execution,
  • terminate with an exit status that reports in detail on the existence and accessability of a given path,
  • contextualized for the current user,
  • accurately even in the event that a directory prior to the last path element is untraversable (note: you cannot have this one no matter what),
  • (maybe) correctly for both directories and regular files.

The Python os.listdir() doesn't do all of that either, even if you exclude the applicability to regular files and traversing untraversible directories, and reinterpret what "exit status" means. However, os.listdir() and ls both do demonstrate a good and useful pattern: to attempt a desired operation and deal with any failure that results, instead of trying to predict what would happen if you tried it.

Moreover, it's unclear why you want what you say you want. The main reason I can think of for wanting information about the reason for a file-access failure is user messaging, but in most cases you get that for free by just trying to perform the wanted access. If you take that out of the picture, then it rarely matters why a given path is inaccessible. Any way around, you need to either switch to an alternative or fail.

If you nevertheless really do want something that does as much as possible of the above, then you probably will have to roll your own. Since you expressed concern for efficiency in some of your comments, you'll probably want to do that in C.

CodePudding user response:

An example of use.

for d in 1 2 3; do
    if [[ -e $d ]]; then printf  "%s exists" $d
       [[ -r $d ]] && echo " and is readable" || echo " but is not readable"
    else echo "$d does not exist"
    fi
    stat -c "%A %n" $d
done
1 exists and is readable
drwxr-xr-x 1
2 exists but is not readable
d--------- 2
3 does not exist
stat: cannot stat ‘3’: No such file or directory

If you absolutely have to have it in one step with differentiated exit codes, write a function.

doubletest() { if [[ -e "$1" ]]; then [[ -r "$1" ]] && return 0 || return 2; else return 1; fi; }
result=( "exists and is readable" "exists but is unreadable" "does not exist" )
for d in 1 2 3; do doubletest $d; echo "$d ${result[$?]}"; done
1 exists and is readable
2 does not exist
3 exists but is unreadable

CodePudding user response:

Given:

$ ls -l
total 0
-rw-r--r--  1 andrew  wheel   0 Mar 22 12:01 can_read
---xr-x--x  1 andrew  wheel   0 Mar 22 12:01 root
drwxr-xr-x  2 andrew  wheel  64 Mar 22 13:09 sub

Note that permissions are by user for the first three, group for the second three and other or world for the last three.

Permission Denied error is from 1) Trying to read or write a file without that appropriate permission bit set for your user or group or 2) Tying to navigate to a directory without x set or 3) Trying to execute a file without appropriate permission.

You can test if a file is readable or not for the user with the -r test:

$ [[ -r root ]] && echo 'readable' || echo 'not readable'
not readable

So if you only are concerned with user permissions, -r, -w and -x test are what you are looking for.

If you want to test permissions generally, you need to use stat.

Here is a simple example with that same directory:

#!/bin/bash

arr=(can_read root sub missing)

for fn in "${arr[@]}"; do
    if [[ -e "$fn" ]]
    then
        p=( $(stat -f "%SHp %SMp %SLp" "$fn") )
        printf "File:\t%s\nUser:\t%s\nGroup:\t%s\nWorld:\t%s\nType:\t%s\n\n" "$fn" "${p[@]}" "$(stat -f "%HT" "$fn")"
    else    
        echo "\"$fn\" does not exist" 
    fi  
done    

Prints:

File:   can_read
User:   rw-
Group:  r--
World:  r--
Type:   Regular File

File:   root
User:   --x
Group:  r-x
World:  --x
Type:   Regular File

File:   sub
User:   rwx
Group:  r-x
World:  r-x
Type:   Directory

"missing" does not exist

Alternatively, you can grab these values directly from the drwxr-xr-x type data with:

for fn in "${arr[@]}"; do
    if [[ -e "$fn" ]]
    then
        p=$(stat -f "%Sp" "$fn")
        typ="${p:0:1}"
        user="${p:1:3}"
        grp="${p:4:3}"
        wrld="${p:7:3}"
    else    
        echo "\"$fn\" does not exist" 
    fi  
done    

In either case, you can then test the individual permissions with either Bash string functions, Bash regex, or get the octal equivalents and use bit masks.

Here is an example:

for fn in "${arr[@]}"; do
    if [[ -e "$fn" ]]
    then
        p=$(stat -f "%Sp" "$fn")
        user="${p:1:3}"
        ty="$(stat -f "%HT" "$fn")"
        printf "%s \"$fn\" is:\n" "$ty" 
        [[ $user =~ 'r' ]] && echo "    readable" || echo "    not readable"
        [[ $user =~ 'w' ]] && echo "    writeable" || echo "    not writeable"
        [[ $user =~ 'x' ]] && echo "    executable" || echo "    not executable"
    else    
        echo "\"$fn\" does not exist" 
    fi  
done    

Prints:

Regular File "can_read" is:
    readable
    writeable
    not executable
Regular File "root" is:
    not readable
    not writeable
    executable
Directory "sub" is:
    readable
    writeable
    executable
"missing" does not exist

(Note: stat tends to be platform specific. This is BSD and Linux will have different format strings...)

  • Related