Чтение вывода команды в массив в Bash

111

Мне нужно прочитать вывод команды в моем скрипте в массив. Например, команда:

ps aux | grep | grep | x 

и выводит строку за строкой вот так:

10
20
30

Мне нужно прочитать значения из вывода команды в массив, а затем я выполню некоторую работу, если размер массива меньше трех.

барп
источник
5
Привет, @barp, ОТВЕТЬТЕ НА ВАШИ ВОПРОСЫ, чтобы ваш тип письма не истощал все сообщество.
Джеймс
9
@ Джеймс, проблема не в том, что он не отвечает на свой вопрос ... это сайт вопросов и ответов. Он просто не пометил их как отвеченные. Он должен их отметить. Подсказка. @ barp
DDPWNAGE
4
Пожалуйста, @barp, отметьте вопрос как ответ.
smonff
Связано: Цикл по содержимому файла в Bash, поскольку чтение вывода команды посредством подстановки процесса аналогично чтению из файла.
codeforester

Ответы:

162

Другие ответы сломается , если вывод команды содержит пробелы (которые довольно часто) или шарик символы , такие как *, ?, [...].

Чтобы получить вывод команды в массиве с одной строкой на элемент, есть три способа:

  1. При использовании Bash≥4 mapfile- это наиболее эффективно:

    mapfile -t my_array < <( my_command )
  2. В противном случае цикл чтения вывода (более медленный, но безопасный):

    my_array=()
    while IFS= read -r line; do
        my_array+=( "$line" )
    done < <( my_command )
  3. Как предложил Чарльз Даффи в комментариях (спасибо!), Следующий метод может работать лучше, чем метод цикла в номере 2:

    IFS=$'\n' read -r -d '' -a my_array < <( my_command && printf '\0' )

    Убедитесь, что вы используете именно эту форму, т.е. убедитесь, что у вас есть следующее:

    • IFS=$'\n' в той же строке, что и readоператор: это установит только переменную среды IFS для readоператора. Так что это никак не повлияет на остальную часть вашего скрипта. Назначение этой переменной - сообщить readо прерывании потока по символу EOL \n.
    • -r: это важно. Он говорит read не интерпретировать обратную косую черту как escape-последовательности.
    • -d '': обратите внимание на пробел между -dпараметром и его аргументом ''. Если вы не оставите здесь пробел, ''он никогда не будет виден, поскольку он исчезнет на этапе удаления цитаты, когда Bash проанализирует оператор. Это говорит readо прекращении чтения на нулевом байте. Некоторые люди пишут это как -d $'\0', но на самом деле это не обязательно. -d ''лучше.
    • -a my_arrayсообщает readзаполнить массив my_arrayпри чтении потока.
    • Вы должны использовать printf '\0'оператор после my_command , чтобы readвернуть 0; на самом деле это не имеет большого значения, если вы этого не сделаете (вы просто получите код возврата 1, что нормально, если вы не используете set -e- что вам все равно не следует), но просто имейте это в виду. Он чище и семантически корректнее. Обратите внимание, что это отличается от printf '', который ничего не выводит. printf '\0'печатает нулевой байт, необходимый readдля благополучного прекращения чтения (помните -d ''опцию?).

Если вы можете, т.е. если вы уверены, что ваш код будет работать на Bash≥4, используйте первый метод. Вы также можете видеть, что он короче.

Если вы хотите использовать read, цикл (метод 2) может иметь преимущество перед методом 3, если вы хотите выполнять некоторую обработку по мере чтения строк: у вас есть прямой доступ к нему (через $lineпеременную в приведенном мной примере) и у вас также есть доступ к уже прочитанным строкам (через массив ${my_array[@]}в приведенном мной примере).

Обратите внимание, что это mapfileдает возможность иметь функцию eval'd обратного вызова при чтении каждой строки, и фактически вы даже можете указать ему вызывать этот обратный вызов только каждые N прочитанных строк; посмотрите help mapfileи варианты -Cи -cтам. (Я считаю, что это немного неуклюже, но иногда его можно использовать, если у вас есть только простые дела - я вообще не понимаю, почему это вообще было реализовано!).


Теперь я расскажу, почему такой метод:

my_array=( $( my_command) )

не работает, когда есть пробелы:

$ # I'm using this command to test:
$ echo "one two"; echo "three four"
one two
three four
$ # Now I'm going to use the broken method:
$ my_array=( $( echo "one two"; echo "three four" ) )
$ declare -p my_array
declare -a my_array='([0]="one" [1]="two" [2]="three" [3]="four")'
$ # As you can see, the fields are not the lines
$
$ # Now look at the correct method:
$ mapfile -t my_array < <(echo "one two"; echo "three four")
$ declare -p my_array
declare -a my_array='([0]="one two" [1]="three four")'
$ # Good!

Тогда некоторые люди порекомендуют IFS=$'\n'исправить это с помощью:

$ IFS=$'\n'
$ my_array=( $(echo "one two"; echo "three four") )
$ declare -p my_array
declare -a my_array='([0]="one two" [1]="three four")'
$ # It works!

Но теперь давайте воспользуемся другой командой с глобусами :

$ echo "* one two"; echo "[three four]"
* one two
[three four]
$ IFS=$'\n'
$ my_array=( $(echo "* one two"; echo "[three four]") )
$ declare -p my_array
declare -a my_array='([0]="* one two" [1]="t")'
$ # What?

Это потому, что у меня есть файл, который называется tв текущем каталоге ... и это имя файла совпадает с глобусом [three four] ... на этом этапе некоторые люди рекомендуют использовать set -fдля отключения глобализации: но посмотрите на это: вы должны изменить IFSи использовать, set -fчтобы иметь возможность исправить сломанная техника (а вы ее даже не исправляете)! при этом мы действительно сражаемся с оболочкой, а не с оболочкой .

$ mapfile -t my_array < <( echo "* one two"; echo "[three four]")
$ declare -p my_array
declare -a my_array='([0]="* one two" [1]="[three four]")'

здесь мы работаем с оболочкой!

gniourf_gniourf
источник
4
Это здорово, о mapfileчем я раньше не слышал , это именно то, чего мне не хватало годами. Я думаю, что в последних версиях bashесть так много хороших новых функций, что мне просто нужно потратить несколько дней на чтение документации и составление хорошей шпаргалки.
Гена Павловский
6
Кстати, чтобы использовать этот синтаксис < <(command)в сценариях оболочки, строка shebang должна быть #!/bin/bash- если запускать от имени #!/bin/sh, bash выйдет с синтаксической ошибкой.
Гена Павловский
1
Продолжая полезное примечание @ GenePavlovsky, скрипт также должен запускаться с помощью команды bash, bash my_script.shа не команды shsh my_script.sh
Вито
2
@Vito: действительно, этот ответ предназначен только для Bash, но это не должно быть проблемой, поскольку строго совместимые оболочки POSIX даже не реализуют массивы ( shи dashвообще не знают о массивах, кроме, конечно, для $@массив позиционных параметров ).
gniourf_gniourf
3
В качестве другой альтернативы, которая не требует bash 4.0, рассмотрим IFS=$'\n' read -r -d '' -a my_array < <(my_command && printf '\0')- она ​​работает правильно в bash 3.x, а также проходит через статус неудачного выхода из my_commandв read.
Чарльз Даффи
86

Ты можешь использовать

my_array=( $(<command>) )

для сохранения вывода команды <command>в массив my_array.

Вы можете получить доступ к длине этого массива, используя

my_array_length=${#my_array[@]}

Теперь длина сохраняется в my_array_length.

Михаэль Шлоттке-Лакемпер
источник
19
Что, если в выводе $ (command) есть пробелы и несколько строк с пробелами? Я добавил «$ (command)», и он помещает весь вывод из всех строк в первый [0] элемент массива.
ikwyl6
3
@ ikwyl6 обходной путь - это присвоение вывода команды переменной, а затем создание с ней массива или добавление его в массив. VAR="$(<command>)"а потом my_array=("$VAR")илиmy_array+=("$VAR")
Вито
10

Представьте, что вы собираетесь поместить имена файлов и каталогов (в текущей папке) в массив и подсчитать его элементы. Сценарий был бы похож;

my_array=( `ls` )
my_array_length=${#my_array[@]}
echo $my_array_length

Или вы можете перебрать этот массив, добавив следующий скрипт:

for element in "${my_array[@]}"
do
   echo "${element}"
done

Обратите внимание, что это основная концепция, и ввод считается очищенным раньше, т.е. удалением лишних символов, обработкой пустых строк и т. Д. (Что не входит в тему этой темы).

Юнесс
источник
3
Ужасная идея по причинам, указанным в ответе выше
Hubert Grzeskowiak