Как я могу избежать пробелов в списке петли bash? - PullRequest
117 голосов
/ 19 ноября 2008

У меня есть скрипт оболочки bash, который перебирает все дочерние каталоги (но не файлы) определенного каталога. Проблема в том, что некоторые имена каталогов содержат пробелы.

Вот содержимое моего тестового каталога:

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

И код, который проходит по каталогам:

for f in `find test/* -type d`; do
  echo $f
done

Вот вывод:

test/Baltimore
test/Cherry
Hill
test/Edison 
test/New
York
City
test/Philadelphia

Черри-Хилл и Нью-Йорк считаются 2 или 3 отдельными записями.

Я попытался процитировать имена файлов, например:

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

но безрезультатно.

Должен быть простой способ сделать это.


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

Я принял предложение Чарльза о настройке IFS и придумал следующее:

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

и это прекрасно работает, если в аргументах командной строки нет пробелов (даже если эти аргументы указаны в кавычках). Например, вызов сценария следующим образом: test.sh "Cherry Hill" "New York City" дает следующий вывод:

Cherry
Hill
New
York
City

Ответы [ 20 ]

103 голосов
/ 19 ноября 2008

Во-первых, не делай так. Наилучший подход - правильно использовать find -exec:

# this is safe
find test -type d -exec echo '{}' +

Другим безопасным подходом является использование списка, заканчивающегося NUL, хотя для этого требуется, чтобы ваша поддержка поиска поддерживала -print0:

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

Вы также можете заполнить массив из find и передать этот массив позже:

# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
  myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want

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

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

Если кто-то не собирается использовать один из вышеперечисленных, третий подход (менее эффективный с точки зрения использования времени и памяти, так как он читает весь вывод подпроцесса перед делением слов), заключается в использовании IFS переменная, которая не содержит пробела. Отключите глобализацию (set -f), чтобы предотвратить расширение строк, содержащих символы глобуса, такие как [], * или ?:

# this is unsafe (but less unsafe than it would be without the following precautions)
(
 IFS=$'\n' # split only on newlines
 set -f    # disable globbing
 for n in $(find test -mindepth 1 -type d); do
   printf '%q\n' "$n"
 done
)

Наконец, для случая параметра командной строки вы должны использовать массивы, если ваша оболочка поддерживает их (то есть это ksh, bash или zsh):

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

будет поддерживать разделение. Обратите внимание, что цитирование (и использование $@ вместо $*) важно. Массивы могут быть заполнены и другими способами, такими как выражения glob:

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done
25 голосов
/ 19 ноября 2008
find . -type d | while read file; do echo $file; done

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

find . -type d -print0 | xargs -0 echo 'The directory is: '
21 голосов
/ 23 сентября 2009

Вот простое решение, которое обрабатывает вкладки и / или пробелы в имени файла. Если вам приходится иметь дело с другими странными символами в имени файла, такими как перевод строки, выберите другой ответ.

Тестовый каталог

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Код для перехода в каталоги

find test -type d | while read f ; do
  echo "$f"
done

Имя файла должно быть заключено в кавычки ("$f"), если используется в качестве аргумента. Без кавычек пробелы выступают в качестве разделителя аргументов, и для вызываемой команды передается несколько аргументов.

А на выходе:

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia
7 голосов
/ 19 ноября 2008

Это очень сложно в стандартном Unix, и большинство решений не содержат символов новой строки или какого-либо другого символа. Однако, если вы используете набор инструментов GNU, вы можете использовать опцию find -print0 и использовать xargs с соответствующей опцией -0 (минус ноль). Есть два символа, которые не могут появиться в простом имени файла; это косая черта и NUL '\ 0'. Очевидно, косая черта появляется в путевых именах, поэтому решение GNU использовать NUL '\ 0' для обозначения конца имени является гениальным и надежным.

4 голосов
/ 26 февраля 2012

Почему бы просто не поставить

IFS='\n'

перед командой for? Это изменяет разделитель полей с на просто

4 голосов
/ 09 марта 2016

Вы можете использовать IFS (внутренний разделитель полей) временно, используя:

OLD_IFS=$IFS     # Stores Default IFS
IFS=$'\n'        # Set it to line break
for f in `find test/* -type d`; do
    echo $f
done

$IFS=$OLD_IFS

4 голосов
/ 10 июня 2012

Я использую

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

Разве этого не достаточно?
Идея взята из http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html

4 голосов
/ 27 апреля 2009

Не хранить списки как строки; храните их как массивы, чтобы избежать путаницы. Вот пример сценария, который будет работать со всеми подкаталогами теста или со списком, представленным в его командной строке:

#!/bin/bash
if [ $# -eq 0 ]; then
        # if no args supplies, build a list of subdirs of test/
        dirlist=() # start with empty list
        for f in test/*; do # for each item in test/ ...
                if [ -d "$f" ]; then # if it's a subdir...
                        dirlist=("${dirlist[@]}" "$f") # add it to the list
                fi
        done
else
        # if args were supplied, copy the list of args into dirlist
        dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
        printf "Directory: %s\n" "$dir"
done

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

$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh 
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City
4 голосов
/ 15 марта 2012
find . -print0|while read -d $'\0' file; do echo "$file"; done
3 голосов
/ 04 ноября 2012

пс, если речь идет только о пробеле во входных данных, то некоторые двойные кавычки сработали для меня ...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;
...