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командою правильно друкує кожен рядок один за одним. Однак перший для циклу друкує всі елементи в одному рядку. Будь-які ідеї?


Просто коментар до всіх відповідей: мені довелося зробити $ (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"
киб

5
У цього підходу є і недоліки, оскільки цикл while буде працювати в нижній частині корпусу: будь-які змінні, призначені в циклі, не зберігатимуться.
Гленн Джекман

66

Ви можете використовувати while+ read:

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

Btw. -eможливість echoнестандартно. Використовуйте printfнатомість, якщо ви хочете портативність.


12
Зауважте, що якщо ви використовуєте цей синтаксис, змінні, призначені всередині циклу, не залишаться після циклу. Як не дивно, <<<версія, запропонована glenn jackman , працює з змінним призначенням.
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
Це виводить усі елементи, а не рядки.
Tomasz Posłuszny

4
@ TomaszPosłuszny Ні, це виводить 3 (кількість 3) рядків на моїй машині. Зверніть увагу на налаштування IFS. Ви також можете використовувати IFS=$'\n'для того ж ефекту.
jmiserez

11

Ось смішний спосіб зробити ваш цикл:

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: оновив мою відповідь - і з більш простою версією :-)
Мат

Я спробував це, і, здається, не працює, якщо у вашому вкладі є пробіли. У наведеному вище прикладі у нас є "один \ ntwo \ nthree", і це працює, але це не вдається, якщо у нас є "запис один \ nentry два \ nentry три", оскільки він також додає новий рядок для простору.
Джон Роша

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.