Как пройтись по строкам в переменной в Bash?

Вопрос или проблема

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

Оцените материал
Добавить комментарий

Капча загружается...