Как применить команду оболочки ко многим файлам во вложенных (и плохо экранированных) подкаталогах? - PullRequest
6 голосов
/ 16 апреля 2009

Я пытаюсь сделать что-то вроде следующего:

for file in `find . *.foo`
do
somecommand $file
done

Но команда не работает, потому что $ file очень странный. Поскольку у моего дерева каталогов есть дрянные имена файлов (включая пробелы), мне нужно экранировать команду find. Но ни одно из очевидных побегов, похоже, не работает: -ls дает мне разделенные пробелами фрагменты имени файла -fprint лучше не делает.

Я тоже пытался: for file in " найти. * .foo -ls "; do echo $file; done - but that gives all of the responses from find in one long line.

Есть намеки? Я рад за любой обходной путь, но расстроен, что не могу понять это.

Спасибо, Alex

(Привет, Мэтт!)

Ответы [ 6 ]

11 голосов
/ 16 апреля 2009

У вас есть множество ответов, которые хорошо объясняют, как это сделать; но для завершения я повторю и добавлю к этому:

xargs всегда полезен только для интерактивного использования (когда вы знаете, что все ваши имена файлов просты - без пробелов и кавычек) или когда используется опция -0. Иначе все сломается.

find - очень полезный инструмент; использование его для передачи имен файлов в xargs (даже с -0) довольно запутанно, поскольку find может делать все сам с помощью -exec command {} \; или -exec command {} + в зависимости от того, что вы хотите:

find /path -name 'pattern' -exec somecommand {} \;
find /path -name 'pattern' -exec somecommand {} +

Первый из них запускает somecommand с одним аргументом для каждого файла рекурсивно в /path, который соответствует pattern.

Последний запускает somecommand с столько аргументов, сколько умещается в командной строке одновременно для файлов рекурсивно в /path, которые соответствуют pattern.

Какой из них использовать, зависит от somecommand. Если он может принимать несколько аргументов имени файла (например, rm, grep и т. Д.), Тогда последний вариант работает быстрее (поскольку вы запускаете somecommand гораздо реже). Если somecommand принимает только один аргумент, вам нужно первое решение. Посмотрите на справочную страницу somecommand.

Подробнее о find: http://mywiki.wooledge.org/UsingFind

В bash, for - это оператор, который перебирает аргументов . Если вы делаете что-то вроде этого:

for foo in "$bar"

вы даете for один аргумент для итерации (обратите внимание на кавычки!). Если вы делаете что-то вроде этого:

for foo in $bar

вы просите bash взять содержимое bar и разорвать его на части, где есть пробелы, символы табуляции или новые строки (технически, какие бы символы не были в IFS), и использовать фрагменты этой операции в качестве аргументов. для. Это НЕ имена файлов . Предполагать, что результат разрывания длинной строки, которая содержит имена файлов, где есть пробелы в куче имен файлов, просто неверен. Как вы только что заметили.

Ответ таков: не используйте for, это, очевидно, неправильный инструмент. Все вышеприведенные команды find предполагают, что somecommand является исполняемым файлом в PATH. Если это оператор bash, вам понадобится эта конструкция (перебирает вывод find, как вы пытались, но безопасно):

while read -r -d ''; do
    somebashstatement "$REPLY"
done < <(find /path -name 'pattern' -print0)

При этом используется цикл while-read, который считывает части строки, выводимой find, до тех пор, пока не достигнет байта NULL (то, что -print0 использует для разделения имен файлов). Поскольку NULL байтов не могут быть частью имен файлов (в отличие от пробелов, табуляции и новых строк), это безопасная операция.

Если вам не нужно, чтобы somebashstatement был частью вашего скрипта (например, он не меняет среду скрипта, сохраняя счетчик или устанавливая переменную или что-то подобное), тогда вы все равно можете использовать find ' s -exec для запуска вашего оператора bash:

find /path -name 'pattern' -exec bash -c 'somebashstatement "$1"' -- {} \;
find /path -name 'pattern' -exec bash -c 'for file; do somebashstatement "$file"; done' -- {} +

Здесь -exec выполняет команду bash с тремя или более аргументами.

  1. Оператор bash для выполнения.
  2. A --. bash поместит это в $0, вы можете поместить все, что вам нравится, на самом деле.
  3. Ваше имя файла или имена файлов (в зависимости от того, использовали ли вы {} \; или {} + соответственно). Имя файла заканчивается в $1$2, $3, ... если их больше одного, конечно).

Оператор bash в первой команде find здесь выполняет somebashstatement с именем файла в качестве аргумента.

Оператор bash во второй команде find здесь выполняет цикл for (! ), который выполняет итерацию по каждому позиционному параметру (вот что такое сокращенный синтаксис for - for foo; do - делает) и запускает somebashstatement с именем файла в качестве аргумента. Разница между самым первым оператором find, который я показал с -exec {} +, заключается в том, что мы запускаем только один bash процесс для большого количества имен файлов, но по-прежнему один somebashstatement для каждого из этих имен файлов.

Все это также хорошо объяснено на странице UsingFind, ссылки на которую приведены выше.

9 голосов
/ 16 апреля 2009

Вместо того, чтобы полагаться на оболочку для выполнения этой работы, положитесь на find, чтобы сделать это:

find . -name "*.foo" -exec somecommand "{}" \;

Тогда имя файла будет правильно экранировано и никогда не будет интерпретироваться оболочкой.

2 голосов
/ 16 апреля 2009
find . -name '*.foo' -print0 | xargs -0 -n 1 somecommand

Хотя, если вам нужно выполнить несколько команд оболочки для каждого элемента, это может привести к путанице.

1 голос
/ 16 апреля 2009
find . -name '*.foo' -print0 | xargs -0 sh -c 'for F in "${@}"; do ...; done' "${0}"
1 голос
/ 16 апреля 2009

xargs ваш друг. Вы также захотите изучить опцию -0 (ноль) с ним. find-print0) поможет составить список. На странице Википедии есть несколько хороших примеров.

Еще одна полезная причина для использования xargs заключается в том, что если у вас много файлов (десятки или больше), xargs разделит их на отдельные вызовы для того, что затем будет вызвано для запуска xargs (в первом примере Википедии, rm)

0 голосов
/ 17 апреля 2009

Некоторое время назад мне пришлось сделать нечто подобное, переименовав файлы, чтобы они могли жить в средах Win32:

<code>#!/bin/bash
IFS=$'\n'
function RecurseDirs
{
for f in "$@"
do
  newf=<code>echo "${f}" | sed -e 's/[\\/:\*\?#"\|<>]/_/g'</code>
  if [ ${newf} != ${f} ]; then
    echo "${f}" "${newf}"
    mv "${f}" "${newf}"
    f="${newf}"
  fi
  if [[ -d "${f}" ]]; then
    cd "${f}"
    RecurseDirs $(ls -1 ".")
  fi
done
cd ..
}
RecurseDirs .

Это, вероятно, немного упрощенно, не избегает конфликтов имен, и я уверен, что это можно сделать лучше - но это устраняет необходимость использования базового имени в результатах поиска (в моем случае) до выполнения замена сед.

Я могу спросить, что вы делаете с найденными файлами, точно?

...