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