У меня есть несколько XML-файлов в дереве каталогов, которые я хотел бы переместить в соответствующие папки с теми же именами в том же дереве каталогов.
Вот пример структуры (в оболочке):
touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
Итак, мой подход здесь таков:
find . -name "*.xml" -exec sh -c '
DST=$(
find . -type d -name "$(basename "{}" .xml)" -print -quit
)
[ -d "$DST" ] && mv -v "{}" "$DST/"' ';'
что дает следующий результат:
‘./( bar ).xml’ -> ‘./bar/( bar )/( bar ).xml’
mv: ‘./bar/( bar )/( bar ).xml’ and ‘./bar/( bar )/( bar ).xml’ are the same file
‘./bar.xml’ -> ‘./bar/bar.xml’
‘./foo.xml’ -> ‘./foo/foo.xml’
Но файл с квадратными скобками ( [ foo ].xml
) не был перемещен, как будто его проигнорировали.
Я проверил и basename
(например basename "[ foo ].xml" ".xml"
) конвертирует файл правильно, однако find
есть проблемы со скобками. Например:
find . -name '[ foo ].xml'
не находит файл правильно. Однако, при экранировании скобок ( '\[ foo \].xml'
), это работает нормально, но это не решает проблему, потому что это часть скрипта, и я не знаю, в каких файлах есть эти специальные (shell?) символы. Проверено как с BSD, так и с GNU find
.
Существует ли универсальный способ экранирования имен файлов при использовании параметра with find
, -name
чтобы я мог исправить свою команду для поддержки файлов с метасимволами?
решение1
С глобами здесь все гораздо проще zsh
:
for f (**/*.xml(.)) (mv -v -- $f **/$f:r:t(/[1]))
Или, если вы хотите включить скрытые XML-файлы и просмотреть содержимое скрытых каталогов, выполните следующие find
действия:
for f (**/*.xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))
Но учтите, что файлы с именами .xml
, ..xml
или ...xml
могут стать проблемой, поэтому вы можете исключить их:
setopt extendedglob
for f (**/(^(|.|..)).xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))
При использовании инструментов GNU еще один подход, позволяющий избежать необходимости сканирования всего дерева каталогов для каждого файла, — это однократное сканирование и поиск всех каталогов и xml
файлов, запись их местонахождения и выполнение перемещения в конце:
(export LC_ALL=C
find . -mindepth 1 -name '*.xml' ! -name .xml ! \
-name ..xml ! -name ...xml -type f -printf 'F/%P\0' -o \
-type d -printf 'D/%P\0' | awk -v RS='\0' -F / '
{
if ($1 == "F") {
root = $NF
sub(/\.xml$/, "", root)
F[root] = substr($0, 3)
} else D[$NF] = substr($0, 3)
}
END {
for (f in F)
if (f in D)
printf "%s\0%s\0", F[f], D[f]
}' | xargs -r0n2 mv -v --
)
Ваш подход имеет ряд проблем, если вы хотите разрешить любое произвольное имя файла:
- встраивание
{}
в код оболочкивсегданеправильно. А что, если есть файл, называемый$(rm -rf "$HOME").xml
например? Правильный способ — передать их{}
как аргумент в скрипт оболочки (-exec sh -c 'use as "$1"...' sh {} \;
). - С GNU
find
(подразумевается здесь, поскольку вы используете-quit
),*.xml
будет соответствовать только файлам, состоящим из последовательности допустимых символов, за которыми следует.xml
, так что это исключает имена файлов, которые содержат недопустимые символы в текущей локали (например, имена файлов в неправильной кодировке). Исправление этого заключается в исправлении локали так, чтобыC
каждый байт был допустимым символом (это означает, что сообщения об ошибках будут отображаться на английском языке). - Если какой-либо из этих
xml
файлов относится к типу каталога или символической ссылки, это может вызвать проблемы (повлиять на сканирование каталогов или сломать символические ссылки при перемещении). Вы можете добавить ,-type f
чтобы перемещать только обычные файлы. - Подстановка команд (
$(...)
) полосывсезавершающие символы новой строки. Это может вызвать проблемы с файлом, называемымfoo.xml
например. Обойти это можно, но это больно:base=$(basename "$1" .xml; echo .); base=${base%??}
. Вы можете по крайней мере заменитьbasename
на${var#pattern}
операторы. И избегайте подстановки команд, если это возможно. - ваша проблема с именами файлов, содержащими подстановочные знаки ( ,
?
и обратная косая черта; они не являются специальными для оболочки, а для сопоставления с образцом ( ), которое, как оказалось, очень похоже на сопоставление с образцом оболочки). Вам нужно будет экранировать их с помощью обратной косой черты.[
*
fnmatch()
find
- проблема с
.xml
,..xml
,...xml
упомянутая выше.
Итак, если рассмотреть все вышеперечисленное, то получится что-то вроде:
LC_ALL=C find . -type f -name '*.xml' ! -name .xml ! -name ..xml \
! -name ...xml -exec sh -c '
for file do
base=${file##*/}
base=${base%.xml}
escaped_base=$(printf "%s\n" "$base" |
sed "s/[[*?\\\\]/\\\\&/g"; echo .)
escaped_base=${escaped_base%??}
find . -name "$escaped_base" -type d -exec mv -v "$file" {\} \; -quit
done' sh {} +
Уф...
Теперь это еще не все. С помощью -exec ... {} +
мы запускаем как sh
можно меньше. Если нам повезет, мы запустим только один, но если нет, после первого sh
вызова мы переместим несколько xml
файлов, а затем find
продолжим искать еще, и вполне можем снова найти файлы, которые мы переместили в первом раунде (и, скорее всего, попытаемся переместить их туда, где они находятся).
В остальном это в основном тот же подход, что и у zsh. Еще несколько заметных отличий:
- С
zsh
одним список файлов сортируется (по имени каталога и имени файла), поэтому целевой каталог более или менее последователен и предсказуем. Сfind
, он основан на необработанном порядке файлов в каталогах. - при использовании
zsh
вы получите сообщение об ошибке, если не будет найден соответствующий каталог для перемещения файла, но не при использованииfind
подхода, описанного выше. - При использовании
find
вы получите сообщения об ошибках, если некоторые каталоги не могут быть просмотрены, а не одинzsh
.
Последнее предупреждение. Если причина, по которой вы получаете некоторые файлы с подозрительными именами, заключается в том, что дерево каталогов доступно для записи злоумышленнику, то будьте осторожны, поскольку ни одно из приведенных выше решений не является безопасным, если злоумышленник может переименовать файлы под действием этой команды.
Например, если вы используете LXDE, злоумышленник может создать вредоносный foo/lxde-rc.xml
, создать lxde-rc
папку, определить, когда вы запускаете свою команду, и заменить ее lxde-rc
символической ссылкой на вашу ~/.config/openbox/
во время окна гонки (которое можно сделать сколь угодно большим разными способами) между find
поиском lxde-rc
и mv
выполнением rename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")
( foo
также может быть изменено на эту символическую ссылку, заставив вас переместить ее lxde-rc.xml
в другое место).
Обойти это, вероятно, невозможно с помощью стандартных или даже GNU-утилиты, вам придется написать это на соответствующем языке программирования, выполняя безопасный обход каталогов и используя renameat()
системные вызовы.
Все вышеперечисленные решения также не сработают, если дерево каталогов достаточно глубокое, что достигается ограничение на длину путей, заданных системному rename()
вызову done by (что приводит к сбою с ). Решение с использованием также может обойти эту проблему.mv
rename()
ENAMETOOLONG
renameat()
решение2
При использовании встроенного скрипта с find ... -exec sh -c ...
, вам следует передать find
результат в оболочку через позиционный параметр, тогда вам не придется использовать его {}
везде во встроенном скрипте.
Если у вас есть bash
или zsh
, вы можете передать basename
вывод через printf '%q'
:
find . -name "*.xml" -exec bash -c '
for f do
BASENAME="$(printf "%q" "$(basename -- "$f" .xml)")"
DST=$(find . -type d -name "$BASENAME" -print -quit)
[ -d "$DST" ] && mv -v -- "$f" "$DST/"
done
' bash {} +
С помощью bash
можно использовать printf -v BASENAME
, но этот подход не будет работать правильно, если имя файла содержит управляющие символы или символы, не входящие в набор ASCII.
Если вы хотите, чтобы это работало правильно, вам нужно написать функцию оболочки, которая экранирует только символы , [
и обратную косую черту.*
?
решение3
Хорошие новости:
find . -name '[ foo ].xml'
не интерпретируется оболочкой, он передается таким образом в программу find. Однако Find интерпретирует аргумент как -name
шаблон glob
, и это необходимо учитывать.
Если вы хотите сделать колл find -exec \;
или лучше find -exec +
, то оболочка не нужна.
Если вы предпочитаете обрабатывать find
вывод с помощью оболочки, я рекомендую просто отключить подстановку имен файлов в оболочке, вызвав ее set -f
перед соответствующим кодом, и включить ее снова, вызвав ее set +f
позже.
решение4
Ниже приведен относительно простой конвейер, соответствующий POSIX. Он дважды сканирует иерархию, сначала для каталогов, а затем для обычных файлов *.xml. Пустая строка между сканированиями сигнализирует AWK о переходе.
Компонент AWK сопоставляет базовые имена с целевыми каталогами (если есть несколько каталогов с одинаковым базовым именем, запоминается только первый обход). Для каждого файла *.xml он печатает строку, разделенную табуляцией, с двумя полями: 1) путь к файлу и 2) соответствующий ему целевой каталог.
{
find . -type d
echo
find . -type f -name \*.xml
} |
awk -F/ '
!NF { ++i; next }
!i && !($NF".xml" in d) { d[$NF".xml"] = $0 }
i { print $0 "\t" d[$NF] }
' |
while IFS=' ' read -r f d; do
mv -- "$f" "$d"
done
Значение, присвоенное IFS непосредственно перед чтением, представляет собой символ табуляции, а не пробел.
Вот расшифровка с использованием скелета touch/mkdir оригинального вопроса:
$ touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
$ mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
$ find .
.
./foo
./foo/[ foo ]
./bar.xml
./foo.xml
./bar
./bar/( bar )
./[ foo ].xml
./( bar ).xml
$ ../mv-xml.sh
$ find .
.
./foo
./foo/[ foo ]
./foo/[ foo ]/[ foo ].xml
./foo/foo.xml
./bar
./bar/( bar )
./bar/( bar )/( bar ).xml
./bar/bar.xml