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


12

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

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

for DIR in `find . -type d -printf "\"%P\"\040"`
do
  echo mkdir -p \"${OUTPATH}${DIR}\"        # 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 "${OUTPATH}${DIR}"
  echo Created $DIR
done

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

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

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


Why_would_you_ever_need_a_space_in_a_file_name?
Кевин Панко

Правда, не мои предпочтения. Хотя, чтобы удалить пробелы, сначала нужно обработать файлы с пробелами;)
Сэмюэль Яешке

1
На самом деле, имена файлов должны содержать пробелы. Я бы позволил все, кроме /и непечатаемых символов. Но разрешено все, кроме вас, /и \0вы должны их разрешить.
Кевин Панко

Ответы:


11

Тебе нужно пускать 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")")"

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


2
Кроме того, вместо "$dir"него предпочтительнее использовать "${dir}"- легко определить разницу между $ {dir} name и $ {dirname}, но $ dirname можно интерпретировать в любом случае.
Джеймс Полли

Здесь важно то, что readвся строка читается ${dir}, поэтому IFS не имеет значения.
Джеймс Полли

1
Спасибо, что нашли опечатку $ / ". В скобках нет необходимости, если после имени переменной ничего нет.
Приостановлено до дальнейшего уведомления.

4
Это будет обрабатывать имена путей с пробелами (U + 0020), но все равно не сможет правильно обрабатывать имена путей с переводами строки (U + 000A). Я предпочитаю, find … -print0 | xargs -0 …потому что используемый разделитель точно соответствует единственному символу, который не разрешен в названиях путей POSIX: NUL (U + 0000).
Крис Джонсен

2
Отлично! Как раз то, что я искал. Мне никогда не приходило в голову, что ты, возможно, сможешь паять while. @Chris Johnsen: Да, но даже программы для копирования музыки не склонны вставлять переводы строк в свои имена файлов. И если они это сделают, я хочу знать (то есть: что-то идет не так) и немедленно избавиться от них ...
Самуэль Яешке

8

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

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

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

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

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

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


Предложение Денниса Уильямсона, однако (кроме опечаток) гораздо более читабельно и, следовательно, предпочтительнее почти во всех отношениях.
Джеймс Полли

Работает, для mkdir, но извините, мне следовало быть более понятным - я хочу запустить серию команд для каждого файла. Видите ли, для моей аналогичной процедуры позже я хочу сгенерировать выходное имя файла на основе входного имени файла (которое включает удаление расширения .ogg и добавление .mp3), а затем использовать эти множественные переменные в моей линии при вызове gst-launch.
Самуэль Яешке

5

Проблема, с которой вы сталкиваетесь, заключается в том, что оператор 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? Тогда вы должны выбрать другой. Но тогда, что если ...
остановился до дальнейшего уведомления.

3
Вы можете выбрать как /на POSIX, так и :на файловых системах DOS. Существуют недопустимые символы для разных файловых систем, которые вы можете выбрать для IFS. Что-нибудь более сложное, и вам лучше использовать Perl.
Даррен Холл

2
Проблема с использованием / заключается в том, что это разделитель каталогов и он findвозвращает имена файлов с путями, включая косую черту. Попробуйте изменить точку с запятой в вашем скрипте на косую черту, и echo напечатает каталог и имя файла в отдельных строках.
Приостановлено до дальнейшего уведомления.

Это также выглядит весьма полезным. Я пошел с трубой к whileварианту, но это также выглядит вполне работоспособным. Да, в моей аналогичной структуре позже мне нужно было сделать дальнейший анализ. (Входное имя файла будет .ogg, которое будет передано как filesrcв конвейере gst, но будет сгенерировано эквивалентное окончание .mp3 на основе выходного каталога, а также передано в конвейер как filesink, и это, конечно, необходимо сделать для каждого файла вместе с некоторыми echoдля пользователя.)
Сэмюэль Яешке

4

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

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

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


Еще одна интересная концепция. Спасибо - я уверен, я найду для этого применение позже :)
Самуэль Йешке

1

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

mkdir -p \"${OUTPATH}${DIR}\"

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

mkdir -p "${OUTPATH}${DIR}"

Благодарю. Исправлена. Он также читал FILENAME вместо DIR - copy-paste: P
Самуэль Яешке


0

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

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

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


Нет, мне нужна такая итерационная структура, которая позволяет мне запускать несколько строк кода для каждого файла. «Теперь мне нужен какой-то способ, с помощью которого я могу выполнять итерации, как это, потому что я также хочу выполнить более сложную команду, включающую gstreamer, для каждого файла в следующей похожей структуре». Извините, если мне было неясно.
Самуэль Яешке

Команда, которую я дал, решает проблему, которую вы задали, не имеет значения, является ли это частью большего «трубопровода» с вашей стороны. для кого-то другого, имеющего проблему, как описано в вопросе, rsync-подход будет работать. так что не стоит сожалеть о потенциальной неясности :)
akira

Да. Нет, я имею в виду, что позже я буду использовать аналогичную while... do... doneструктуру, чтобы выполнить аналогичную обработку из find, что потребует выполнения нескольких строк кода для каждого файла (изменение строки, echo, gst-launch и т. Д. ) и rsyncне достиг бы этого. Вот почему я указал, что мне нужно иметь возможность запускать более сложный набор команд в рамках аналогичной структуры. Мой сценарий использует эту структуру цикла дважды, поэтому для вопроса я разместил в середине тот, который был менее грубым.
Самуэль Яешке

0

Если у вас установлен 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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.