Membaca output dari sebuah perintah ke dalam array di Bash

111

Saya perlu membaca output dari sebuah perintah dalam skrip saya menjadi sebuah array. Perintahnya adalah, misalnya:

ps aux | grep | grep | x 

dan memberikan output baris demi baris seperti ini:

10
20
30

Saya perlu membaca nilai dari output perintah ke dalam array, dan kemudian saya akan melakukan beberapa pekerjaan jika ukuran array kurang dari tiga.

barp
sumber
5
Hai @barp, JAWAB PERTANYAAN ANDA, jangan sampai tipe Anda menguras seluruh komunitas.
James
9
@James Masalahnya bukan dengan fakta bahwa dia tidak menjawab pertanyaannya ... ini adalah situs Tanya Jawab. Dia hanya tidak menandai mereka sebagai terjawab. Dia harus menandai mereka. Petunjuk. @ barp
DDPWNAGE
4
Silakan @barp, tandai pertanyaan sebagai sudah dijawab.
smonff
Terkait: Perulangan melalui konten file di Bash karena membaca output dari perintah melalui substitusi proses mirip dengan membaca dari file.
codeforester

Jawaban:

161

Jawaban yang lain akan pecah jika output dari perintah mengandung spasi (yang agak sering) atau gumpal karakter seperti *, ?, [...].

Untuk mendapatkan output dari sebuah perintah dalam sebuah array, dengan satu baris per elemen, pada dasarnya ada 3 cara:

  1. Dengan penggunaan Bash≥4 mapfile— yang paling efisien:

    mapfile -t my_array < <( my_command )
  2. Jika tidak, loop membaca output (lebih lambat, tapi aman):

    my_array=()
    while IFS= read -r line; do
        my_array+=( "$line" )
    done < <( my_command )
    
  3. Seperti yang disarankan oleh Charles Duffy di komentar (terima kasih!), Berikut ini mungkin bekerja lebih baik daripada metode loop di nomor 2:

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

    Harap pastikan Anda menggunakan formulir ini persis, yaitu, pastikan Anda memiliki yang berikut:

    • IFS=$'\n' pada baris yang sama dengan readpernyataan: ini hanya akan menyetel variabel lingkungan IFS untuk readpernyataan saja. Jadi itu tidak akan memengaruhi seluruh skrip Anda sama sekali. Tujuan dari variabel ini adalah untuk memberitahu readuntuk menghentikan aliran pada karakter EOL \n.
    • -r: ini penting. Ini memberitahu read untuk tidak menafsirkan garis miring terbalik sebagai urutan pelarian.
    • -d '': harap perhatikan jarak antara -dopsi dan argumennya ''. Jika Anda tidak meninggalkan spasi di sini, maka ''tidak akan pernah terlihat, karena akan hilang pada langkah penghapusan kutipan saat Bash mengurai pernyataan tersebut. Ini memberitahu readuntuk berhenti membaca pada byte nol. Beberapa orang menulisnya sebagai -d $'\0', tetapi sebenarnya tidak terlalu perlu. -d ''lebih baik.
    • -a my_arraymemberitahu readuntuk mengisi larik my_arraysaat membaca aliran.
    • Anda harus menggunakan printf '\0'pernyataan setelah my_command , sehingga readmengembalikan 0; sebenarnya bukan masalah besar jika Anda tidak melakukannya (Anda hanya akan mendapatkan kode pengembalian 1, yang tidak masalah jika Anda tidak menggunakannya set -e- yang seharusnya tidak Anda gunakan ), tetapi ingatlah itu. Ini lebih bersih dan lebih tepat secara semantik. Perhatikan bahwa ini berbeda dari printf '', yang tidak menghasilkan apa pun. printf '\0'mencetak byte nol, diperlukan oleh readuntuk dengan senang hati berhenti membaca di sana (ingat -d ''opsinya?).

Jika Anda bisa, misalnya, jika Anda yakin kode Anda akan berjalan di Bash≥4, gunakan metode pertama. Dan Anda juga bisa melihatnya lebih pendek.

Jika Anda ingin menggunakan read, loop (metode 2) mungkin memiliki keunggulan dibandingkan metode 3 jika Anda ingin melakukan beberapa pemrosesan saat baris dibaca: Anda memiliki akses langsung ke sana (melalui $linevariabel dalam contoh yang saya berikan), dan Anda juga memiliki akses ke baris yang sudah dibaca (melalui array ${my_array[@]}dalam contoh yang saya berikan).

Perhatikan bahwa mapfilemenyediakan cara agar callback mengevaluasi pada setiap baris yang dibaca, dan pada kenyataannya Anda bahkan dapat memberitahukannya untuk hanya memanggil callback ini setiap baris N yang dibaca; lihat help mapfiledan opsi -Cdan di -cdalamnya. (Pendapat saya tentang ini adalah bahwa ini sedikit kikuk, tetapi terkadang dapat digunakan jika Anda hanya memiliki hal-hal sederhana yang harus dilakukan - Saya tidak begitu mengerti mengapa ini bahkan diterapkan di tempat pertama!).


Sekarang saya akan memberi tahu Anda mengapa metode berikut:

my_array=( $( my_command) )

rusak jika ada spasi:

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

Kemudian beberapa orang kemudian akan merekomendasikan penggunaan IFS=$'\n'untuk memperbaikinya:

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

Tapi sekarang mari kita gunakan perintah lain, dengan globs :

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

Itu karena saya memiliki file bernama tdi direktori saat ini ... dan nama file ini cocok dengan glob [three four] ... pada titik ini beberapa orang akan merekomendasikan penggunaan set -funtuk menonaktifkan globbing: tetapi lihatlah: Anda harus mengubah IFSdan menggunakan set -funtuk dapat memperbaiki sebuah teknik rusak (dan Anda bahkan tidak benar-benar memperbaikinya)! ketika melakukan itu kita benar-benar melawan shell, bukan bekerja dengan shell .

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

di sini kami bekerja dengan cangkangnya!

gniourf_gniourf
sumber
4
Ini luar biasa, saya tidak pernah mendengarnya mapfilesebelumnya, itulah yang telah saya lewatkan selama bertahun-tahun. Saya rasa versi terbaru bashmemiliki begitu banyak fitur baru yang bagus, saya hanya perlu menghabiskan beberapa hari membaca dokumen dan menuliskan lembar contekan yang bagus.
Gene Pavlovsky
6
Btw, untuk menggunakan sintaks ini < <(command)dalam skrip shell, baris shebang harus #!/bin/bash- jika dijalankan sebagai #!/bin/sh, bash akan keluar dengan kesalahan sintaks.
Gene Pavlovsky
1
Memperluas catatan bermanfaat @ GenePavlovsky, skrip juga harus dijalankan dengan perintah bash bash my_script.shdan bukan perintah shsh my_script.sh
Vito
2
@Vito: memang, jawaban ini hanya untuk Bash, tetapi ini seharusnya tidak menjadi masalah, karena shell POSIX yang sangat patuh bahkan tidak mengimplementasikan array ( shdan dashtidak tahu tentang array sama sekali, kecuali, tentu saja, untuk $@array parameter posisi ).
gniourf_gniourf
3
Sebagai alternatif lain yang tidak memerlukan bash 4.0, pertimbangkan IFS=$'\n' read -r -d '' -a my_array < <(my_command && printf '\0')- keduanya berfungsi dengan benar di bash 3.x, dan juga melewati status keluar gagal dari my_commandke read.
Charles Duffy
86

Kamu bisa memakai

my_array=( $(<command>) )

untuk menyimpan output dari perintah <command>ke dalam array my_array.

Anda dapat mengakses panjang array itu menggunakan

my_array_length=${#my_array[@]}

Sekarang panjangnya disimpan my_array_length.

Michael Schlottke-Lakemper
sumber
19
Bagaimana jika output $ (command) memiliki spasi dan banyak baris dengan spasi? Saya menambahkan "$ (command)" dan menempatkan semua output dari semua baris ke dalam elemen [0] pertama dari array.
ikwyl6
3
@ ikwyl6 solusinya adalah menugaskan output perintah ke variabel dan kemudian membuat array dengannya atau menambahkannya ke array. VAR="$(<command>)"dan kemudian my_array=("$VAR")ataumy_array+=("$VAR")
Vito
10

Bayangkan Anda akan meletakkan file dan nama direktori (di bawah folder saat ini) ke sebuah array dan menghitung itemnya. Naskahnya akan seperti;

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

Atau, Anda dapat mengulangi array ini dengan menambahkan skrip berikut:

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

Harap dicatat bahwa ini adalah konsep inti dan masukan dianggap telah dibersihkan sebelumnya, yaitu menghapus karakter tambahan, menangani String kosong, dan lain-lain (yang berada di luar topik utas ini).

Youness
sumber
3
Ide buruk untuk alasan yang disebutkan dalam jawaban di atas
Hubert Grzeskowiak