Here's a POSIX-compliant solution that postprocesses the output of find
to remove directories that have a listed subdirectory. It assumes that there are no newlines in the directory names.
{ find . -type d; echo; } |
awk 'index($0,prev"/")!=1 && NR!=1 {print prev}
1 {sub(/\/$/,""); prev=$0}'
Explanation: the awk script delays the printing of each line until it's read the next line and only prints the previous line if it's not a prefix. This takes advantage of the fact that find
lists subdirectories immediately after their parent. The extra "/"
is to avoid spuriously removing foo
when foobar
also exists. The inelegant NR!=1
avoids printing an initial empty line, and the inelegant echo;
is not to have an equally inelegant special case for the last line. The call to sub
removes a trailing slash from the toplevel directory, in case e.g. find ./
was called.
As usual there's a cryptic zsh one-liner.
echo **/.(e\''test -z $REPLY/*(/DN[1])'\':h)
Longer, more readable version:
is_leaf () { [ -z $REPLY/*(/DN[1]) ] }
echo **/.(+is_leaf:h)
The last line can be simplified to echo **/(+is_leaf)
if you dont' mind the trailing /
.
Summary explanation: The stuff in parentheses are glob qualifiers, documented in the zshexpn
man page. We filter the results of the glob **/
(expanding to the current directory and all its subdirectories), keeping only those for which the function is_leaf
(or the code between '…'
) returns 0. The filter code globs the subdirectories of the match being tested ($REPLY
) (in fact, [1]
makes it stops after the first subdirectory) and returns a status indicating whether at least one subdirectory was found. The glob qualifier /
restricts the expansion to directories; N
means the expansion is empty if there is no match; D
causes dot files to be included; :h
is a history modifier and causes the /.
suffix to be stripped (in general it means dirname
).
Just to illustrate the possibilities of zsh's glob qualifiers, here are two other variants (longer and I think more obscure) with a corresponding is_leaf
function:
echo **/.(e\''tmp=($REPLY/*(/DN[1])); ((!#tmp))'\':h)
echo **/.(e\''$REPLY/*(/DN[1]e:REPLY=false:)'\':h)
is_leaf () { set -- $REPLY/*(/DN[1]); ((!#)); }
is_leaf () { return $REPLY/*(/DN[1]e:REPLY=1:) }