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

Как автоматически экранировать метасимволы оболочки с помощью команды `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.

Существует ли универсальный способ экранирования имен файлов при использовании параметра 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 (что приводит к сбою с ). Решение с использованием также может обойти эту проблему.mvrename()ENAMETOOLONGrenameat()

решение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

Связанный контент