Вопрос или проблема
Как правильно проходить по строкам в bash, либо в переменной, либо по выходу команды?
Простая установка переменной IFS на новую строку работает для выхода команды, но не при обработке переменной, которая содержит новые строки.
Например
#!/bin/bash
list="One\ntwo\nthree\nfour"
#Вывод списка с помощью echo
echo -e "echo: \n$list"
#Установите разделитель полей на новую строку
IFS=$'\n'
#Попробуйте пройтись по каждой строке
echo "Цикл for:"
for item in $list
do
echo "Item: $item"
done
#Вывод переменной в файл
echo -e $list > list.txt
#Попробуйте пройтись по каждой строке из команды cat
echo "Цикл for по выходу команды:"
for item in `cat list.txt`
do
echo "Item: $item"
done
Это дает следующий вывод:
echo:
One
two
three
four
Цикл for:
Item: One\ntwo\nthree\nfour
Цикл for по выходу команды:
Item: One
Item: two
Item: three
Item: four
Как видите, вывод переменной или итерация по команде cat
печатает каждую строку по отдельности корректно. Однако первый цикл for выводит все элементы в одной строке. Есть идеи?
С помощью bash, если вы хотите вставить новые строки в строку, заключите строку в $''
:
$ list="One\ntwo\nthree\nfour"
$ echo "$list"
One\ntwo\nthree\nfour
$ list=$'One\ntwo\nthree\nfour'
$ echo "$list"
One
two
three
four
И если у вас уже есть такая строка в переменной, вы можете считывать ее построчно с помощью:
while IFS= read -r line; do
echo "... $line ..."
done <<< "$list"
@wheeler делает хорошее замечание о <<<
добавлении завершающей новой строки.
Предположим, переменная заканчивается новой строкой
list=$'One\ntwo\nthree\nfour\n'
Тогда цикл while выводит
while IFS= read -r line; do
echo "... $line ..."
done <<< "$list"
... One ...
... two ...
... three ...
... four ...
... ...
Чтобы это обойти, используйте перенаправление от подстановки процесса вместо строки здесь
while IFS= read -r line; do
echo "... $line ..."
done < <(printf '%s' "$list")
... One ...
... two ...
... three ...
... four ...
Но теперь это “не срабатывает” для строк без завершающей новой строки
list2=$'foo\nbar\nbaz'
while IFS= read -r line; do
echo "... $line ..."
done < <(printf '%s' "$list2")
... foo ...
... bar ...
Документация по read
говорит
Код завершения равен нулю, если не достигнут конец файла
и поскольку ввод не заканчивается новой строкой, EOF достигается до того, как read
может получить целую строку. read
завершается не нулевым значением, и цикл while завершается.
Однако символы потребляются в переменную.
Итак, абсолютно правильный способ пройтись по строкам строки:
while IFS= read -r line || [[ -n $line ]]; do
echo "... $line ..."
done < <(printf '%s' "$list2")
Это дает ожидаемый вывод как для $list
, так и для $list2
Вы можете использовать while
+ read
:
some_command | while read line ; do
echo === $line ===
done
Кстати, опция -e
для echo
нестандартна. Используйте вместо этого printf
, если хотите портативность.
#!/bin/sh
items="
one two three four
hello world
this should work just fine
"
IFS='
'
count=0
for item in $items
do
count=$((count+1))
echo $count $item
done
Вот забавный способ сделать ваш цикл for:
for item in ${list//\\n/
}
do
echo "Item: $item"
done
Немного более разумным/читабельным будет:
cr="
"
for item in ${list//\\n/$cr}
do
echo "Item: $item"
done
Но это все слишком сложно, вам просто нужно пробел:
for item in ${list//\\n/ }
do
echo "Item: $item"
done
Ваша переменная $line
не содержит новых строк. Она содержит экземпляры \
, за которым следует n
. Вы можете четко это увидеть с помощью:
$ cat t.sh
#! /bin/bash
list="One\ntwo\nthree\nfour"
echo $list | hexdump -C
$ ./t.sh
00000000 4f 6e 65 5c 6e 74 77 6f 5c 6e 74 68 72 65 65 5c |One\ntwo\nthree\|
00000010 6e 66 6f 75 72 0a |nfour.|
00000016
Замена заменяет их пробелами, что достаточно для успешной работы в циклах for:
$ cat t.sh
#! /bin/bash
list="One\ntwo\nthree\nfour"
echo ${list//\\n/ } | hexdump -C
$ ./t.sh
00000000 4f 6e 65 20 74 77 6f 20 74 68 72 65 65 20 66 6f |One two three fo|
00000010 75 72 0a |ur.|
00000013
Демо:
$ cat t.sh
#! /bin/bash
list="One\ntwo\nthree\nfour"
echo ${list//\\n/ } | hexdump -C
for item in ${list//\\n/ } ; do
echo $item
done
$ ./t.sh
00000000 4f 6e 65 20 74 77 6f 20 74 68 72 65 65 20 66 6f |One two three fo|
00000010 75 72 0a |ur.|
00000013
One
two
three
four
Вы также можете сначала преобразовать переменную в массив, а затем пройтись по этому массиву.
lines="abc
def
ghi"
declare -a theArray
while read -r line
do
theArray+=("$line")
done <<< "$lines"
for line in "${theArray[@]}"
do
echo "$line"
#Здесь делайте что-то сложное, что может нарушить ваш цикл чтения
done
Это полезно только если вы не хотите возиться с IFS
и также сталкиваетесь с проблемами с командой read
, так как может произойти, если вы вызываете другой скрипт внутри цикла, который может очистить ваш буфер чтения, прежде чем вернуться, как это произошло со мной.
list="One\ntwo\nthree\nfour"
for line in $(echo ${list} | xargs -L1 echo)
do
echo ${line}
done
Создадим переменную с подстроками, разделенными новой строкой:
tmp=$'one\ntwo\nthree'
Обратите внимание на ведущий $
, чтобы фактически вставить управляющие символы новой строки, а не просто подстроки ‘\n’.
Итерация строки в for loop
может быть достигнута через развертывание без кавычек:
_IFS="$IFS"; IFS=$'\n'
for e in $tmp; do
printf "[$e]\n"
done
IFS="$_IFS"
Обратите внимание на отсутствие двойных кавычек вокруг $tmp
, чтобы расширить и разделить по управляющим символам новой строки. Окружение IFS
и его сброс должны предотвратить случайное разделение по пробелам или табуляциям, но это также может повлиять на команды внутри цикла.
Здесь, while loop
по расширенной, заключенной в кавычки Here String
может быть лучшим выбором, в зависимости от ввода. Уменьшение IFS
только до новых строк для этого цикла возможно, но не должно быть необходимым:
while IFS=$'\n' read -r; do
printf "[$REPLY]\n"
done <<<"$tmp"
Когда у нас есть массив (из элементов, разделенных новой строкой), его можно просто перебрать с помощью for
цикла, из заключенного в кавычки Parameter Expansion
:
tmpA=('1 cup' '2 plates' '3 bowls')
for e in "${tmpA[@]}"; do
printf "[$e]\n"
done
или с помощью while loop
:
while read -r; do
echo "[$REPLY]"
done <<<"${tmp5A[@]@E}"
Обратите внимание на использование @E
как оператора
для расширения массива Parameter Expansion
, чтобы также развернуть управляющие символы новой строки (и другие, если есть).
Ответ или решение
Чтобы правильно итерироваться по строкам в переменной в Bash, необходимо учитывать несколько аспектов, связанных с обработкой строк, содержащих разделители, такие как переносы строк. В данном ответе мы рассмотрим несколько методов, как это сделать, включая примеры и пояснения.
Способы итерации по строкам в переменной в Bash
1. Использование конструкции while read
Этот метод является наиболее безопасным и читаемым вариантом, особенно если вы работаете с многострочными переменными. Пример кода для этого подхода:
list=$'One\ntwo\nthree\nfour' # Обратите внимание на использование $'...' для интерпретации управляющих символов
while IFS= read -r line; do
echo "Элемент: $line"
done <<< "$list"
Здесь мы задаем переменную list с помощью $'...'
, что позволяет интерпретировать последовательности \n
как переносы строк. Затем используем конструкцию while
в сочетании с read
, чтения построчно.
2. Использование цикла for
с изменением IFS
Вы также можете использовать цикл for
, изменив значение переменной IFS
(Internal Field Separator) на символ переноса строки:
list=$'One\ntwo\nthree\nfour'
IFS=$'\n'
for item in $list; do
echo "Элемент: $item"
done
Обратите внимание, что этот метод делает возможным деление строки на подстроки, основываясь на новом значении IFS
, однако он может быть менее безопасным, если строка содержит пробелы или другие символы, которые могут вызвать нежелательное разделение.
3. Преобразование строки в массив
Если вы предпочитаете работать с массивом, вы можете сначала преобразовать строку в массив, а затем итерироваться по элементам массива:
list=$'One\ntwo\nthree\nfour'
IFS=$'\n' read -r -d '' -a array <<< "$list"
for item in "${array[@]}"; do
echo "Элемент: $item"
done
Здесь мы используем read -r -d ''
для считывания строки в массив.
4. Альтернативный метод с помощью xargs
Для выполнения циклов по выводам, вы также можете использовать xargs
:
list=$'One\ntwo\nthree\nfour'
echo -e "$list" | xargs -L1 echo
Этот метод позволяет обрабатывать вывод команд как строки, что может быть полезно для работы с большими объемами данных.
Заключение
Итерирование по строкам в переменной в Bash может быть выполнено различными способами в зависимости от контекста использования и требований к обработке данных. Наиболее безопасным и рекомендуемым методом является использование конструкции while read
, особенно при работе с многострочными переменными. Каждый метод имеет свои преимущества, и выбор подхода должен зависеть от конкретной задачи. Помните, что использование правильного метода помогает избежать ошибок и упрощает поддержку кода.