Search code examples
shellunixtestingdirectoryposix

Test -d directory true - subdirectory false (POSIX)


I'm trying to print all directories/subdirectories from a given start directory.

for i in $(ls -A -R -p); do 
    if [ -d "$i" ]; then
            printf "%s/%s \n" "$PWD" "$i"
    fi
done; 

This script returns all of the directories found in the . directory and all of the files in that directory, but for some reason the test fails for subdirectories. All of the directories end up in $i and the output looks exactly the same.
Let's say I have the following structure:

foo/bar/test

echo $i prints

foo/
bar/
test/

While the contents of the folders are listed like this:

./foo:
file1
file2
./bar:
file1
file2

However the test statement just prints:

PWD/TO/THIS/DIRECTORY/foo

For some reason it returns true for the first level directories, but false for all of the subdirectories.

(ls is probably not a good way of doing this and I would be glad for a find statement that solves all of my issues, but first I want to know why this script doesn't work the way you'd think.)


Solution

  • Consider your output:

    dir1:
    dir1a
    

    Now, the following will be true:

    [ -d dir1/dir1a ]
    

    but that's not what your code does; instead, it runs:

    [ -d dir1a ]
    

    To avoid this, don't attempt to parse ls; if you want to implement recursion in baseline POSIX sh, do it yourself:

    callForEachEntry() {
      # because calling this without any command provided would try to execute all found files
      # as commands, checking for safe/correct invocation is essential.
      if [ "$#" -lt 2 ]; then
        echo "Usage: callForEachEntry starting-directory command-name [arg1 arg2...]" >&2
        echo "  ...calls command-name once for each file recursively found" >&2
        return 1
      fi
      # try to declare variables local, swallow/hide error messages if this fails; code is
      # defensively written to avoid breaking if recursing changes either, but may be faulty if
      # the command passed as an argument modifies "dir" or "entry" variables.
      local dir entry 2>/dev/null ||: "not strict POSIX, but available in dash"
      dir=$1; shift
      for entry in "$dir"/*; do
        # skip if the glob matched nothing
        [ -e "$entry" ] || [ -L "$entry" ] || continue
        # invoke user-provided callback for the entry we found
        "$@" "$entry"
        # recurse last for if on a baseline platform where the "local" above failed.
        if [ -d "$entry" ]; then
          callForEachEntry "$entry" "$@"
        fi
      done
    }
    
    # call printf '%s\n' for each file we recursively find; replace this with the code you
    # actually want to call, wrapped in a function if appropriate.
    callForEachEntry "$PWD" printf '%s\n'
    

    find can also be used safely, but not as a drop-in replacement for the way ls was used in the original code -- for dir in $(find . -type d) is just as buggy. Instead, see the "Complex Actions" and "Actions In Bulk" section of Using Find.