Bash: перебор строк в переменной

225

Как правильно перебирать строки в bash либо в переменной, либо из вывода команды? Простая установка переменной IFS для новой строки работает для вывода команды, но не для обработки переменной, содержащей новые строки.

Например

#!/bin/bash

list="One\ntwo\nthree\nfour"

#Print the list with echo
echo -e "echo: \n$list"

#Set the field separator to new line
IFS=$'\n'

#Try to iterate over each line
echo "For loop:"
for item in $list
do
        echo "Item: $item"
done

#Output the variable to a file
echo -e $list > list.txt

#Try to iterate over each line from the cat command
echo "For loop over command output:"
for item in `cat list.txt`
do
        echo "Item: $item"
done

Это дает вывод:

echo: 
One
two
three
four
For loop:
Item: One\ntwo\nthree\nfour
For loop over command output:
Item: One
Item: two
Item: three
Item: four

Как видите, при выводе переменной или итерации по catкоманде каждая строка выводится правильно. Однако первый цикл for печатает все элементы в одной строке. Есть идеи?

Алекс Сперлинг
источник
Просто комментарий ко всем ответам: мне нужно было сделать $ (echo "$ line" | sed -e 's / ^ [[: space:]] * //'), чтобы обрезать символ новой строки.
servermanfail

Ответы:

290

В bash, если вы хотите встроить строки в строку, заключите строку в $'':

$ list="One\ntwo\nthree\nfour"
$ echo "$list"
One\ntwo\nthree\nfour
$ list=$'One\ntwo\nthree\nfour'
$ echo "$list"
One
two
three
four

И если у вас уже есть такая строка в переменной, вы можете читать ее построчно с помощью:

while read -r line; do
    echo "... $line ..."
done <<< "$list"
Гленн Джекман
источник
30
Просто обратите внимание, что это done <<< "$list"имеет решающее значение
Джейсон Аксельсон
21
Причина done <<< "$list"является решающей, потому что это передаст «$ list» в качестве входных данныхread
wisbucky
22
Я должен подчеркнуть это: двойное цитирование $listочень важно.
Андре Шалелла
8
echo "$list" | while ...может показаться более ясным, чем... done <<< "$line"
kyb
5
У этого подхода есть свои недостатки, поскольку цикл while будет выполняться в подоболочке: любые переменные, назначенные в цикле, не будут сохраняться.
Гленн Джекман
66

Вы можете использовать while+ read:

some_command | while read line ; do
   echo === $line ===
done

Btw. -eвозможность echoнестандартно. Используйте printfвместо этого, если вы хотите переносимость.

maxelost
источник
12
Обратите внимание, что если вы используете этот синтаксис, переменные, назначенные внутри цикла, не будут придерживаться цикла. Как ни странно, <<<версия, предложенная Гленном Джекманом , работает с присвоением переменных.
Sparhawk
7
@Sparhawk Да, это потому, что канал запускает подоболочку, выполняющую whileчасть. <<<Версия не дает (в новых версиях Баша, по крайней мере).
maxelost
26
#!/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
никто не важен
источник
2
Это выводит все элементы, а не строки.
Томаш Послушный
4
@ TomaszPosłuszny Нет, это выводит 3 (количество 3) строки на моем компьютере. Обратите внимание на настройку IFS. Вы также можете использовать IFS=$'\n'для того же эффекта.
jmiserez
11

Вот забавный способ сделать цикл 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

Подстановка заменяет те пробелами, которых достаточно для работы циклов:

$ 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
Мат
источник
Интересно, вы можете объяснить, что здесь происходит? Похоже, вы заменяете \ n новой строкой ... В чем разница между исходной строкой и новой?
Алекс Сперлинг
@Alex: обновил мой ответ - тоже с более простой версией :-)
Mat
Я попробовал это, и это не работает, если у вас есть пробелы на входе. В приведенном выше примере у нас есть "one \ ntwo \ nthree", и это работает, но это не работает, если у нас есть "entry one \ nentry two \ nentry three", поскольку это также добавляет новую строку для пространства.
Джон Роча
2
Просто обратите внимание, что вместо определения crвы можете использовать $'\n'.
devios1
1

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

lines="abc
def
ghi"

declare -a theArray

while read -r line
do
    theArray+=($line)            
done <<< "$lines"

for line in "${theArray[@]}"
do
    echo "$line"
    #Do something complex here that would break your read loop
done

Это полезно только в том случае, если вы не хотите связываться с командой, IFSа также возникают проблемы с readкомандой, как это может случиться, если вы вызовете другой скрипт внутри цикла, чтобы этот скрипт мог очистить буфер чтения перед возвратом, как это случилось со мной. ,

Torge
источник