Как избежать пробелов в списке циклов bash?

121

У меня есть сценарий оболочки 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

Вот результат:

тест / Балтимор
Испытание / Cherry
возвышенность
Тест / Edison 
Тест / New
Йорк
город
тест / Филадельфия

Черри-Хилл и Нью-Йорк рассматриваются как 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"дает следующий результат:

вишня
возвышенность
новый
Йорк
город
MCS
источник
re: edit, list="$@"полностью отбрасывает исходное значение списком, сворачивая его в строку. Пожалуйста, следуйте практике , изложенной в моем ответе, в точности так, как дано - такое задание нигде в нем не поощряется; если вы хотите передать программе список аргументов командной строки, вы должны собрать их в массив и напрямую расширить этот массив.
Чарльз Даффи

Ответы:

105

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

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

Другой безопасный подход - использовать список с завершающим NUL, хотя для этого требуется ваша поддержка find -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переменной, которая не выполняет не содержат пробела. Отключите globbing ( set -f), чтобы предотвратить раскрытие строк, содержащих символы glob, такие как [], *или ?:

# 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

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

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done
Чарльз Даффи
источник
1
не знал об этом «+» для -exec. sweet
Йоханнес Шауб - лит
1
похоже, что он также может, как и xargs, помещать аргументы только в конец данной команды: / это иногда меня
раздражало
Я думаю, -exec [name] {} + - это расширение GNU и 4.4-BSD. (По крайней мере, его нет в Solaris 8, и я не думаю, что он был в AIX 4.3.) Думаю, остальные из нас могут застрять в конвейере с xargs ...
Майкл Ратанапинта,
2
Я никогда раньше не видел синтаксиса $ '\ n'. Как это работает? (Я бы подумал, что IFS = '\ n' или IFS = "\ n" будет работать, но ни то, ни другое не работает.)
MCS
1
@crosstalk это точно в Solaris 10, только что использовал.
Ник
26
find . -type d | while read file; do echo $file; done

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

find . -type d -print0 | xargs -0 echo 'The directory is: '
Йоханнес Шауб - litb
источник
Нет необходимости в xargs, см. Find -exec ... {} +
Чарльз Даффи
4
@Charles: для большого количества файлов xargs намного эффективнее: он порождает только один процесс. Параметр -exec запускает новый процесс для каждого файла, который может быть на порядок медленнее.
Адам Розенфилд
1
Мне xargs больше нравится. По сути, эти двое, похоже, делают одно и то же, в то время как у xargs больше возможностей, например, параллельная работа
Йоханнес Шауб - litb
2
Адам, нет, этот знак «+» объединит как можно больше имен файлов и затем выполнит. но у него не будет таких изящных функций, как параллельная работа :)
Йоханнес Шауб - litb
2
Обратите внимание: если вы хотите что-то сделать с именами файлов, вам придется их заключить в кавычки. Например:find . -type d | while read file; do ls "$file"; done
David Moles
23

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

Каталог тестов

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
cbliard
источник
спасибо, это сработало для псевдонима, который я создавал, чтобы указать, сколько места использует каждый каталог в текущей папке, в предыдущем воплощении он подавлялся некоторыми каталогами с пробелами. Это работает в zsh, но некоторые другие ответы нет:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Тед Налейд
2
Если вы используете zsh, вы также можете сделать это:alias duc='du -sh *(/)'
cbliard
@cbliard Это все еще глючит. Попробуйте запустить его с именем файла, например, с последовательностью табуляции или несколькими пробелами; вы заметите, что он заменяет любой из них одним пробелом, потому что вы не цитируете в своем эхо. И еще есть случай, когда имена файлов содержат символы новой строки ...
Чарльз Даффи
@CharlesDuffy Я пробовал использовать последовательности табуляции и несколько пробелов. Работает с кавычками. Я также пробовал с новой строкой, и это вообще не работает. Я обновил ответ соответственно. Спасибо, что указали на это.
cbliard
1
@cbliard Правильно - добавление кавычек в вашу команду echo - вот что я имел в виду. Что касается новых строк, вы можете заставить это работать, используя find -print0и IFS='' read -r -d '' f.
Charles Duffy
7

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

Джонатан Леффлер
источник
4

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

IFS='\n'

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

oshunluvr
источник
4
find . -print0|while read -d $'\0' file; do echo "$file"; done
Freakus
источник
1
-d $'\0'точно так же, как -d ''- поскольку bash использует строки с завершающим NUL, первый символ пустой строки - это NUL, и по той же причине NUL вообще не могут быть представлены внутри строк C.
Charles Duffy
4

я использую

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

murpel
источник
отличный совет: это очень полезно для параметров osascript командной строки (OS X AppleScript), где пробелы разделяют аргумент на несколько параметров, где предназначен только один
Тим
Нет, этого мало. Это неэффективно (из-за ненужного использования $(echo ...)), неправильно обрабатывает имена файлов с глобальными выражениями, неправильно обрабатывает имена файлов, содержащие $'\b'символы или $ '\ n', и, кроме того, преобразует несколько пробелов пробелов в одиночные пробельные символы на сторона вывода из-за неправильного цитирования.
Charles Duffy
4

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

#!/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
Гордон Дэвиссон
источник
1
Оглядываясь назад на это - на самом деле было решение с POSIX sh: вы могли повторно использовать "$@"массив, добавляя к нему с помощью set -- "$@" "$f".
Чарльз Даффи
4

Вы можете временно использовать 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

amazingthere
источник
Пожалуйста, дайте объяснение.
Steve K
IFS указывает, что такое символ-разделитель, тогда имя файла с пробелом не будет усечено.
amazingthere
$ IFS = $ OLD_IFS в конце должно быть: IFS = $ OLD_IFS
Мишель Донэ,
3

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

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;
hardbutnot
источник
2

Чтобы добавить к тому, что сказал Джонатан : используйте -print0опцию для findв сочетании со xargsследующим:

find test/* -type d -print0 | xargs -0 command

Это выполнит команду commandс правильными аргументами; каталоги с пробелами в них будут правильно заключены в кавычки (т.е. они будут переданы как один аргумент).

Адам Розенфилд
источник
1
#!/bin/bash

dirtys=()

for folder in *
do    
 if [ -d "$folder" ]; then    
    dirtys=("${dirtys[@]}" "$folder")    
 fi    
done    

for dir in "${dirtys[@]}"    
do    
   for file in "$dir"/\*.mov   # <== *.mov
   do    
       #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'`   -- This line will replace each space into '\ '   
       out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'`     # These two line code can be written in one line using multiple sed commands.    
       out=`echo "$out" | sed 's/[[:space:]]/_/g'`    
       #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"    
       `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`    
   done    
done

Приведенный выше код преобразует файлы .mov в .avi. Файлы .mov находятся в разных папках, и в именах папок также есть пробелы . Мой сценарий выше преобразует файлы .mov в файл .avi в той же самой папке. Не знаю, поможет ли это вам, народы.

Случай:

[sony@localhost shell_tutorial]$ ls
Chapter 01 - Introduction  Chapter 02 - Your First Shell Script
[sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/
[sony@localhost Chapter 01 - Introduction]$ ls
0101 - About this Course.mov   0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ ./above_script
 ... successfully executed.
[sony@localhost Chapter 01 - Introduction]$ ls
0101_-_About_this_Course.avi  0102_-_Course_Structure.avi
0101 - About this Course.mov  0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ CHEERS!

Ура!

Сони Джордж
источник
echo "$name" | ...не работает, если nameесть -n, и то, как он ведет себя с именами с обратной косой чертой и escape-последовательностями, зависит от вашей реализации - POSIX делает поведение echoв этом случае явно неопределенным (тогда как POSIX с расширенным XSI расширяет стандартные обратные косые черты с escape-последовательностями и системы GNU, включая bash, без POSIXLY_CORRECT=1нарушения стандарта POSIX путем реализации -e(в то время как спецификация требует echo -eпечати -eна выходе). printf '%s\n' "$name" | ...безопаснее.
Чарльз Даффи
1

Также приходилось иметь дело с пробелами в именах путей. В конце концов я использовал рекурсию и for item in /path/*:

function recursedir {
    local item
    for item in "${1%/}"/*
    do
        if [ -d "$item" ]
        then
            recursedir "$item"
        else
            command
        fi
    done
}
Жиль 'ТАК - перестань быть злым'
источник
1
Не используйте functionключевое слово - оно делает ваш код несовместимым с POSIX sh, но не имеет другой полезной цели. Вы можете просто определить функцию с помощью recursedir() {, добавив две скобки и удалив ключевое слово function, и это будет совместимо со всеми POSIX-совместимыми оболочками.
Charles Duffy
1

Преобразуйте список файлов в массив Bash. Здесь используется подход Мэтта МакКлюра для возврата массива из функции Bash: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html Результатом является способ для преобразования любого многострочного ввода в массив Bash.

#!/bin/bash

# This is the command where we want to convert the output to an array.
# Output is: fileSize fileNameIncludingPath
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'"

# This eval converts the multi-line output of multiLineCommand to a
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" )
eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`"

for f in "${myArray[@]}"
do
   echo "Element: $f"
done

Этот подход работает, даже когда присутствуют плохие символы, и является общим способом преобразования любого ввода в массив Bash. Недостатком является то, что если ввод длинный, вы можете превысить ограничения на размер командной строки Bash или использовать большой объем памяти.

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

Мне также не нравится установка IFS, это может испортить другой код.

Стив Зобелл
источник
Если вы используете IFS='' read, в той же строке, параметр IFS присутствует только для команды чтения и не экранирует ее. Нет причин не любить такую ​​настройку IFS.
Charles Duffy
1

Что ж, я вижу слишком много сложных ответов. Я не хочу передавать вывод утилиты find или писать цикл, потому что для этого у find есть опция "exec".

Моя проблема заключалась в том, что я хотел переместить все файлы с расширением dbf в текущую папку, а некоторые из них содержали пробелы.

Я взялся за это так:

 find . -name \*.dbf -print0 -exec mv '{}'  . ';'

Для меня это выглядит очень просто

Теб
источник
0

только что выяснил, что между моим и вашим вопросом есть некоторые сходства . Например, если вы хотите передавать аргументы в команды

test.sh "Cherry Hill" "New York City"

распечатать их по порядку

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

обратите внимание, что $ @ заключен в двойные кавычки, некоторые примечания здесь

Jeffrey04
источник
0

Мне нужна была такая же концепция для последовательного сжатия нескольких каталогов или файлов из определенной папки. Я решил использовать awk для анализа списка из ls и избежать проблемы с пустым пространством в имени.

source="/xxx/xxx"
dest="/yyy/yyy"

n_max=`ls . | wc -l`

echo "Loop over items..."
i=1
while [ $i -le $n_max ];do
item=`ls . | awk 'NR=='$i'' `
echo "File selected for compression: $item"
tar -cvzf $dest/"$item".tar.gz "$item"
i=$(( i + 1 ))
done
echo "Done!!!"

что ты думаешь?

Hìr0
источник
Я думаю, что это не будет работать правильно, если в именах файлов есть символы новой строки. Возможно, тебе стоит попробовать.
user000001
0
find Downloads -type f | while read file; do printf "%q\n" "$file"; done
Йохан Кассельман
источник
-3

Для меня это работает, и это довольно "чисто":

for f in "$(find ./test -type d)" ; do
  echo "$f"
done
AndrzejP
источник
4
Но это еще хуже. Двойные кавычки вокруг поиска приводят к объединению всех имен путей в одну строку. Измените эхо на ls, чтобы увидеть проблему.
NVRAM
-4

Просто возникла проблема простого варианта ... Конвертировать файлы из набранного .flv в .mp3 (зевать).

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done

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

Марк Вашейм
источник
2
readПосле того, как inеще одно слово в списке вы Перебор. То, что вы опубликовали, представляет собой немного поврежденную версию того, что было у автора вопроса, что не работает. Возможно, вы намеревались опубликовать что-то другое, но, вероятно, здесь есть другие ответы.
Жиль 'SO- перестань быть злом'