Bagaimana cara mengumpulkan array garis dengan benar di zsh

42

Saya pikir yang berikut ini akan mengelompokkan output my_commanddalam array baris:

IFS='\n' array_of_lines=$(my_command);

sehingga $array_of_lines[1]akan merujuk pada baris pertama dalam output dari my_command, $array_of_lines[2]ke yang kedua, dan sebagainya.

Namun, perintah di atas sepertinya tidak berfungsi dengan baik. Tampaknya juga membagi output my_commandsekitar karakter n, seperti yang telah saya periksa print -l $array_of_lines, yang saya percaya mencetak elemen-elemen dari array baris demi baris. Saya juga sudah memeriksa ini dengan:

echo $array_of_lines[1]
echo $array_of_lines[2]
...

Dalam upaya kedua, saya pikir menambahkan evaldapat membantu:

IFS='\n' array_of_lines=$(eval my_command);

tapi saya mendapat hasil yang sama persis tanpa itu.

Akhirnya, mengikuti jawaban pada elemen Daftar dengan spasi di zsh , saya juga mencoba menggunakan flag ekspansi parameter alih-alih IFSmemberi tahu zsh cara membagi input dan mengumpulkan elemen menjadi array, yaitu:

array_of_lines=("${(@f)$(my_command)}");

Tapi saya masih mendapat hasil yang sama (splitting terjadi pada n)

Dengan ini, saya punya pertanyaan berikut:

Q1. Apa cara "yang tepat" untuk mengumpulkan output dari perintah dalam array baris?

Q2. Bagaimana saya dapat menentukan IFSuntuk membagi pada baris baru saja?

Q3. Jika saya menggunakan flag ekspansi parameter seperti pada upaya ketiga saya di atas (yaitu menggunakan @f) untuk menentukan pemisahan, apakah zsh mengabaikan nilai IFS? Mengapa itu tidak berhasil?

Amelio Vazquez-Reina
sumber

Jawaban:

71

TL, DR:

array_of_lines=("${(@f)$(my_command)}")

Kesalahan pertama (→ Q2): IFS='\n'diatur IFSke dua karakter \dan n. Untuk mengatur IFSke baris baru, gunakan IFS=$'\n'.

Kedua kesalahan: untuk mengatur variabel ke nilai array, Anda perlu kurung di sekitar elemen: array_of_lines=(foo bar).

Ini akan berhasil, kecuali bahwa itu menghapus garis kosong, karena spasi putih berturut-turut dianggap sebagai pemisah tunggal:

IFS=$'\n' array_of_lines=($(my_command))

Anda dapat mempertahankan baris kosong kecuali di bagian paling akhir dengan menggandakan karakter spasi putih di IFS:

IFS=$'\n\n' array_of_lines=($(my_command))

Untuk tetap mengikuti garis kosong juga, Anda harus menambahkan sesuatu ke output perintah, karena ini terjadi dalam substitusi perintah itu sendiri, bukan dari penguraiannya.

IFS=$'\n\n' array_of_lines=($(my_command; echo .)); unset 'array_of_lines[-1]'

(dengan asumsi output my_commandtidak berakhir pada baris yang tidak dibatasi; perhatikan juga bahwa Anda kehilangan status keluar dari my_command)

Perhatikan bahwa semua cuplikan di atas IFSmemiliki nilai non-default, sehingga mereka dapat mengacaukan kode selanjutnya. Untuk mempertahankan pengaturan IFSlokal, masukkan semuanya ke dalam fungsi di mana Anda mendeklarasikan IFSlokal (di sini juga menjaga status keluar perintah):

collect_lines() {
  local IFS=$'\n\n' ret
  array_of_lines=($("$@"; ret=$?; echo .; exit $ret))
  ret=$?
  unset 'array_of_lines[-1]'
  return $ret
}
collect_lines my_command

Tapi saya sarankan untuk tidak main-main IFS; alih-alih, gunakan fbendera ekspansi untuk dipisah pada baris baru (→ Q1):

array_of_lines=("${(@f)$(my_command)}")

Atau untuk melestarikan garis kosong:

array_of_lines=("${(@f)$(my_command; echo .)}")
unset 'array_of_lines[-1]'

Nilai IFStidak masalah di sana. Saya menduga bahwa Anda menggunakan perintah yang membelah IFSuntuk mencetak $array_of_linesdalam tes Anda (→ Q3).

Gilles 'SANGAT berhenti menjadi jahat'
sumber
7
Ini sangat rumit! "${(@f)...}"sama dengan ${(f)"..."}, tetapi dengan cara yang berbeda. (@)di dalam tanda kutip ganda berarti "menghasilkan satu kata per elemen array" dan (f)berarti "dibagi menjadi array dengan baris baru". PS: Tolong tautkan ke dokumen
domba terbang
3
@flyingsheep, tidak ${(f)"..."}akan melewatkan garis kosong, "${(@f)...}"melestarikannya. Itu perbedaan yang sama antara $argvdan "$argv[@]". Benda itu "$@"untuk melestarikan semua elemen array berasal dari cangkang Bourne pada akhir 70-an.
Stéphane Chazelas
4

Dua masalah: pertama, tampaknya tanda kutip ganda juga tidak menafsirkan lolos backslash (maaf tentang itu :). Gunakan $'...'tanda kutip. Dan menurut man zshparam, untuk mengumpulkan kata-kata dalam array Anda harus melampirkannya dalam tanda kurung. Jadi ini bekerja:

% touch 'a b' c d 'e f'
% IFS=$'\n' arr=($(ls)); print -l $arr
a b
c
d
e f
% print $arr[1]
a b

Saya tidak bisa menjawab Q3 Anda. Saya harap saya tidak akan pernah tahu hal-hal esoterik seperti ini :).

angus
sumber
-2

Anda juga dapat menggunakan tr untuk mengganti baris baru dengan spasi:

lines=($(mycommand | tr '\n' ' '))
select line in ("${lines[@]}"); do
  echo "$line"
  break
done
Jimchao
sumber
3
bagaimana jika garis mengandung spasi?
don_crissti
2
Itu tidak masuk akal. SPC dan NL keduanya dalam nilai default $IFS. Menerjemahkan satu ke yang lain tidak ada bedanya.
Stéphane Chazelas
Apakah suntingan saya masuk akal? Saya tidak bisa membuatnya berfungsi seperti semula,
John P
(kehabisan waktu) Saya akui saya mengeditnya tanpa benar-benar memahami apa maksudnya, tetapi saya pikir terjemahannya adalah awal yang baik untuk pemisahan berdasarkan manipulasi string. Saya tidak akan menerjemahkan ke spasi, dan membaginya, kecuali perilaku yang diharapkan lebih seperti echo, di mana input semua lebih atau kurang tumpukan kata yang dipisahkan oleh siapa yang peduli.
John P