列出linux中的所有叶子目录

时间:2009-10-15 19:09:09

标签: bash

是否有一种简单的方法可以列出Linux中给定目录下的目录? 为了更好地解释,我可以这样做:

find mydir -type d

给出:

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

我想要的是:

mydir/src/main
mydir/bin/classes

我可以在一个循环遍历行的bash脚本中执行此操作,如果下一行包含路径,则删除前一行,但我想知道是否有一个更简单的方法不使用bash循环。

8 个答案:

答案 0 :(得分:11)

find . -type d | sort | awk '$0 !~ last "/" {print last} {last=$0} END {print last}'

答案 1 :(得分:11)

如果只想要叶子目录(不包含任何子目录的目录),请查看this other question。答案也是explains it,但简而言之就是:

find . -type d -links 2

答案 2 :(得分:5)

如果您正在寻找视觉效果,tree -d很不错。

drinks
|-- coke
|   |-- cherry
|   `-- diet
|       |-- caffeine-free
|       `-- cherry
|-- juice
|   `-- orange
|       `-- homestyle
|           `-- quart
`-- pepsi
    |-- clear
    `-- diet

答案 3 :(得分:4)

如果没有循环,我无法想到任何可以做到这一点的事情。所以,这里有一些循环:

这将显示当前目录下的叶目录,无论其深度如何:

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

此版本正确处理包含空格的目录名称:

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

以下是使用 Jefromi 建议的版本:

find -depth -type d | while read dir;  do [[ ! $prev =~ $dir ]] && echo "${dir}" ; prev="$dir"; done

答案 4 :(得分:1)

使用awk的解决方案很好,很简单……并且如果目录名包含任何在形成正则表达式模式中被认为特殊的字符,则失败。这也给Bash中的~!=测试带来了问题。

以下似乎对BSD和GNU find都适用:

find . -type d | sed 's:$:/:' | sort -r | while read -r dir;do [[ "${dir}" != "${prev:0:${#dir}}" ]] && echo "${dir}" && prev="${dir}”;done
  • find .更改为要在其中开始搜索的任何目录。
  • sed命令将斜杠添加到由以下命令返回的每个目录中 find
  • sort -r以相反的字母顺序对目录列表进行排序, 这样做的好处是列出了离目录最远的目录 首先要扎根,这就是我们想要的。
  • 然后通过while read循环逐行读取此列表, -r选项可以进一步防止某些人 字符与其他字符不同。
  • 然后,我们需要将当前行与上一行进行比较。如 我们不能使用!=测试,并且中间目录将 路径比相应的叶目录的路径短, 我们的测试会将当前行与截断的前一行进行比较 到当前行的长度。如果那是一场比赛,那么我们可以 将此行作为非叶目录丢弃,否则我们将其打印出来 行并将其设置为prev行,可用于下一次迭代。 请注意,字符串需要在测试语句中加引号, 否则可能会产生一些误报。

哦,如果您不想使用find

shopt -s nullglob globstar;printf "%s\n" **/ | sort -r | while read -r dir;do [[ "${dir}" != "${prev:0:${#dir}}" ]] && echo "${dir}" && prev="${dir}";done;shopt -u nullglob globstar

更新(2020-06-03):这是我汇总的一个脚本,希望对您有用。显然可以随时改善/适应/指出明显的问题……

#!/usr/bin/env bash

# leaf: from a given source, output only the directories
#       required ('leaf folders' ) to recreate a full
#       directory structure when passed to mkdir -p 

usage() {
    echo "Usage: ${0##*/} [-f|-g|-h|-m <target>|-s|-S|-v] <source>" 1>&2
}

# Initial variables...
dirMethod=0 # Set default method of directory listing (find -d)
addSource=0 # Set default ouput path behaviour

# Command options handling with Bash getopts builtin
while getopts ":fghm:sSv" options; do
    case "${options}" in
        f) # use depth-first find method of directory listing
            dirMethod=0 # set again here if user sets both f and g
            ;;
        g) # Use extended globbing and sort method of directory listing
            dirMethod=1
            ;;
        h) # Help text
            echo "Leaf - generate shortest list of directories to (optionally)"
            echo "       fully recreate a directory structure from a given source"
            echo 
            echo "Options"
            echo "======="
            usage
            echo
            echo "Directory listing method"
            echo "------------------------"
            echo "-f           Use find command with depth-first search [DEFAULT]"
            echo "-g           Use shell globbing method"
            echo
            echo "Output options"
            echo "--------------"
            echo "-m <target>  Create directory structure in <target> directory"
            echo "-v           Verbose output [use with -m option]"
            echo "-s           Output includes source directory"
            echo "-S           Output includes full given path of <source> directory"
            echo
            echo "Other options"
            echo "-------------"
            echo "-h           This help text"
            exit 0 # Exit script cleanly
            ;;
        m) # make directories in given location
            destinationRootDir="${OPTARG}"
            ;;
        s) # Include source directory as root of output paths/tree recreation
            addSource=1
            ;;
        S) # Include full source path as root of output paths/tree recreation
            addSource=2
            ;;
        v) # Verbose output if -m option given
            mdOpt="v"
            ;;
        *) # If no options... 
            usage
            exit 1 # Exit script with an error
            ;;
    esac
done
shift $((OPTIND-1))

# Positional parameters handling - only one (<source>) expected
if (( $# == 1 )); then
    if [[ $1 == "/" ]]; then # Test to see if <source> is the root directory /
        (( dirMethod == 0 )) && sourceDir="${1}" || sourceDir=
            # Set sourceDir to '/' if using find command dir generation or null if bash globbing method
    else
        sourceDir="${1%/}" # Strip trailing /
    fi
else
    usage  # Show usage message and...
    exit 1 # Quit with an error
fi

# Generate full pre-filtered directory list depending on requested method
if (( dirMethod == 0 )); then # find command method
    dirList=$(find "${sourceDir}" -depth -type d 2>/dev/null | sed -e 's:^/::' -e '/^$/ ! s:$:/:')
        # find command with depth-first search should eliminate need to sort directories
        # sed -e 's:^/::' -e '/^$/ ! s:$:/:' - strip leading '/' if present and add '/'
        #                                      to all directories except root
else
    shopt -s nullglob globstar dotglob
    # nullglob - don't return search string if no match
    # globstar - allow ** globbing to descend into subdirectories. '**/' returns directories only
    # dotglob  - return hidden folders (ie. those beginning with '.') 
    dirList=$(printf "%s\n" "${sourceDir}"/**/ | sed -e 's:^/::' | sort -r)
    # sort command required so filtering works correctly
fi

# Determine directory stripping string. ie. if given path/to/source[/] as the
# source directory (src), should the output be just that of the contents of src,
# src and its contents or the path/to/src and contents?
sourceDir="${sourceDir#/}"
case "${addSource}" in
    0) strip="${sourceDir}/";; # Set 'strip' to <source> 
    1) [[ "${sourceDir}" =~ (\/?.+)\/.+$ ]] && strip="${BASH_REMATCH[1]}/" || strip="/"
       # To strip down to <source> only, check to see if matched by regex and only return matched part
       # If not found, behave like -S
       ;;
    2) strip="/";; # Set 'strip' to nothing but a forward slash
esac

# Main loop
# Feed the generated dirList into this while loop which is run line-by-line (ie. directory by directory)
while read -r dir;do
    if [[ "${dir}" != "${prev:0:${#dir}}" ]]; then
        # If current line is not contained within the previous line then that is a valid directory to display/create 
        if [[ -v destinationRootDir ]]; then # If destinationRootDir is set (-m) then create directory in <target>
            mkdir -p${mdOpt} "${destinationRootDir%/}/${dir#$strip}"
            # -p - create intermediate directories if they don't exist. The filtered list means no unnecessary mkdir calls
            # if mdOpt is set, it is 'v', meaning mkdir will output each created directory path to stdin
            # ${dir#$strip} removes the set strip value from the line before it is displayed/created
        else
            echo "${dir#$strip}" # Same as above but no directories created. Displayed only, so -v ignored here
        fi
        prev="${dir}" # Set prev to this line before the loop iterates again and the next line passed to dir
    fi
done <<<"${dirList}" # This is a here string

答案 5 :(得分:0)

这仍然是一个循环,因为它使用sed中的分支命令:

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

基于info sed(uniq work-working)中的脚本。

修改以下是带有评论的sed脚本(从info sed复制并修改):

# 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

以下是正则表达式的作用:

  • / - 启动模式
  • ^ - 匹配行的开头
  • \( - 启动捕获组(后引用子表达式)
  • .* - 零个或多个(*)任何字符(。)
  • \) - 结束捕获组
  • \/ - 斜杠(/)(使用\进行转义)
  • .* - 零个或多个任何字符
  • \n - 换行符
  • \1 - 后引用的副本(在本例中是行的开头和最后一个斜杠之间的任何内容)
  • $ - 匹配行尾
  • / - 结束模式

答案 6 :(得分:0)

我认为您可以查看所有目录,然后重定向输出并使用xargs计算每个子目录的数字文件,当没有子目录时(xargs找到SUBDIR -type d | wc -l ...类似的东西,我现在无法测试)你找到了一片叶子。

这仍然是一个循环。

答案 7 :(得分:0)

尝试以下单行程序(在Linux和OS X上测试):

find . -type d -execdir sh -c 'test -z "$(find "{}" -mindepth 1 -type d)" && echo $PWD/{}' \;