Заставить BASH-скрипт `for` обрабатывать имена файлов с пробелами (или обходным путем)

14671
Samuel Jaeschke

Хотя я использую BASH в течение нескольких лет, мой опыт работы со сценариями BASH относительно ограничен.

Мой код, как показано ниже. Он должен получить всю структуру каталогов из текущего каталога и скопировать его в $OUTDIR.

for DIR in `find . -type d -printf "\"%P\"\040"` do echo mkdir -p \"$$\" # Using echo for debug; working script will simply execute mkdir echo Created $DIR done 

Проблема, вот пример моей файловой структуры:

$ ls Expect The Impossible-Stellar Kart Five Iron Frenzy - Cheeses... Five Score and Seven Years Ago-Relient K Hello-After Edmund I Will Go-Starfield Learning to Breathe-Switchfoot MMHMM-Relient K 

Обратите внимание на пробелы: -S И forпринимает параметры слово за словом, поэтому вывод моего скрипта выглядит примерно так:

Creating directory structure... mkdir -p "/myfiles/multimedia/samjmusicmp3test/Learning" Created Learning mkdir -p "/myfiles/multimedia/samjmusicmp3test/to" Created to mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot" Created Breathe-Switchfoot 

Но мне нужно, чтобы получить целые имена файлов (по одной строке за раз) из вывода find. Я также попытался сделать findдвойные кавычки вокруг каждого имени файла. Но это не помогает.

for DIR in `find . -type d -printf "\"%P\"\040"` 

И вывод с этой измененной строкой:

Creating directory structure... mkdir -p "/myfiles/multimedia/samjmusicmp3test/""" Created "" mkdir -p "/myfiles/multimedia/samjmusicmp3test/"Learning" Created "Learning mkdir -p "/myfiles/multimedia/samjmusicmp3test/to" Created to mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot"" Created Breathe-Switchfoot" 

Теперь мне нужен какой-то способ, с помощью которого я мог бы выполнять итерацию таким образом, потому что я также хотел бы выполнить более сложную команду, включающую gstreamerкаждый файл в следующей аналогичной структуре. Как я должен это делать?

Редактировать: мне нужна структура кода, которая позволит мне запускать несколько строк кода для каждого каталога / файла / цикла. Извините, если мне было неясно.

Решение: я изначально пробовал:

find . -type d | while read DIR do mkdir -p "$$" echo Created $DIR done 

Это работало нормально по большей части. Однако позже я обнаружил, что, поскольку конвейер приводит к выполнению цикла while в подоболочке, все переменные, установленные в цикле, впоследствии были недоступны, что затрудняло реализацию счетчика ошибок. Мое окончательное решение (из этого ответа на SO ):

while read DIR do mkdir -p "$$" echo Created $DIR done < <(find . -type d) 

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

12
Why_would_you_ever_need_a_space_in_a_file_name? Kevin Panko 10 лет назад 0
Правда, не мои предпочтения. Хотя, чтобы удалить пробелы, сначала нужно обработать файлы пробелами;) Samuel Jaeschke 10 лет назад 0
На самом деле имена файлов должны содержать пробелы. Я бы позволил все, кроме `/` и непечатаемых символов. Но разрешено все, кроме `/` и `\ 0`, поэтому вы должны разрешить их. Kevin Panko 10 лет назад 1

8 ответов на вопрос

11
Dennis Williamson

Тебе нужно пускать трубку findв whileпетлю.

find ... | while read -r dir do something with "$dir" done 

Кроме того, вам не нужно использовать -printfв этом случае.

Вы можете сделать это доказательство для файлов с символами новой строки в их именах, если хотите, используя разделитель нулевым байтом (это единственный символ, который не может появиться в * nix filepath):

find ... -print0 | while read -d '' -r dir do something with "$dir" done 

Вы также найдете использование $()вместо обратных кавычек, чтобы быть более универсальным и простым. Они могут быть вложены гораздо проще, а цитирование - гораздо проще. Этот надуманный пример проиллюстрирует эти моменты:

echo "$(echo "$(echo "hello")")" 

Попробуйте сделать это с помощью галочек.

Кроме того, вместо `" $ dir "` предпочтительно использовать `" $ "` - легко определить разницу между $ name и $ , но $ dirname можно интерпретировать в любом случае , James Polley 14 лет назад 2
Здесь важно то, что `read` читает всю строку в` $ `, поэтому IFS не имеет значения. James Polley 14 лет назад 0
Спасибо, что нашли опечатку $ / ". В скобках нет необходимости, если после имени переменной ничего нет. Dennis Williamson 14 лет назад 1
@James Polley: только люди будут интерпретировать «$ dirname» как «$ dir» + «имя»; `bash` всегда видит его как" $ dirname ". но вы правы, я нахожу синтаксис $ более понятным. quack quixote 14 лет назад 0
Это будет обрабатывать имена путей с пробелами (U + 0020), но все равно не сможет правильно обрабатывать имена путей с переводами строки (U + 000A). Я предпочитаю `найти… -print0 | xargs -0… `потому что используемый разделитель точно соответствует единственному символу, который не разрешен в путевых именах POSIX: NUL (U + 0000). Chris Johnsen 14 лет назад 4
Отлично! Как раз то, что я искал. Мне никогда не приходило в голову, что вы, возможно, сможете передать «время». @Chris Johnsen: Да, но даже программы для копирования музыки не склонны вставлять переводы строк в свои имена файлов. И если они это сделают, я хочу знать (то есть: что-то идет не так) и немедленно избавиться от них ... Samuel Jaeschke 14 лет назад 2
7
James Polley

Посмотрите этот ответ, который я написал несколько дней назад, для примера скрипта, который обрабатывает имена файлов с пробелами.

Есть немного более запутанный (но более лаконичный) способ достичь того, что вы пытаетесь сделать:

find . -type d -print0 | xargs -0 -I {} mkdir -p ../theredir/{} 

-print0говорит find для разделения аргументов с нулем; -0 для xargs говорит ему ожидать аргументы, разделенные нулями. Это означает, что он прекрасно обрабатывает пробелы.

-I {}говорит xargs заменить строку {}именем файла. Это также подразумевает, что в командной строке должно использоваться только одно имя файла (обычно xargs будет заполнять столько, сколько поместится в строке)

В остальном должно быть очевидно.

Предложение Денниса Уильямсона, однако (кроме опечаток) гораздо более читабельно, и, следовательно, предпочтительнее почти во всех отношениях. James Polley 14 лет назад 0
Работает, для mkdir, но извините, мне следовало быть более ясным - я хочу запустить серию команд для каждого файла. Видите ли, для моей аналогичной подпрограммы позже я хочу сгенерировать выходное имя файла на основе входного имени файла (которое включает удаление расширения .ogg и добавление .mp3), а затем использовать эти несколько переменных в моей линии при вызове gst-launch. Samuel Jaeschke 14 лет назад 0
5
Darren Hall

Проблема, с которой вы сталкиваетесь, заключается в том, что оператор for отвечает на поиск как отдельные аргументы. Разделитель пространства. Вам нужно использовать переменную IFS bash, чтобы не разделять пространство.

Вот ссылка, которая объясняет, как это сделать.

Внутренняя переменная IFS

Одним из способов решения этой проблемы является изменение внутренней переменной Bash IFS (Internal Field Separator) так, чтобы она разделяла поля чем-то отличным от пробела по умолчанию (пробел, табуляция, символ новой строки), в данном случае запятой.

#!/bin/bash IFS=$';'  for I in `find -type d -printf \"%P\"\;` do echo "== $I ==" done 

Настройте свою находку для вывода разделителя полей после% P и установите свой IFS соответствующим образом. Я выбрал точку с запятой, так как это вряд ли можно найти в ваших именах файлов.

Другой альтернативой является вызов mkdir из поиска непосредственно через -exec, если вы можете вообще пропустить цикл for. Это если вам не нужно делать дополнительный анализ.

Что если имя файла содержит IFS? Тогда вы должны выбрать другой. Но тогда, что если ... Dennis Williamson 14 лет назад 0
Вы можете выбрать `/` в POSIX и `:` в файловых системах DOS. Существуют недопустимые символы для разных файловых систем, которые вы можете выбрать для IFS. Что-нибудь более сложное, и вам лучше использовать Perl. Darren Hall 14 лет назад 3
Проблема с использованием / заключается в том, что это разделитель каталогов, а `find` возвращает имена файлов с путями, включая косую черту. Попробуйте изменить точку с запятой в вашем скрипте на косую черту, и echo напечатает каталог и имя файла в отдельных строках. Dennis Williamson 14 лет назад 2
Это также выглядит весьма полезным. Я пошел с опцией pipe к `while`, но это также выглядит вполне работоспособным. Да, в моей аналогичной структуре позже мне нужно было сделать дальнейший анализ. (Входное имя файла будет .ogg, которое будет передано как `filesrc` в конвейере gst, но будет сгенерировано эквивалентное окончание .mp3 на основе выходного каталога, а также передано в конвейер как` filesink`, и это Конечно, это необходимо сделать для каждого файла, а также для пользователя "echo".) Samuel Jaeschke 14 лет назад 0
4
Chris Johnsen

Если тело вашего цикла больше, чем одна команда, можно использовать xargs для запуска сценария оболочки:

export OUTPATH=/some/where/else/ find . -type d -print0 | xargs -0 bash -c 'for DIR in "$@"; do printf "mkdir -p %q\\n" "$$" # Using echo for debug; working script will simply execute mkdir echo Created $DIR done' - 

Не забудьте включить завершающую черту (или другое слово), если оболочка относится к разновидности Bourne / POSIX (она используется для установки $ 0 в сценарии оболочки). Кроме того, следует соблюдать осторожность при заключении в кавычки, поскольку сценарий оболочки пишется внутри строки в кавычках, а не непосредственно в приглашении.

Еще одна интересная концепция. Спасибо - я уверен, я найду это позже :) Samuel Jaeschke 14 лет назад 0
1
user23307

в вашем обновленном вопросе у вас есть

mkdir -p \"$$\" 

это должно быть

mkdir -p "$$" 
Благодарю. Исправлена. Он также читал в FILENAME вместо DIR - copy-paste: P Samuel Jaeschke 14 лет назад 0
1
Vouze
find . -type d -exec mkdir -p "{}\040" ';' -exec echo "Created {}\040" ';' 
0
akira

или сделать все это намного менее сложным:

% rsync -av --include='*/' --exclude='*' SRC DST 

это копирует структуру каталогов SRC в DST.

Нет, мне нужна такая итерационная структура, которая позволяет мне запускать несколько строк кода для каждого файла. «Теперь мне нужен какой-то способ, с помощью которого я мог бы выполнять итерацию таким образом, потому что я также хотел бы выполнить более сложную команду, включающую gstreamer, для каждого файла в следующей аналогичной структуре». Извините, если мне было неясно. Samuel Jaeschke 14 лет назад 0
Команда, которую я дал, решает проблему, которую вы задали, не имеет значения, является ли это частью более крупного «трубопровода» с вашей стороны. для кого-то другого, имеющего проблему, как описано в вопросе, rsync-подход будет работать. так что не стоит сожалеть о потенциальной неясности :) akira 14 лет назад 0
Да. Нет, я имею в виду, что позже я буду использовать аналогичную структуру `while` ...` do` ... `done`, чтобы выполнить аналогичную обработку из find, что потребует выполнения нескольких строк кода для каждого файла (изменить строку , echo, gst-launch и т. д.) и `rsync` этого не добьются. Вот почему я указал, что мне нужно иметь возможность запускать более сложный набор команд в рамках аналогичной структуры. Мой сценарий использует эту структуру цикла дважды, поэтому для вопроса я разместил в середине тот, который был менее грубым. Samuel Jaeschke 14 лет назад 0
0
Ole Tange

Если у вас установлен GNU Parallel http: // www.gnu.org/software/parallel/, вы можете сделать это:

find . -type d | parallel echo making {} ";" mkdir -p /tmp/outdir/{} ";" echo made {} 

Посмотрите вступительное видео для GNU Parallel, чтобы узнать больше: http://www.youtube.com/watch?v=OpaiGYxkSuQ