Odczytywanie danych wyjściowych polecenia do tablicy w Bash

111

Muszę odczytać dane wyjściowe polecenia w moim skrypcie do tablicy. Polecenie to na przykład:

ps aux | grep | grep | x 

i daje wynik wiersz po wierszu w następujący sposób:

10
20
30

Muszę odczytać wartości z danych wyjściowych polecenia do tablicy, a następnie wykonam trochę pracy, jeśli rozmiar tablicy jest mniejszy niż trzy.

barp
źródło
5
Hej @barp, ODPOWIEDZ NA PYTANIA, aby Twój typ nie osłabił całej społeczności.
James,
9
@James problem nie polega na tym, że nie odpowiada na swoje pytanie ... to jest strona z pytaniami i odpowiedziami. Po prostu nie oznaczył ich jako odpowiedzi. Powinien je oznaczyć. Wskazówka. @ barp
DDPWNAGE
4
Proszę @barp, oznacz pytanie jako odpowiedź.
smonff
Powiązane: Zapętlanie się zawartości pliku w Bash, ponieważ odczyt wyniku polecenia przez podstawianie procesu jest podobny do odczytu z pliku.
codeforester

Odpowiedzi:

162

Pozostałe odpowiedzi pęknie jeśli wyjście komendy zawiera spacje (co jest dość częste) lub znaki jak glob *, ?, [...].

Aby uzyskać wynik polecenia w tablicy, z jednym wierszem na element, istnieją zasadniczo 3 sposoby:

  1. Z Bash≥4 użyj mapfile- jest najbardziej wydajny:

    mapfile -t my_array < <( my_command )
  2. W przeciwnym razie pętla odczytująca wyjście (wolniej, ale bezpiecznie):

    my_array=()
    while IFS= read -r line; do
        my_array+=( "$line" )
    done < <( my_command )
  3. Jak zasugerował Charles Duffy w komentarzach (dzięki!), Poniższe mogą działać lepiej niż metoda pętli w numerze 2:

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

    Upewnij się, że używasz dokładnie tego formularza, tj. Upewnij się, że masz następujące:

    • IFS=$'\n' w tym samym wierszu co readinstrukcja: spowoduje to ustawienie zmiennej środowiskowej tylko IFS dla readinstrukcji. Więc w ogóle nie wpłynie to na resztę twojego skryptu. Celem tej zmiennej jest nakazanie readprzerwania strumienia przy znaku EOL \n.
    • -r: To jest ważne. Mówi, read aby nie interpretować odwrotnych ukośników jako sekwencji ucieczki.
    • -d '': zwróć uwagę na spację między -dopcją a jej argumentem ''. Jeśli nie zostawisz tutaj spacji, ''nigdy nie będzie widoczne, ponieważ zniknie w kroku usuwania cytatu, gdy Bash przeanalizuje instrukcję. To mówi, readaby przestać czytać na bajcie zerowym. Niektórzy piszą to jako -d $'\0', ale tak naprawdę nie jest to konieczne. -d ''jest lepiej.
    • -a my_arraymówi, readaby wypełnić tablicęmy_array podczas czytania strumienia.
    • Musisz użyć printf '\0'instrukcji później my_command , aby to readpowróciło 0; to właściwie nie jest wielka sprawa, jeśli tego nie zrobisz (otrzymasz po prostu kod powrotu 1, co jest w porządku, jeśli nie używasz set -e- czego i tak nie powinieneś), ale po prostu miej to na uwadze. Jest czystszy i bardziej poprawny semantycznie. Zauważ, że różni się to od printf '', które nic nie wyświetla. printf '\0'wypisuje bajt zerowy, potrzebny, readaby szczęśliwie przestał tam czytać (pamiętasz -d ''opcję?).

Jeśli możesz, tj. Jeśli masz pewność, że Twój kod będzie działał na Bash≥4, użyj pierwszej metody. Widać też, że jest krótszy.

Jeśli chcesz użyć read, pętla (metoda 2) może mieć przewagę nad metodą 3, jeśli chcesz wykonać pewne przetwarzanie podczas odczytywania wierszy: masz do niej bezpośredni dostęp (poprzez $linezmienną z podanego przykładu) i masz również dostęp do już przeczytanych wierszy (poprzez tablicę ${my_array[@]}z przykładu, który podałem).

Zauważ, że mapfileumożliwia to wywołanie zwrotne w każdej przeczytanej linii, aw rzeczywistości możesz nawet powiedzieć mu, aby wywoływał to wywołanie zwrotne tylko po przeczytaniu N linii; spójrz na help mapfilei opcje -Ci -ctam. (Moja opinia na ten temat jest taka, że ​​jest to trochę niezgrabne, ale czasami może być używane, jeśli masz tylko proste rzeczy do zrobienia - nie bardzo rozumiem, dlaczego zostało to wdrożone!).


Teraz powiem ci, dlaczego następująca metoda:

my_array=( $( my_command) )

jest uszkodzony, gdy są spacje:

$ # 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!

Wtedy niektórzy ludzie zalecą użycie, IFS=$'\n'aby to naprawić:

$ 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!

Ale teraz użyjmy innego polecenia, z globami :

$ 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?

Dzieje się tak, ponieważ mam plik o nazwie tw bieżącym katalogu… a ta nazwa pliku jest dopasowana przez glob [three four] … w tym momencie niektórzy ludzie zalecaliby użycie set -fdo wyłączenia globbingu: ale spójrz na to: musisz zmienić IFSi użyć, set -faby móc naprawić zepsuta technika (a nawet tego nie naprawiasz)! robiąc to, naprawdę walczymy z powłoką, a nie z nią .

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

tutaj pracujemy z powłoką!

gniourf_gniourf
źródło
4
To jest świetne, nigdy wcześniej o tym nie słyszałem mapfile, właśnie tego brakowało mi od lat. Wydaje mi się, że ostatnie wersje bashmają tak wiele fajnych nowych funkcji, że powinienem poświęcić kilka dni na przeczytanie dokumentacji i spisanie fajnej ściągawki.
Gene Pavlovsky
6
Przy okazji, aby użyć tej składni < <(command)w skryptach powłoki, wiersz shebang powinien wyglądać tak #!/bin/bash- jeśli zostanie uruchomiony jako #!/bin/sh, bash zakończy działanie z błędem składni.
Gene Pavlovsky
1
Rozwijając pomocną notatkę @ GenePavlovsky, skrypt musi być również uruchamiany z poleceniem bash, bash my_script.sha nie z poleceniem shsh my_script.sh
Vito
2
@Vito: rzeczywiście, ta odpowiedź dotyczy tylko Basha, ale nie powinno to stanowić problemu, ponieważ powłoki ściśle zgodne z POSIX nawet nie implementują tablic ( shi dashnie wiedzą w ogóle o tablicach, z wyjątkiem, oczywiście, $@tablica parametrów pozycyjnych ).
gniourf_gniourf
3
Jako inną alternatywę, która nie wymaga basha 4.0, rozważ IFS=$'\n' read -r -d '' -a my_array < <(my_command && printf '\0')- zarówno działa poprawnie w bash 3.x, jak i przechodzi przez nieudany kod wyjścia z my_commanddo read.
Charles Duffy
86

Możesz użyć

my_array=( $(<command>) )

do przechowywania danych wyjściowych polecenia <command>w tablicy my_array.

Możesz uzyskać dostęp do długości tej tablicy za pomocą

my_array_length=${#my_array[@]}

Teraz długość jest przechowywana w my_array_length.

Michael Schlottke-Lakemper
źródło
19
A co, jeśli wynik polecenia $ (polecenie) zawiera spacje i wiele wierszy ze spacjami? Dodałem „$ (polecenie)” i umieszcza wszystkie dane wyjściowe ze wszystkich linii w pierwszym [0] elemencie tablicy.
ikwyl6
3
@ ikwyl6 obejściem jest przypisanie wyniku polecenia do zmiennej, a następnie utworzenie z nim tablicy lub dodanie jej do tablicy. VAR="$(<command>)"a potem my_array=("$VAR")lubmy_array+=("$VAR")
Vito
10

Wyobraź sobie, że zamierzasz umieścić pliki i nazwy katalogów (w bieżącym folderze) w tablicy i policzyć jej elementy. Scenariusz byłby taki;

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

Lub możesz iterować po tej tablicy, dodając następujący skrypt:

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

Zwróć uwagę, że jest to podstawowa koncepcja i uważa się, że dane wejściowe zostały wcześniej oczyszczone, tj. Usunięcie dodatkowych znaków, obsługa pustych ciągów itd. (Co jest poza tematem tego wątku).

Youness
źródło
3
Okropny pomysł z powodów wymienionych w powyższej odpowiedzi
Hubert Grześkowiak