Список всех листовых подкаталогов в linux

Есть ли простой способ перечислить только каталоги в данном каталоге в Linux? Чтобы объяснить лучше, я могу сделать:

find mydir -type d

который дает:

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

Вместо этого я хочу:

mydir/src/main
mydir/bin/classes

Я могу сделать это в скрипте bash, который перебирает строки и удаляет предыдущую строку, если следующая строка содержит путь, но мне интересно, есть ли более простой метод, который не использует циклы bash.

9 ответов

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

Если вам нужны только листовые каталоги (каталоги, которые не содержат подкаталогов), посмотрите на этот другой вопрос. Ответ также объясняет это, но вкратце это так:

find . -type d -links 2

Если вы ищете что-то визуальное, tree -d это мило.

напитки |- кокс |   |- вишня |   `- диета |   |- без кофеина |   `- вишня |- сок |   `- оранжевый |   `- домашний стиль |   `- кварта` - пепси |- очистить` - диета

Я не могу думать ни о чем, что сделает это без петли. Итак, вот несколько петель:

Это отображает листовые каталоги под текущим каталогом, независимо от их глубины:

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

Вот версия, использующая предложение Джефроми:

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

На большинстве файловых систем (не btrfs) простой ответ:

      find . -type d -links 2

В https://unix.stackexchange.com/questions/497185/how-to-find-only-directories-without-subdirectories есть решение, которое работает на btrfs, но невыносимо уродливо:

      find . -type d \
    \( -exec sh -c 'find "$1" -mindepth 1 -maxdepth 1 -type d -print0 | grep -cz "^" >/dev/null 2>&1' _ {} \; -o -print \)

Есть альтернатива, которую можно найти под названием сыромятная кожа (rh), которая делает это намного проще:

      rh 'd && "[ `rh -red %S | wc -l` = 0 ]".sh'

Немного более короткая/быстрая версия:

      rh 'd && "[ -z \"`rh -red %S`\" ]".sh'

Вышеупомянутые команды ищут каталоги, а затем перечисляют их подкаталоги и сопоставляются только тогда, когда их нет (первый путем подсчета количества строк вывода, а второй путем проверки наличия каких-либо выходных данных для каждого каталога).

Если вам не нужна поддержка btrfs, это больше похоже на find, но короче:

      rh 'd && nlink == 2'

Для версии, которая максимально эффективно работает на всех файловых системах:

      rh 'd && (nlink == 2 || nlink == 1 && "[ -z \"`rh -red %S`\" ]".sh)'

В обычных (не btrfs) файловых системах это будет работать без необходимости каких-либо дополнительных процессов для каждого каталога, но в btrfs они потребуются. Это, вероятно, лучше всего, если у вас есть смесь разных файловых систем, включая btrfs.

Сыромятная кожа (rh) доступна на https://raf.org/rawhide или https://github.com/raforg/rawhide. Он работает как минимум на Linux, FreeBSD, OpenBSD, NetBSD, Solaris, macOS и Cygwin.

Отказ от ответственности: я являюсь текущим автором сыромятной кожи

Решение с использованием 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 опция дополнительно защищает от отношения к одним персонажам иначе, чем к другим.
  • Затем нам нужно сравнить текущую строку с предыдущей. Поскольку мы не можем использовать!=test и что промежуточные каталоги будут иметь путь короче, чем путь к соответствующему конечному каталогу, наш тест будет сравнивать текущую строку с предыдущей строкой, усеченной до длины текущей строки. Если это совпадение, то мы можем отбросить эту строку как не листовой каталог, в противном случае мы печатаем эту строку и устанавливаем ее как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

Попробуйте следующую однострочную версию (протестировано на Linux и OS X):

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

Я думаю, что вы можете посмотреть на все каталоги, а затем перенаправить вывод и использовать xargs для подсчета числовых файлов для каждого подкаталога, когда нет подкаталога ( xargs находит SUBDIR -типа d | wc -l ... что-то в этом роде, я не могу проверить прямо сейчас) вы нашли лист.

Это все еще цикл, хотя.

Это все еще цикл, так как он использует команду перехода в sed:

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

На основе сценария в info sed (уникальный труд).

Изменить вот 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 - копия обратной ссылки (которая в данном случае совпадает с началом строки и последней косой чертой)
  • $ - соответствует концу строки
  • / - закончить шаблон
Другие вопросы по тегам