How do I recursively list all directories at a location, breadth-first?

72,714

Solution 1

The find command supports -printf option which recognizes a lot of placeholders.

One such placeholder is %d which renders the depth of given path, relative to where find started.

Therefore you can use following simple one-liner:

find -type d -printf '%d\t%P\n' | sort -r -nk1 | cut -f2-

It is quite straightforward, and does not depend on heavy tooling like perl.

How it works:

  • it internally generates list of files, each rendered as a two-field line
  • the first field contains the depth, which is used for (reverse) numerical sorting, and then cut away
  • resulting is simple file listing, one file per line, in the deepest-first order

Solution 2

If you want to do it using standard tools, the following pipeline should work:

find . -type d | perl -lne 'print tr:/::, " $_"' | sort -n | cut -d' ' -f2

That is,

  1. find and print all the directories here in depth first order
  2. count the number of slashes in each directory and prepend it to the path
  3. sort by depth (i.e., number of slashes)
  4. extract just the path.

To limit the depth found, add the -maxdepth argument to the find command.

If you want the directories listed in the same order that find output them, use "sort -n -s" instead of "sort -n"; the "-s" flag stabilizes the sort (i.e., preserves input order among items that compare equally).

Solution 3

You can use find command, find /path/to/dir -type d So below example list of directories in current directory :

find . -type d

Solution 4

My feeling is that this is a better solution than previously mentioned ones. It involves grep and such and a loop, but I find it works very well, specifically for cases where you want things line buffered and not the full find buffered.

It is more resource intensive because of:

  • Lots of forking
  • Lots of finds
  • Each directory before the current depth is hit by find as many times as there is total depth to the file structure (this shouldn't be a problem if you have practically any amount of ram...)

This is good because:

  • It uses bash and basic gnu tools
  • It can be broken whenever you want (like you see what you were looking for fly by)
  • It works per line and not per find, so subsequent commands don't have to wait for a find and a sort
  • It works based on the actual file system separation, so if you have a directory with a slash in it, it won't be listed deeper than it is; if you have a different path separator configured, you still are fine.
#!/bin/bash 
depth=0

while find -mindepth $depth -maxdepth $depth | grep '.'
do
    depth=$((depth + 1))
done

You can also fit it onto one line fairly(?) easily:

depth=0; while find -mindepth $depth -maxdepth $depth | grep --color=never '.'; do depth=$((depth + 1)); done

But I prefer small scripts over typing...

Solution 5

I don't think you could do it using built-in utilities, since when traversing a directory hierarchy you almost always want a depth-first search, either top-down or bottom-up. Here's a Python script that will give you a breadth-first search:

import os, sys

rootdir = sys.argv[1]
queue = [rootdir]

while queue:
    file = queue.pop(0)
    print(file)
    if os.path.isdir(file):
        queue.extend(os.path.join(file,x) for x in os.listdir(file))

Edit:

  1. Using os.path-module instead of os.stat-function and stat-module.
  2. Using list.pop and list.extend instead of del and += operators.
Share:
72,714
Andrey Fedorov
Author by

Andrey Fedorov

Updated on December 24, 2020

Comments

  • Andrey Fedorov
    Andrey Fedorov over 3 years

    Breadth-first list is important, here. Also, limiting the depth searched would be nice.

    $ find . -type d
    /foo
    /foo/subfoo
    /foo/subfoo/subsub
    /foo/subfoo/subsub/subsubsub
    /bar
    /bar/subbar
    
    $ find . -type d -depth
    /foo/subfoo/subsub/subsubsub
    /foo/subfoo/subsub
    /foo/subfoo
    /foo
    /bar/subbar
    /bar
    
    $ < what goes here? >
    /foo
    /bar
    /foo/subfoo
    /bar/subbar
    /foo/subfoo/subsub
    /foo/subfoo/subsub/subsubsub
    

    I'd like to do this using a bash one-liner, if possible. If there were a javascript-shell, I'd imagine something like

    bash("find . -type d").sort( function (x) x.findall(/\//g).length; )