tags:

views:

257

answers:

6

Is there an easy way to list only directories under a given directory in Linux? To explain better, I can do:

find mydir -type d

which gives:

mydir/src
mydir/src/main
mydir/bin
mydir/bin/classes

What I want instead is:

mydir/src/main
mydir/bin/classes

I can do this in a bash script that loops over the lines and removes previous line if next line contains the path, but I'm wondering if there is a simpler method that does not use bash loops.

A: 

ls -d mydir/*/*/.

DigitalRoss
+4  A: 

I can't think of anything that will do this without a loop. So, here are some loops:

This displays the leaf directories under the current directory, regardless of their depth:

for dir in $(find -depth -type d); do [[ ! $prev =~ $dir ]] && echo "$dir" ; prev="$dir"; done

This version properly handles directory names containing spaces:

saveIFS=$IFS; IFS=$'\n'; for dir in $(find -depth -type d ); do [[ ! $prev =~ $dir ]] && echo "${dir}" ; prev="$dir"; done; IFS=$saveIFS

Here is a version using Jefromi's suggestion:

find -depth -type d | while read dir;  do [[ ! $prev =~ $dir ]] && echo "${dir}" ; prev="$dir"; done
Dennis Williamson
The OP was asking if there was a simpler way than looping over the output, not how to write the loop. That said, it's also a good idea to use `find .... | while read dir` instead of `for dir in $(...)`, because it doesn't have to do the entire find before printing anything.
Jefromi
@Jefromi: Piping `find` into `while` also handles names with spaces properly for free.
Dennis Williamson
Well, avoiding loops is obviously not a hard "requirement", but I was hoping there would be a simpler+intuitive way to do it without loops. Thanks for the find | while info though.
amol
@Dennis: Indeed! Is there anything `<cmd> | while read` can't do?
Jefromi
+1  A: 
find . -type d | sort | awk '$0 !~ last {print last} {last=$0} END {print last}'
Brian
Thanks, just the kind of solution I was looking for :)
amol
This will produce correct, but unsorted, results without the `sort` (so you could put it at the end, instead, if you want).
Dennis Williamson
A: 

This is still a loop, since it uses the branch command in sed:

find -depth -type d |sed 'h; :b; $b; N; /^\(.*\)\/.*\n\1$/ { g; bb }; $ {x; b}; P; D'

Based on a script in info sed (uniq work-alike).

Edit Here is the sed script broken out with comments (copied from info sed and modified):

# copy the pattern space to the hold space
h 

# label for branch (goto) command
:b
# on the last line ($) goto the end of 
# the script (b with no label), print and exit
$b
# append the next line to the pattern space (it now contains line1\nline2
N
# if the pattern space matches line1 with the last slash and whatever comes after
# it followed by a newline followed by a copy of the part before the last slash
# in other words line2 is different from line one with the last dir removed
# see below for the regex
/^\(.*\)\/.*\n\1$/ {
    # Undo the effect of
    # the n command by copying the hold space back to the pattern space
    g
    # branch to label b (so now line2 is playing the role of line1
    bb
}
# If the `N' command had added the last line, print and exit
# (if this is the last line then swap the hold space and pattern space
# and goto the end (b without a label) 
$ { x; b }

# The lines are different; print the first and go
# back working on the second.
# print up to the first newline of the pattern space
P
# delete up to the first newline in the pattern space, the remainder, if any,
# will become line1, go to the top of the loop
D

Here is what the regex is doing:

  • / - start a pattern
  • ^ - matches the beginning of the line
  • \( - start a capture group (back reference subexpression)
  • .* - zero or more (*) of any character (.)
  • \) - end capture group
  • \/ - a slash (/) (escaped with \)
  • .* - zero or more of any character
  • \n - a newline
  • \1 - a copy of the back reference (which in this case is whatever was between the beginning of the line and the last slash)
  • $ - matches the end of the line
  • / - end the pattern
Dennis Williamson
Your suggestion works just great, but I find it hard to understand. Thanks though, I will try to figure out what the sed options mean.
amol
A: 

I think you can look at all the directories and then redirect the ouput and use xargs for counting the number files for each subdirectories, when there's no subdirectory ( xargs find SUBDIR -type d | wc -l ... something like that, i cannot test right now ) you've found a leaf.

This is still a loop though.

LB
Ah.. maybe I did not explain what "leaf" meant quite correctly. I meant "leaf dirs", so if all a dir had were files, it would still qualify as a "leaf" for my problem.
amol
ouhla, i'm tired, i you look for directories under and wc -l == 0, that should the trick...
LB
A: 

If you're looking for something visual, tree -d is nice.

drinks
|-- coke
|   |-- cherry
|   `-- diet
|       |-- caffeine-free
|       `-- cherry
|-- juice
|   `-- orange
|       `-- homestyle
|           `-- quart
`-- pepsi
    |-- clear
    `-- diet
Dennis Williamson
Thanks, this is useful as well (not for my current problem, but good to know)I'm wondering, is tree a recent addition? I recall needing something like this (was on RHEL at the time, now on Ubuntu) about a year back and ended up using the script at http://centerkey.com/tree instead.
amol