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вместо этого, если вы хотите переносимость.


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командой, как это может случиться, если вы вызовете другой скрипт внутри цикла, чтобы этот скрипт мог очистить буфер чтения перед возвратом, как это случилось со мной. ,

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.