Як я можу читати рядок за рядком зі змінної в bash?


49

У мене є змінна, яка містить багаторядковий вихід команди. Який найефективніший спосіб прочитати вихідний рядок за рядком зі змінної?

Наприклад:

jobs="$(jobs)"
if [ "$jobs" ]; then
    # read lines from $jobs
fi

Відповіді:


65

Ви можете використовувати цикл час із заміною процесу:

while read -r line
do
    echo "$line"
done < <(jobs)

Оптимальним способом зчитування багаторядкової змінної є встановлення порожньої IFSзмінної та printfзмінної у з останнього нового рядка:

# Printf '%s\n' "$var" is necessary because printf '%s' "$var" on a
# variable that doesn't end with a newline then the while loop will
# completely miss the last line of the variable.
while IFS= read -r line
do
   echo "$line"
done < <(printf '%s\n' "$var")

Примітка. Відповідно до shellcheck sc2031 , використання технологічної підстанції є кращим, ніж труба, щоб уникнути [тонко] створення підзаголовка.

Крім того, врахуйте, що, називаючи змінну, jobsце може спричинити плутанину, оскільки це також назва загальної команди оболонки.


3
Якщо ви хочете зберегти весь пробіл, тоді використовуйте while IFS= read.... Якщо ви хочете запобігти \ інтерпретації, тоді використовуйтеread -r
Peter.O

Я виправив згадані пункти fred.bear, а також змінив echoна printf %sтак, щоб ваш сценарій працював навіть із неручним введенням.
Жил "ТАК - перестань бути злим"

Для читання з багаторядкової змінної перевагу є переважним, ніж розміщення з printf (див. Відповідь l0b0).
ata

1
@ata Хоча я чув про це "переважніше" досить часто, треба зазначити, що для встановлення Herestring завжди потрібний /tmpкаталог для запису, оскільки він покладається на можливість створення тимчасового робочого файлу. Якщо ви коли-небудь опинитесь у системі з обмеженим /tmpдоступом, лише для читання (і не змінена вами), ви будете раді можливості використання альтернативного рішення, наприклад, з printfтрубою.
синтаксис-помилка

1
У другому прикладі, якщо багаторядкова змінна не містить зворотного нового рядка, ви втратите останній елемент. Змініть його на:printf "%s\n" "$var" | while IFS= read -r line
Девід Х. Беннетт

26

Для обробки результатів командного рядка за рядком ( пояснення ):

jobs |
while IFS= read -r line; do
  process "$line"
done

Якщо у вас вже є дані в змінній:

printf %s "$foo" | 

printf %s "$foo"майже ідентичний echo "$foo", але друкується $fooбуквально, тоді як echo "$foo"може трактуватися $fooяк опція команди echo, якщо він починається з a -, а також може розширювати послідовності зворотної косої риси $fooв деяких оболонках.

Зауважте, що в деяких оболонках (ash, bash, pdksh, але не ksh або zsh) права частина трубопроводу працює в окремому процесі, тому будь-яка змінна, яку ви встановите в циклі, втрачається. Наприклад, наступний скрипт підрахунку рядків друкує 0 у цих оболонках:

n=0
printf %s "$foo" |
while IFS= read -r line; do
  n=$(($n + 1))
done
echo $n

Вирішення завдання полягає в тому, щоб залишити скрипт (або принаймні ту частину, яка потребує значення $nз циклу) в список команд:

n=0
printf %s "$foo" | {
  while IFS= read -r line; do
    n=$(($n + 1))
  done
  echo $n
}

Якщо дія на непорожніх рядках досить хороша, а введення не величезне, ви можете використовувати розділення слів:

IFS='
'
set -f
for line in $(jobs); do
  # process line
done
set +f
unset IFS

Пояснення: встановлення IFSна один новий рядок призводить до того, що розбиття слів відбувається лише в нових рядках (на відміну від будь-якого символу пробілу в налаштуваннях за замовчуванням). set -fвимикає глобалізацію (тобто розширення підстановки), що в іншому випадку стане результатом заміни команди $(jobs)або змінної субститутіно $foo. forПетля діє на всі частини $(jobs), які є всі непусті рядки в вихідних даних команди. Нарешті, відновіть глобулінг та IFSналаштування.


У мене виникли проблеми з налаштуванням IFS та знеструмленням IFS. Я думаю, що правильно зробити це - зберегти старе значення IFS і встановити IFS назад до цього старого значення. Я не фахівець з башти, але, з мого досвіду, це повертає вас до початкової поведінки.
Бйорн Рош

1
@BjornRoche: всередині функції використовуйте local IFS=something. Це не вплине на значення глобального масштабу. IIRC, unset IFSне повертає вас до замовчування (і, звичайно, не працює, якщо раніше не було типовим).
Пітер Кордес

14

Проблема: якщо ви використовуєте цикл, він запускається в нижній частині, і всі змінні будуть втрачені. Рішення: використовувати для циклу

# change delimiter (IFS) to new line.
IFS_BAK=$IFS
IFS=$'\n'

for line in $variableWithSeveralLines; do
 echo "$line"

 # return IFS back if you need to split new line by spaces:
 IFS=$IFS_BAK
 IFS_BAK=
 lineConvertedToArraySplittedBySpaces=( $line )
 echo "{lineConvertedToArraySplittedBySpaces[0]}"
 # return IFS back to newline for "for" loop
 IFS_BAK=$IFS
 IFS=$'\n'

done 

# return delimiter to previous value
IFS=$IFS_BAK
IFS_BAK=

ДУЖЕ ДЯКУЮ!! Всі перераховані вище рішення для мене не вдалися.
hax0r_n_code

перекочування в while readцикл в bash означає, що цикл while знаходиться в нижній частині, тому змінні не є глобальними. while read;do ;done <<< "$var"робить тіло петлі не підгруппою. (Останній bash має можливість помістити тіло cmd | whileпетлі не в нижню частину корпусу , як завжди було у ksh.)
Пітер Кордес,


10

В останніх версіях bash використовуйте mapfileабо readarrayефективно читайте вихід команди в масиви

$ readarray test < <(ls -ltrR)
$ echo ${#test[@]}
6305

Відмова: жахливий приклад, але ви можете придумати кращу команду для використання, ніж ls себе


Це приємний спосіб, але літер / var / tmp з тимчасовими файлами в моїй системі. +1 у будь-якому випадку
Євген Ярмаш

@eugene: це смішно. Яка система (distro / OS) увімкнена?
sehe

Це FreeBSD 8. Як відтворити: ввести readarrayфункцію і викликати функцію кілька разів.
Євген Ярмаш

Гарний, @sehe. +1
Teresa e Junior

7
jobs="$(jobs)"
while IFS= read
do
    echo $REPLY
done <<< "$jobs"

Список літератури:


3
-rтакож гарна ідея; Це заважає \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(що важливо, щоб не втратити пробіли)
Peter.O

-1

Ви можете використовувати <<< для простого зчитування зі змінної, що містить дані, розділені на новий рядок:

while read -r line
do 
  echo "A line of input: $line"
done <<<"$lines"

1
Ласкаво просимо до Unix & Linux! Це, по суті, дублює відповідь чотири роки тому. Будь ласка, не публікуйте відповіді, якщо у вас є щось нове, щоб зробити свій внесок.
G-Man каже: "Відновіть Моніку"
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.