Вопрос или проблема
Что я имею в виду, это предположим, у нас есть следующий каталог
Dropbox (папка)
—> Bob (папка)
——-> 2017 (папка)
————> images (папка)
—————-> image.png (файл)
Я мог бы сделать
cd Dropbox
, но тогда мне придется вручную переходить до самого глубокого каталога images
.
Существует ли команда типа
cd Dropbox:deepest directory
, которая сразу перенесет меня в Dropbox/Bob/2017/images
?
Если на каком-то уровне возникает совпадение, то остановитесь на этом уровне
С zsh
:
bydepth() REPLY=${REPLY//[^\/]}
cd Dropbox/**/*(D/O+bydepth[1])
Мы определяем функцию сортировки bydepth
, которая возвращает файл с удаленными символами, отличными от /
(так, чтобы порядок после этой трансформации был по глубине), и используем рекурсивное глоббинг (**/
обозначает любой уровень подкаталогов) с квалификаторами для глоббинга:
D
для учета скрытых каталогов/
только для каталоговO+bydepth
: обратная сортировка по глубине[1]
берём только первый (после сортировки).
С bash
и инструментами GNU эквивалент будет выглядеть примерно так:
IFS= read -rd '' deepest < <(find Dropbox/ -type d -print0 |
awk -v RS='\0' -v ORS='\0' -F / '
NF > max {max = NF; deepest = $0}
END {if (max) print deepest}') && cd -- "$deepest"
(в случае совпадений выбранный каталог может не совпадать с подходом zsh
).
С вашим новым дополнительным требованием это становится сложнее. В принципе, если я правильно понимаю, в случае совпадений необходимо перейти в каталог, являющийся самым глубоким общим родителем всех этих каталогов на максимальной глубине. С zsh
:
cd_deepest() {
setopt localoptions rematchpcre
local REPLY dirs result dir match
dirs=(${1:-.}/**/*(ND/nOne:'
REPLY=${#REPLY//[^\/]}-$REPLY':))
(($#dirs)) || return
result=$dirs[1]
for dir ($dirs[2,-1]) {
[[ $result//$dir =~ '^([^-]*-.*)/.*//\1/' ]] || break
result=$match[1]
}
cd -- ${result#*-} && print -rD -- $PWD
}
Пример:
$ tree Dropbox
Dropbox
├── a
│ └── b
│ ├── 1
│ │ └── x
│ └── 2
│ └── x
└── c
└── d
└── e
9 directories, 0 files
$ cd_deepest Dropbox
~/Dropbox/a/b
(Dropbox/a/b/1/x
и Dropbox/a/b/2/x
являются самыми глубокими, и мы переходим в их общего родителя (Dropbox/a/b
)).
find . -type d -print0 | while IFS= read -r -d $'\0' line; do echo -n $(echo "$line" | grep -o "https://unix.stackexchange.com/" | wc -l); echo " $line"; done | sort | tail -1 | cut -d' ' -f2-
Тестировалось на macOS (bash) и Arch Linux (zsh и bash).
find . -type d
используется для поиска всех каталогов в текущем пути.-print0
в сочетании сread
используется для обработки вывода из find, также и для каталогов, которые могут содержать пробелы.grep -o
используется для извлечения слэшей из путей.wc -l
используется для подсчета количества слэшей.sort
иtail
используются для извлечения пути, содержащего наибольшее количество слэшей.cut
используется для исключения количества слэшей и отображения только пути к самому глубокому каталогу.
Вот версия, ориентированная на bash; она опирается на следующие функции bash:
- опция оболочки globstar, чтобы включить расширение каталогов и подкаталогов с
**/
- читать, поддерживает массивы, чтобы подсчитать глубины каталогов
cdd() {
local _cdd_unset_globstar=0
shopt -q globstar || _cdd_unset_globstar=1
shopt -s globstar
local _cdd_deepest=$1
local _cdd_level=1
local _cdd_array=()
for d in "${1}/"**/
do
IFS=/ read -r -d '' -a _cdd_array <<< "$d" || true
if [ "${#_cdd_array[*]}" -gt "$_cdd_level" ]
then
_cdd_deepest=$d
_cdd_level=${#_cdd_array[*]}
fi
done
cd -- "$_cdd_deepest" && true
local _cdd_ret="$?"
[ "$_cdd_unset_globstar" -eq 1 ] && shopt -u globstar
return "$_cdd_ret"
}
Функция выполняет следующие действия:
- проверяет, установлена ли уже опция оболочки globstar; если нет, то мы записываем флаг, чтобы сбросить опцию в конце.
- инициализирует текущий и самый глубокий известный каталог и его уровень (
$1
и1
соответственно). - Расширяет каждый подкаталог под данным параметром и проходит по ним.
- Для каждого подкаталога читает его в массив, разделенный по
/
; подсчитывает количество элементов в массиве и сравнивает его с текущей известной глубиной самого глубокого каталога. Если он глубже, сбрасывает эти переменные. - Когда мы находим самый глубокий подкаталог, выполняем
cd
на него. - Если мы должны сбросить опцию оболочки globstar, делаем это.
Если вам кажется, что использование подоболочки для установки опций оболочки будет чище, вы можете подойти к этому с помощью двух функций: обёртки и функции подсhell, выполняющей вышеописанное:
cdd_helper() (
shopt -s globstar
_cdd_deepest=$1
_cdd_level=1
for d in "${1}/"**/
do
IFS=/ read -r -d '' -a _cdd_array <<< "$d" || true
if [ "${#_cdd_array[*]}" -gt "$_cdd_level" ]
then
_cdd_deepest=$d
_cdd_level=${#_cdd_array[*]}
fi
done
printf "%s" "$_cdd_deepest"
)
cdd() {
cd -- "$(cdd_helper "$1")"
}
Альтернативное решение с использованием find
и sort
для определения самых глубоких каталогов:
$ pwd
/home/gv/Desktop/PythonTests
$ find $PWD -type d -printf '%d:%p\0' |sort -z -t: -r |awk -F: -v RS='\0' -v ORS='\n' 'NR<=3' #Вывести 3 самых глубоких каталога
5:/home/gv/Desktop/PythonTests/tmp/tmp2/tmp3/tmp4/tmp 5 #обратите внимание на пробел в последнем имени каталога
5:/home/gv/Desktop/PythonTests/.git/logs/refs/remotes/origin
4:/home/gv/Desktop/PythonTests/tmp/tmp2/tmp3/tmp4
С опцией -printf
у команды find
вы можете выбирать, что выводить:
%d
: Глубина каталога относительно текущего рабочего каталога
%p
: Имя результата = имя каталога
\0
: разделитель nul
Вы можете перейти в первый самый глубокий каталог следующим образом:
$ cd "$(find $PWD -type d -printf '%d:%p\0' |sort -z -t: -r |awk -F: -v RS='\0' -v ORS='\n' 'NR==1{print $2}')"
$ pwd
/home/gv/Desktop/PythonTests/tmp/tmp2/tmp3/tmp4/tmp 5
Чтобы не запоминать всё это, вы можете закрепить функцию в своем файле алиасов, чтобы она загружалась с вашим профилем bash:
$ deepest () { cd "$(find $PWD -type d -printf '%d:%p\0' |sort -z -t: -r |awk -F: -v RS='\0' -v ORS='\n' 'NR==1{print $2}')"; }
$ pwd
/home/gv/Desktop/PythonTests
$ deepest
$ pwd
/home/gv/Desktop/PythonTests/tmp/tmp2/tmp3/tmp4/tmp 5
Первый способ – рекурсия, на мой взгляд лучший.
Использование: загрузите функцию rcd
в bash source recur_cd.sh
, затем протестируйте ее: rcd some_dir
.
rcd () {
if [ ! -d "$*" ]; then
return 121
else
rcd "${*}"*/
fi
if (($? == 121)); then
cd "$*"
fi
}
Второй способ – команда find (два варианта здесь).
Использование: Сначала загрузите две внутренние функции (dcd1
и dcd2
) в bash
, с помощью этой команды: source deep_cd.sh
. Функции делают одно и то же, но реализованы по-разному.
Затем, протестируйте это:
dcd1 dir # первый вариант
pwd
cd - # вернуться назад для дальнейшего тестирования
dcd2 dir # второй вариант
pwd
# и так далее.
deep_cd.sh
#!/bin/bash
# первый вариант, поддерживает имена файлов с пробелами, работает в основном через awk
dcd1 () {
cd "$(find "$1" -type d -printf "%d %p\n" | sort | awk '{
a[$1]++;
tmp_num=$1;
sub("^[0-9]* ", "", $0);
path = $0;
if (a[tmp_num] == 2) { sub("[^/]*$", ""); path = $0; exit; }
}
END { print path; }')"
}
# второй вариант, поддерживает имена файлов с пробелами - используется больше программ
# (cut, uniq, tail), awk только выводит путь, а не ищет его
dcd2 () {
str=$(find "$1" -type d -printf "%d %p\n" | sort)
num=$(cut -d " " -f 1 <<< "$str" | uniq -u | tail -n 1)
path=$(awk -v n=$num 'NR == n+1 { $1=""; print $0; }' <<< "$str")
cd "${path# }"
}
Третий способ – использование макросов readline.
bind '"\e\C-i":"\C-i\C-i\C-i\C-i"'
И все 🙂
Объяснение:
bind
– прочитайтеhelp bind
. Это функция readline для привязки клавиш.\e\C-i
– Ctlr + Alt + i – новая соединенная комбинация, когда она будет нажата, она выполнит серию автодополнений до тех пор, пока не появятся два или несколько каталогов.\C-i
то же, что и Tab, означающее ‘дополнить’. Вам нужно столько\C-i
, сколько предполагается уровень глубины каталога. Если предполагается 10 уровней, вам нужно 10 блоков\C-i
. Хотя, возможно, будет удобнее выбрать среднее количество блоков, скажем, 3 или 4, и просто нажимать комбинациюAlt + Ctrl + i
дважды, где это необходимо. Это предотвратит повторение вывода (см. комментарии в разделе “Тестирование”)
Если вы хотите сделать такое поведение постоянным, добавьте эту строку в .bashrc
Тестирование
cd o # затем Alt + Ctrl + i
cd one/two/three/four/ # каталог был изменен на самый глубокий
cd A # затем Alt + Ctrl + i
cd A/B/C/ # каталог был изменен на самый глубокий
D/ E/ # и появились два внутренних каталога
~/deepest_dir$ cd A/B/C/ # одно препятствие здесь - если
D/ E/ # достигнут самый глубокий каталог,
~/deepest_dir$ cd A/B/C/ # автодополнение работает вхолостую
# и дублирует вывод, так что возможно
# стоит уменьшить количество блоков
# 'C-i' в строке привязки
Недавно у меня была эта же проблема с приложением, которое создало вложенный ряд из сотен подкаталогов со структурой .trash/<uuid4-32-chars>/.trash/<uuid4-32-chars>/.../.trash/<uuid4-32-chars>
.
Мне удалось перейти в самый глубокий каталог с помощью этой команды bash
:
find_child_dir="find ./ -mindepth 1 -maxdepth 1 -type d"; while [[ "$(eval "$find_child_dir" | wc -l)" -gt 0 ]]; do cd "$(eval "$find_child_dir" | head -n 1)"; pwd; done;
Объяснение:
find ./ -mindepth 1 -maxdepth 1 -type d
находит прямых потомков текущего каталога.-mindepth 1 -maxdepth 1
обеспечивает исключение.
и..
из результатов../
обеспечивает включение скрытых подкаталогов в результаты.find_child_dir="..."
сохраняет команду find как строку для последующего использования сeval "$find_child_dir"
.| wc -l
подсчитывает, сколько подкаталогов было найдено.while [[ "$(eval "$find_child_dir" | wc -l)" -gt 0 ]];
продолжает цикл, пока текущий каталог имеет по крайней мере один подкаталог.| head -n 1
получает первый элемент, возвращенный изfind
cd "eval "$find_child_dir" | head -n 1)"
перемещается в первый найденный подкаталог.
Конечно, это предполагает, что у вас есть только один проблемный глубоко вложенный каталог, с которым нужно справиться.
.
Ответ или решение
Вопрос касается автоматизации перехода к самой глубокой поддиректории в заданной директории, что может быть полезно для упрощения навигации по сложным файловым структурам. Давайте разберем несколько подходов к решению этой задачи, используя различные инструменты командной строки, такие как Bash и Zsh.
Решение с использованием Zsh
При использовании оболочки Zsh, вы можете воспользоваться мощной системой глоббирования (расширения имени файла). Следующий скрипт позволяет на основе глубины структуры папок перейти к самой глубокой директории:
bydepth() REPLY=${REPLY//[^\/]}
cd Dropbox/**/*(D/O+bydepth[1])
Этот скрипт выполняет сортировку директорий по глубине и выбирает самую глубокую. Если на каком-то уровне присутствует несколько директорий с одинаковой глубиной, движение будет остановлено на этом уровне. Полезные элементы:
- `/`** позволяет рекурсивно искать поддиректории.
(D)
учитывается возможность скрытых директорий.O+bydepth
сортирует результаты по глубине.
Bash и GNU утилиты
Для Bash, использование find
и awk
позволяет выполнить аналогичную операцию:
IFS= read -rd '' deepest < <(find Dropbox/ -type d -print0 |
awk -v RS='\0' -v ORS='\0' -F / '
NF > max {max = NF; deepest = $0}
END {if (max) print deepest}') && cd -- "$deepest"
Этот скрипт находит самый глубокий путь и переходит в него. Он также сортирует потенциальные пути и выбирает наиболее вложенные.
Хранение функции для более удобного использования
Создание собственной Bash функции, такой как cdd
, может существенно упростить задачу:
cdd() {
cd "$(find "$1" -type d -printf '%d:%p\0' | sort -z -t: -r | awk -F: -v RS='\0' 'NR==1{print $2}')"
}
Эта функция использует find
, чтобы построить список директорий, отсортированных по глубине, а затем использует cd
для перехода. Добавьте эту функцию в ваш .bashrc
для повседневного использования.
Альтернативные методы
Вы можете рассмотреть также другие способы, такие как рекурсия и макросы readline, в зависимости от ваших предпочтений и требований. Однако они могут потребовать больше настройки и тестирования.
Каждое из этих решений решает задачу перехода к самой глубокой поддиректории, минимизируя затраты времени на ручную навигацию, а также может быть адаптировано под ваши конкретные нужды. Выбор инструмента зависит от вашего предпочтения и окружения, будь то Bash, Zsh или другие системы.