Как я могу читать построчно из переменной в Bash?

49

У меня есть переменная, которая содержит многострочный вывод команды. Какой самый эффективный способ читать вывод построчно из переменной?

Например:

jobs="$(jobs)"
if [ "$jobs" ]; then
    # read lines from $jobs
fi
Евгений Ярмаш
источник

Ответы:

65

Вы можете использовать цикл while с подстановкой процесса:

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")

Примечание. В соответствии с проверкой оболочки sc2031 использование подстановки процесса предпочтительнее, чем конвейер, чтобы избежать [тонкого] создания подоболочки.

Также, пожалуйста, поймите, что присвоение имени переменной jobsможет вызвать путаницу, так как это также имя обычной команды оболочки.

кендырь
источник
3
Если вы хотите сохранить все свои пробелы, тогда используйте while IFS= read.... Если вы хотите предотвратить \ интерпретацию, тогда используйтеread -r
Peter.O
Я исправил точки, упомянутые в fred.bear, а также изменил их echoна printf %sтак, чтобы ваш сценарий работал даже при нестандартном вводе.
Жиль "ТАК - перестань быть злым"
Чтобы читать из многострочной переменной, строка отсчета предпочтительнее, чем конвейер из printf (см. Ответ l0b0).
ата
1
@ata Хотя я слышал об этом «предпочтительном» достаточно часто, следует отметить, что для этой строки всегда требуется, чтобы /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)или переменной substitutino $foo. forПетля действует на все части $(jobs), которые являются все непустые строки в выходных данных команды. Наконец-то восстановим сглаживание и IFSнастройки.

Жиль "ТАК - перестань быть злым"
источник
У меня были проблемы с настройкой IFS и отключением IFS. Я думаю, что правильно сделать, это сохранить старое значение IFS и установить IFS обратно на это старое значение. Я не эксперт по Bash, но по моему опыту, это возвращает вас к первоначальному поведению.
Бьорн Рош
1
@BjornRoche: внутри функции используйте local IFS=something. Это не повлияет на значение глобальной области видимости. IIRC, unset IFSне возвращает вас к значениям по умолчанию (и, конечно, не работает, если это не было заранее задано по умолчанию).
Питер Кордес
14

Проблема: если вы используете цикл while, он будет работать в подоболочке и все переменные будут потеряны. Решение: использовать для цикла

# 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.)
Питер Кордес
Также см. Этот связанный пост .
Wildcard
10

В последних версиях bash используйте mapfileили readarrayдля эффективного чтения вывода команд в массивы

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

Отказ от ответственности: ужасный пример, но вы можете придумать лучшую команду для использования, чем вы сами

sehe
источник
Это хороший способ, но он засоряет / var / tmp временными файлами в моей системе. +1 все равно
Евгений Ярмаш
@ Евгений: это смешно. Какая система (дистрибутив / ОС) включена?
Сехе
Это FreeBSD 8. Как воспроизводить: вставьте readarrayфункцию и вызовите функцию несколько раз.
Евгений Ярмаш
Хороший, @sehe. +1
Teresa e Junior
7
jobs="$(jobs)"
while IFS= read
do
    echo $REPLY
done <<< "$jobs"

Рекомендации:

l0b0
источник
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 говорит: «Восстановите Монику»