Как избежать метасимволов оболочки с помощью команды `find`?

У меня есть куча 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 .

Есть ли универсальный способ экранирования имен файлов при использовании с параметром find -s -name , поэтому я могу исправить мою команду для поддержки файлов с метасимволами?

4 Solutions collect form web for “Как избежать метасимволов оболочки с помощью команды `find`?”

Здесь гораздо проще с zsh globs:

 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%??} 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 one список файлов сортируется (по имени каталога и имени файла), поэтому целевой каталог более или менее согласован и предсказуем. С помощью find он основан на необработанном порядке файлов в каталогах.
  • с zsh , вы получите сообщение об ошибке, если не найден соответствующий каталог для перемещения файла, а не с помощью метода find выше.
  • С помощью find вы получите сообщения об ошибках, если некоторые каталоги не пройдут, а не с помощью zsh .

Последнее предупреждение. Если причина, по которой вы получаете некоторые файлы с изворотливыми именами файлов, связана с тем, что дерево каталогов доступно для записи противником, тогда остерегайтесь, если ни одно из вышеперечисленных решений не будет безопасным, если противник может переименовать файлы под ногами этой команды.

Например, если вы используете LXDE, злоумышленник может создать вредоносный foo/lxde-rc.xml , создать папку lxde-rc , определить, когда вы используете свою команду, и заменить 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() безопасный обход каталога и использовать системные вызовы renameat() .

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

Когда вы используете встроенный скрипт с 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.

Если вы хотите, чтобы он работал правильно, вам нужно написать функцию оболочки для выхода только [ , * ? и обратную косую черту.

Хорошие новости:

 find . -name '[ foo ].xml' 

не интерпретируется оболочкой, он передается таким образом программе поиска. Найти, однако, интерпретирует аргумент -name как шаблон glob , и это нужно учитывать.

Если вы хотите вызвать find -exec \; или лучше find -exec + , нет оболочки.

Если вы хотите обработать вывод вывода оболочкой, я рекомендую отключить имя файла globbing в оболочке, вызвав set -f перед рассматриваемым кодом и снова включив его, вызвав set +f позже.

Ниже представлен относительно простой, совместимый с 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 -rfd; 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 
  • Каков порядок сортировки при использовании условных операторов?
  • Поиск длины сеанса терминала
  • Сохранять выходные данные в памяти для записи позже на диск
  • exec перенаправляет в bash
  • Как получить двоичные представления строк в Shell?
  • Поиск текстового файла по столбцу
  • Повторное использование пользовательского ввода в скрипте
  • как выполнять команды на удаленном сервере как разные пользователи
  • Переменные баша в команде
  • разделить линию на основе пробела и удалить вторую часть
  • Как подождать файл в сценарии оболочки?
  • Linux и Unix - лучшая ОС в мире.