Bagaimana cara menyimpan hasil perintah "temukan" sebagai array di Bash

93

Saya mencoba menyimpan hasil dari findsebagai array. Ini kode saya:

#!/bin/bash

echo "input : "
read input

echo "searching file with this pattern '${input}' under present directory"
array=`find . -name ${input}`

len=${#array[*]}
echo "found : ${len}"

i=0

while [ $i -lt $len ]
do
echo ${array[$i]}
let i++
done

Saya mendapatkan 2 file .txt di bawah direktori saat ini. Jadi saya mengharapkan '2' sebagai hasil dari ${len}. Namun, mencetak 1. Alasannya adalah karena mengambil semua hasil findsebagai satu elemen. Bagaimana cara memperbaikinya?

PS
Saya menemukan beberapa solusi di StackOverFlow tentang masalah yang sama. Namun, mereka sedikit berbeda jadi saya tidak dapat melamar dalam kasus saya. Saya perlu menyimpan hasil dalam variabel sebelum loop. Terima kasih lagi.

Juneyoung Oh
sumber

Jawaban:

134

Perbarui 2020 untuk Pengguna Linux:

Jika Anda memiliki versi up-to-date dari bash (4.4-alpha atau lebih baik), karena Anda mungkin lakukan jika Anda berada di Linux, maka Anda harus menggunakan jawaban Benjamin W. ini .

Jika Anda menggunakan Mac OS, yang — terakhir saya periksa — masih menggunakan bash 3.2, atau menggunakan bash yang lebih lama, lanjutkan ke bagian berikutnya.

Jawaban untuk bash 4.3 atau sebelumnya

Berikut adalah salah satu solusi untuk mendapatkan output findmenjadi basharray:

array=()
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done < <(find . -name "${input}" -print0)

Ini rumit karena, secara umum, nama file dapat memiliki spasi, baris baru, dan karakter bermusuhan skrip lainnya. Satu-satunya cara untuk menggunakan finddan memisahkan nama file dengan aman satu sama lain adalah menggunakan -print0yang mencetak nama file yang dipisahkan dengan karakter null. Ini tidak akan merepotkan jika bash readarray/ mapfilefunctions mendukung string yang dipisahkan null, tetapi tidak. Bash readmelakukannya dan itu membawa kita ke loop di atas.

[Jawaban ini aslinya ditulis pada tahun 2014. Jika Anda memiliki versi bash terbaru, lihat pembaruan di bawah.]

Bagaimana itu bekerja

  1. Baris pertama membuat array kosong: array=()

  2. Setiap kali readpernyataan itu dijalankan, nama file yang dipisahkan dengan null dibaca dari input standar. The -rpilihan memberitahu readmeninggalkan karakter backslash saja. The -d $'\0'memberitahu readbahwa input akan null dipisahkan. Karena kita menghilangkan nama untuk read, shell menempatkan masukan ke nama default: REPLY.

  3. The array+=("$REPLY")Pernyataan menambahkan nama file baru untuk array array.

  4. Baris terakhir menggabungkan pengalihan dan substitusi perintah untuk memberikan keluaran findke masukan standar whileperulangan.

Mengapa menggunakan substitusi proses?

Jika kami tidak menggunakan substitusi proses, loop dapat ditulis sebagai:

array=()
find . -name "${input}" -print0 >tmpfile
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done <tmpfile
rm -f tmpfile

Di atas, output dari finddisimpan dalam file sementara dan file itu digunakan sebagai input standar untuk loop sementara. Ide substitusi proses adalah membuat file-file sementara tidak diperlukan. Jadi, alih-alih whilemendapatkan loop mendapatkan stdin dari tmpfile, kita bisa mendapatkannya dari stdin <(find . -name ${input} -print0).

Substitusi proses sangat berguna. Di banyak tempat di mana perintah ingin membaca dari sebuah file, Anda dapat menentukan substitusi proses <(...), sebagai ganti nama file. Ada bentuk analogi`` >(...)yang dapat digunakan sebagai pengganti nama file di mana perintah ingin menulis ke file.

Seperti array, substitusi proses adalah fitur bash dan shell tingkat lanjut lainnya. Ini bukan bagian dari standar POSIX.

Alternatif: lastpipe

Jika diinginkan, lastpipedapat digunakan sebagai pengganti proses substitusi (ujung topi: Caesar ):

set +m
shopt -s lastpipe
array=()
find . -name "${input}" -print0 | while IFS=  read -r -d $'\0'; do array+=("$REPLY"); done; declare -p array

shopt -s lastpipememberi tahu bash untuk menjalankan perintah terakhir di pipeline di shell saat ini (bukan latar belakang). Dengan cara ini, arraysisa - sisa yang ada setelah pipa selesai. Karena lastpipehanya berpengaruh jika kontrol pekerjaan dimatikan, kita jalankan set +m. (Dalam skrip, sebagai lawan dari baris perintah, kontrol pekerjaan dinonaktifkan secara default.)

Catatan tambahan

Perintah berikut membuat variabel shell, bukan array shell:

array=`find . -name "${input}"`

Jika Anda ingin membuat sebuah array, Anda perlu meletakkan parens di sekitar output find. Jadi, secara naif, seseorang dapat:

array=(`find . -name "${input}"`)  # don't do this

Masalahnya adalah bahwa shell melakukan pemisahan kata pada hasil findsehingga elemen array tidak dijamin sesuai keinginan Anda.

Perbarui 2019

Dimulai dengan versi 4.4-alpha, bash sekarang mendukung -dopsi sehingga loop di atas tidak lagi diperlukan. Sebaliknya, seseorang dapat menggunakan:

mapfile -d $'\0' array < <(find . -name "${input}" -print0)

Untuk informasi lebih lanjut tentang ini, silakan lihat (dan upvote) jawabannya Benjamin W. ini .

Yohanes 1024
sumber
1
@JuneyoungOh Senang bisa membantu. Saya menambahkan bagian substitusi proses.
John1024
3
@Rockallite Itu observasi yang bagus tapi tidak lengkap. Meskipun benar bahwa kami tidak membagi menjadi beberapa kata, kami masih perlu IFS=menghindari penghapusan spasi putih dari awal atau akhir baris masukan. Anda dapat mengujinya dengan mudah dengan membandingkan keluaran dari read var <<<' abc '; echo ">$var<"dengan keluaran dari IFS= read var <<<' abc '; echo ">$var<". Dalam kasus sebelumnya, spasi sebelum dan sesudah abcdihilangkan. Yang terakhir, mereka tidak. Nama file yang dimulai atau diakhiri dengan spasi mungkin tidak biasa tetapi, jika ada, kami ingin agar diproses dengan benar.
Yohanes1024
1
Hai, setelah saya menjalankan kode Anda, saya mendapatkan kesalahan sintaks pesan di dekat token yang tidak terduga <' selesai <<(temukan aaa / -not -newermt "$ last_build_timestamp_v" -type f -print0) '
Przemysław Sienkiewicz
1
Catatan: yang lebih sederhana ''dapat digunakan sebagai pengganti $'\0':n=0; while IFS= read -r -d '' line || [ "$line" ]; do echo "$((++n)):$line"; done < <(printf 'first\nstill first\0second\0third')
glenn jackman
1
@theeagle Saya berasumsi bahwa Anda bermaksud untuk menulis BLAH=$(find . -name '*.php'). Seperti yang dibahas dalam jawaban, pendekatan itu akan bekerja dalam kasus terbatas tetapi tidak akan berfungsi secara umum dengan semua nama file dan tidak menghasilkan, seperti yang diharapkan OP, sebuah array .
Yohanes1024
35

Bash 4.4 memperkenalkan -dopsi ke readarray/ mapfile, jadi ini sekarang dapat diselesaikan dengan

readarray -d '' array < <(find . -name "$input" -print0)

untuk metode yang bekerja dengan nama file sembarang termasuk kosong, baris baru, dan karakter globbing. Ini membutuhkan finddukungan Anda -print0, seperti misalnya GNU find.

Dari manual (menghilangkan opsi lain):

mapfile [-d delim] [array]

-d
Karakter pertama dari delimdigunakan untuk mengakhiri setiap baris masukan, bukan baris baru. Jika delimadalah string kosong, mapfileakan menghentikan baris saat membaca karakter NUL.

Dan readarrayitu hanya sinonim dari mapfile.

Benjamin W.
sumber
18

Jika Anda menggunakan bash4 atau lebih baru, Anda dapat mengganti penggunaan finddengan

shopt -s globstar nullglob
array=( **/*"$input"* )

The **Pola diaktifkan oleh globstarpertandingan 0 atau lebih direktori, yang memungkinkan pola untuk mencocokkan dengan kedalaman sewenang-wenang dalam direktori saat ini. Tanpa nullglobopsi, pola (setelah perluasan parameter) diperlakukan secara harfiah, jadi tanpa kecocokan Anda akan memiliki larik dengan string tunggal, bukan larik kosong.

Tambahkan dotglobopsi ke baris pertama juga jika Anda ingin melintasi direktori tersembunyi (suka .ssh) dan juga mencocokkan file tersembunyi (suka .bashrc).

chepner
sumber
4
Mungkin nullglobjuga…
kojiro
1
Ya, saya selalu lupa itu.
chepner
5
Perhatikan bahwa ini tidak akan menyertakan file dan direktori tersembunyi, kecuali dotglobsudah diatur (ini mungkin atau mungkin tidak diinginkan, tetapi perlu disebutkan juga).
gniourf_gniourf
10

Anda bisa mencoba sesuatu seperti

array=(`find . -type f | sort -r | head -2`)
, dan untuk mencetak nilai array, Anda dapat mencoba sesuatu seperti echo "${array[*]}"

Ahmed Al-Haffar
sumber
8
Putus jika ada nama file dengan spasi atau karakter glob.
gniourf_gniourf
1

Berikut ini tampaknya berfungsi untuk Bash dan Z Shell di macOS.

#! /bin/sh

IFS=$'\n'
paths=($(find . -name "foo"))
unset IFS

printf "%s\n" "${paths[@]}"
sunknudsen
sumber
Ini bekerja dengan file yang memiliki spasi dan karakter khusus lainnya, gagal dengan kasus file yang memiliki linebreak pada namanya. Anda dapat membuatnya untuk pengujian denganprintf "%b" "file name with spaces, a star * ...\012and a second line\0" | xargs -0 touch
Stéphane Gourichon
-1

Dalam bash, $(<any_shell_cmd>)membantu menjalankan perintah dan menangkap hasilnya. Meneruskan ini ke IFSdengan \nsebagai pembatas membantu mengonversinya menjadi array.

IFS='\n' read -r -a txt_files <<< $(find /path/to/dir -name "*.txt")
rashok
sumber
4
Ini hanya akan mendapatkan file pertama dari hasil findke dalam array.
Benjamin W.
-2

Anda bisa melakukan seperti ini:

#!/bin/bash
echo "input : "
read input

echo "searching file with this pattern '${input}' under present directory"
array=(`find . -name '*'${input}'*'`)

for i in "${array[@]}"
do :
    echo $i
done
pengguna1357768
sumber
1
Terima kasih. banyak. Tetapi seperti yang ditunjukkan oleh @anishsane, ruang kosong di nama file harus dipertimbangkan dalam program saya. Pokoknya Terima Kasih!
Juneyoung Oh
-3

Bagi saya, ini berfungsi dengan baik di cygwin:

declare -a names=$(echo "("; find <path> <other options> -printf '"%p" '; echo ")")
for nm in "${names[@]}"
do
    echo "$nm"
done

Ini berfungsi dengan spasi, tetapi tidak dengan tanda kutip ganda (") di nama direktori (yang toh tidak diizinkan di lingkungan Windows).

Hati-hati dengan ruang di opsi -printf.

R Risack
sumber
3
Rusak dan berbahaya : tidak akan menangani kutipan, dan tunduk pada injeksi kode arbitrer. JANGAN GUNAKAN.
gniourf_gniourf
2
Sepertinya seseorang menandai postingan ini untuk dihapus. "Ini salah" bukanlah alasan untuk penghapusan SO. Pengguna berusaha menjawab, sesuai topik, dan memenuhi kriteria jawaban. Tombol downvote digunakan untuk mengukur kegunaan dan ketepatan, bukan tombol penghapusan.
Frambot
3
Seperti yang ditunjukkan oleh Gniourf, ini bukan untuk lingkungan di mana orang lain memasukkan opsi pada sistem Anda, misalnya halaman web. Tetapi tidak semua orang memprogram untuk lingkungan itu. Saya menggunakannya untuk mengganti nama file di direktori.
R Risack