Bagaimana memisahkan output perintah ke masing-masing baris

12
list=`ls -a R*`
echo $list

Di dalam skrip shell, perintah gema ini akan mencantumkan semua file dari direktori saat ini dimulai dengan R, tetapi dalam satu baris. Bagaimana saya bisa mencetak setiap item dalam satu baris?

Aku butuh perintah generik untuk semua skenario terjadi dengan ls, du, find -type -d, dll

Anony
sumber
6
Catatan umum: jangan lakukan ini dengan ls, itu akan merusak nama file yang aneh . Lihat di sini .
terdon

Jawaban:

12

Jika output dari perintah berisi beberapa baris, maka kutip variabel Anda untuk mempertahankan baris-baris baru ketika menggema:

echo "$list"

Jika tidak, shell akan memperluas dan membagi konten variabel di spasi putih (spasi, baris baru, tab) dan itu akan hilang.

muru
sumber
15

Alih-alih menempatkan lsoutput dalam variabel secara tidak benar dan kemudian echomemasukkannya, yang menghilangkan semua warna, gunakan

ls -a1

Dari man ls

       -1     list one file per line.  Avoid '\n' with -q or -b

Saya tidak menyarankan Anda melakukan apa pun dengan output ls, kecuali menampilkannya :)

Gunakan, misalnya, gumpalan shell dan forloop untuk melakukan sesuatu dengan file ...

shopt -s dotglob                  # to include hidden files*

for i in R/*; do echo "$i"; done

* Ini tidak akan menyertakan direktori saat ini .atau induknya ..meskipun

Zanna
sumber
1
Saya akan merekomendasikan untuk mengganti: echo "$i"dengan printf "%s\n" "$i" (yang tidak akan tersedak jika nama file dimulai dengan "-"). Sebenarnya, sebagian besar waktu echo somethingharus diganti printf "%s\n" "something".
Olivier Dulac
2
@OlivierDulac Mereka semua mulai dengan di Rsini, tetapi secara umum, ya. Namun, bahkan untuk penulisan skrip, berusaha selalu mengakomodasi pemimpin -di jalur menyebabkan masalah: orang berasumsi --, yang hanya didukung oleh beberapa perintah, menandakan akhir dari opsi. Saya telah melihat di find . [tests] -exec [cmd] -- {} \;mana [cmd]tidak mendukung --. Bagaimanapun, setiap jalan di sana dimulai dengan .! Tapi itu tidak ada argumen melawan mengganti echodengan printf '%s\n'. Di sini, seseorang dapat mengganti seluruh loop dengan printf '%s\n' R/*. Bash memiliki printfbuiltin, jadi tidak ada batasan jumlah / panjang argumen.
Eliah Kagan
12

Sambil meletakkannya dalam tanda kutip karena @muru menyarankan akan melakukan apa yang Anda minta, Anda mungkin juga ingin mempertimbangkan untuk menggunakan array untuk ini. Sebagai contoh:

IFS=$'\n' dirs=( $(find . -type d) )

The IFS=$'\n'memberitahu bash untuk hanya membagi output pada baris baru characcters o mendapatkan setiap elemen dari array. Tanpa itu, itu akan terpecah pada spasi, jadi a file name with spaces.txtakan ada 5 elemen yang terpisah, bukan satu. Pendekatan ini akan rusak jika nama file / direktori Anda dapat berisi baris baru ( \n). Ini akan menyimpan setiap baris dari output perintah sebagai elemen dari array.

Perhatikan bahwa saya juga mengubah gaya lama `command`ke $(command)yang merupakan sintaks yang lebih disukai.

Anda sekarang memiliki array yang disebut $dirs, masing-masing elemen yang merupakan garis dari output dari perintah sebelumnya. Sebagai contoh:

$ find . -type d
.
./olad
./ho
./ha
./ads
./bar
./ga
./da
$ IFS=$'\n' dirs=( $(find . -type d) )
$ for d in "${dirs[@]}"; do
    echo "DIR: $d"
  done
DIR: .
DIR: ./olad
DIR: ./ho
DIR: ./ha
DIR: ./ads
DIR: ./bar
DIR: ./ga
DIR: ./da

Sekarang, karena beberapa keanehan bash (lihat di sini ), Anda harus mengatur ulang IFSkembali ke nilai asli setelah melakukan ini. Jadi, simpan dan tetapkan kembali:

oldIFS="$IFS"
IFS=$'\n' dirs=( $(find . -type d) ) 
IFS="$oldIFS"

Atau lakukan secara manual:

IFS=" "$'\t\n '

Atau, tutup saja terminal saat ini. Yang baru Anda akan memiliki IFS asli diatur lagi.

terdon
sumber
Saya mungkin telah menyarankan sesuatu seperti ini, tetapi kemudian bagian tentang dumembuatnya terlihat OP lebih tertarik untuk mempertahankan format output.
muru
Bagaimana saya bisa membuat ini berfungsi jika nama direktori mengandung spasi?
hidangan penutup
@dabut whoops! Ini harus bekerja dengan spasi (tetapi bukan baris baru) sekarang. Terima kasih telah menunjukkannya.
terdon
1
Perhatikan bahwa Anda harus mengatur ulang atau menghapus IFS setelah penugasan array itu. unix.stackexchange.com/q/264635/70524
muru
@uru oh wow, terima kasih, saya sudah lupa tentang keanehan itu.
terdon
2

Jika Anda harus menyimpan output dan Anda menginginkan sebuah array, mapfilemembuatnya mudah.

Pertama, pertimbangkan jika Anda perlu menyimpan output perintah Anda sama sekali. Jika tidak, jalankan saja perintahnya.

Jika Anda memutuskan ingin membaca output dari suatu perintah sebagai array dari baris, memang benar bahwa salah satu cara untuk melakukannya adalah dengan menonaktifkan globbing, mengatur IFSuntuk membagi pada baris, menggunakan substitusi perintah di dalam ( )sintaks pembuatan array, dan mengatur ulang IFSsetelah itu, yang lebih kompleks dari yang terlihat. Jawaban terdon mencakup beberapa pendekatan itu. Tapi saya sarankan Anda menggunakan mapfile, perintah built-in Bash shell untuk membaca teks sebagai array baris. Ini kasus sederhana di mana Anda membaca dari sebuah file:

mapfile < filename

Ini membaca baris ke dalam array yang disebut MAPFILE. Tanpa pengalihan input , Anda akan membaca dari input standar shell (biasanya terminal Anda) alih-alih file yang dinamai oleh . Untuk membaca ke dalam array selain dari default , berikan namanya. Sebagai contoh, ini berbunyi ke dalam array :< filenamefilenameMAPFILElines

mapfile lines < filename

Perilaku default lain yang mungkin Anda pilih untuk diubah adalah bahwa karakter baris baru di ujung baris dibiarkan di tempatnya; mereka muncul sebagai karakter terakhir di setiap elemen array (kecuali input berakhir tanpa karakter baris baru, dalam hal ini elemen terakhir tidak memilikinya). Untuk mengejar baris baru ini sehingga tidak muncul dalam array, berikan -topsi mapfile. Misalnya, ini membaca array recordsdan tidak menulis karakter baris baru ke elemen arraynya:

mapfile -t records < filename

Anda juga dapat menggunakan -ttanpa melewati nama array; yaitu, ia bekerja dengan nama array implisit MAPFILEjuga.

The mapfileshell builtin mendukung pilihan lain, dan dapat alternatif dipanggil sebagai readarray. Jalankan help mapfile(dan help readarray) untuk detail.

Tetapi Anda tidak ingin membaca dari file, Anda ingin membaca dari output dari perintah. Untuk mencapai itu, gunakan substitusi proses . Perintah ini membaca baris dari perintah some-commanddengan arguments...sebagai argumen baris perintah dan menempatkannya dalam mapfilelarik default MAPFILE, dengan karakter baris baru yang dihapus dihapus:

mapfile -t < <(some-command arguments...)

Substitusi proses menggantikan dengan nama file aktual dari mana output dari menjalankan dapat dibaca. File tersebut adalah pipa bernama daripada file biasa dan, di Ubuntu, itu akan dinamai seperti (kadang-kadang dengan beberapa nomor selain ), tetapi Anda tidak perlu khawatir dengan detailnya, karena shell menangani itu semua di belakang layar.<(some-command arguments...)some-command arguments.../dev/fd/6363

Anda mungkin berpikir Anda bisa melupakan proses substitusi dengan menggunakan gantinya, tetapi itu tidak akan berhasil karena, ketika Anda memiliki pipeline dari beberapa perintah yang dipisahkan oleh , Bash menjalankan menjalankan semua perintah dalam subkulit . Dengan demikian keduanya dan berjalan di lingkungan mereka sendiri , diinisialisasi dari tetapi terpisah dari lingkungan shell di mana Anda menjalankan pipa. Dalam subkulit tempat berjalan, array tidak diisi, tetapi kemudian array tersebut dibuang saat perintah berakhir. tidak pernah dibuat atau dimodifikasi untuk pemanggil.some-command arguments... | mapfile -t|some-command arguments...mapfile -tmapfile -tMAPFILEMAPFILE

Inilah yang contoh di jawaban Terdon ini terlihat seperti, secara keseluruhan, jika Anda menggunakan mapfile:

mapfile -t dirs < <(find . -type d)
for d in "${dirs[@]}"; do
    echo "DIR: $d"
done

Itu dia. Anda tidak perlu memeriksa apakah IFSsudah disetel , melacak apakah belum disetel dan dengan nilai apa, setel ke baris baru, lalu setel ulang atau atur ulang pengaturan nanti. Anda tidak perlu menonaktifkan globbing (misalnya, dengan set -f) - yang benar-benar diperlukan jika Anda menggunakan metode yang serius, karena nama file mungkin berisi *, ?dan [--then mengaktifkan kembali itu ( set +f) setelah itu.

Anda juga dapat mengganti loop tertentu sepenuhnya dengan satu printfperintah - meskipun ini sebenarnya bukan manfaat mapfile, karena Anda dapat melakukannya apakah Anda menggunakan mapfileatau metode lain untuk mengisi array. Ini versi yang lebih pendek:

mapfile -t < <(find . -type d)
printf 'DIR: %s\n' "${MAPFILE[@]}"

Penting untuk diingat bahwa, seperti yang disebutkan terdon , operasi baris demi baris tidak selalu sesuai, dan tidak akan berfungsi dengan benar dengan nama file yang mengandung baris baru. Saya merekomendasikan untuk tidak menamai file seperti itu, tetapi itu bisa terjadi, termasuk secara tidak sengaja.

Sebenarnya tidak ada solusi satu ukuran untuk semua.

Anda meminta "perintah umum untuk semua skenario," dan pendekatan menggunakan mapfilependekatan yang agak mendekati tujuan itu, tetapi saya mendorong Anda untuk mempertimbangkan kembali kebutuhan Anda.

Tugas yang ditunjukkan di atas lebih baik dicapai hanya dengan satu findperintah:

find . -type d -printf 'DIR: %p\n'

Anda juga bisa menggunakan perintah eksternal ingin sedmenambahkan DIR: ke awal setiap baris. Ini bisa dibilang agak jelek, dan tidak seperti perintah find itu akan menambah "awalan" tambahan dalam nama file yang mengandung baris baru, tetapi itu bekerja terlepas dari inputnya sehingga memenuhi persyaratan Anda dan masih lebih baik untuk membaca output menjadi sebuah variabel atau array :

find . -type d | sed 's/^/DIR: /'

Jika Anda perlu mendaftar dan juga melakukan tindakan pada setiap direktori yang ditemukan, seperti menjalankan some-commanddan melewati jalur direktori sebagai argumen, findmemungkinkan Anda melakukannya juga:

find . -type d -print -exec some-command {} \;

Sebagai contoh lain, mari kembali ke tugas umum menambahkan awalan untuk setiap baris. Misalkan saya ingin melihat output help mapfiletetapi nomor baris. Saya sebenarnya tidak akan menggunakan mapfileini, atau metode lain yang membacanya menjadi variabel shell atau shell array. Seandainya help mapfile | cat -ntidak memberikan format yang saya inginkan, saya dapat menggunakan awk:

help mapfile | awk '{ printf "%3d: %s\n", NR, $0 }'

Membaca semua output dari suatu perintah ke variabel tunggal atau array kadang-kadang berguna dan sesuai, tetapi memiliki kelemahan utama. Anda tidak hanya harus berurusan dengan kompleksitas tambahan menggunakan shell Anda ketika perintah yang ada atau kombinasi dari perintah yang sudah ada mungkin sudah melakukan apa yang Anda butuhkan juga atau lebih baik, tetapi seluruh output dari perintah harus disimpan dalam memori. Terkadang Anda mungkin tahu itu bukan masalah, tetapi kadang-kadang Anda mungkin memproses file besar.

Alternatif yang biasanya dicoba - dan kadang dilakukan dengan benar - adalah membaca input baris demi baris dengan read -rdalam satu lingkaran. Jika Anda tidak perlu menyimpan saluran sebelumnya saat beroperasi pada saluran selanjutnya, dan Anda harus menggunakan input panjang, maka itu mungkin lebih baik daripada mapfile. Tapi itu juga, harus dihindari dalam kasus-kasus ketika Anda bisa mengirimkannya ke perintah yang dapat melakukan pekerjaan, yang kebanyakan kasus .

Eliah Kagan
sumber