Mengapa tanda seru `!` Terkadang mengecewakan bash?

14

Saya menyadari bahwa !memiliki arti khusus pada baris perintah dalam konteks sejarah baris perintah, tetapi selain itu, dalam skrip runing tanda seru kadang-kadang dapat menyebabkan kesalahan penguraian.
Saya pikir itu ada hubungannya dengan event, tetapi saya tidak tahu apa acara itu atau apa fungsinya. Meski begitu, perintah yang sama dapat berperilaku berbeda dalam situasi yang berbeda.
Contoh terakhir, di bawah ini, menyebabkan kesalahan; tetapi mengapa, ketika kode yang sama bekerja di luar substitusi perintah? .. menggunakan GNU bash 4.1.5

# This works, with or without a space between ! and p
  { echo -e "foo\nbar" | sed -nre '/foo/! p'
    echo -e "foo\nbar" | sed -nre '/foo/!p'; }
# bar
# bar

# This works, works when there is a space between ! and p
  var="$(echo -e "foo\nbar" | sed -nre '/foo/! p')"; echo "$var"
# bar

# This causes an ERROR, with NO space between ! and p
  var="$(echo -e "foo\nbar" | sed -nre '/foo/!p')"; echo "$var"
# bash: !p': event not found
Peter.O
sumber
@ Warren .. Terima kasih. Saya telah melihat QA itu, tetapi hanya berbicara tentang bagaimana cara menghindari backslash ... Masalah saya lebih berkaitan dengan mengapa kode yang tampaknya sudah lolos bekerja dalam satu situasi dan bukan yang lain ...
Peter.O
@ fred: "sepertinya sudah lolos"? Saya tidak melihat ada jalan keluar sama sekali dan Anda menggunakan tanda kutip ganda. Lihat jawaban (revisi) saya. Menurut Anda bagian mana yang lolos?
Caleb
@ Caleb. Ya, saya menggunakan istilah yang salah .. protectedakan lebih tepat. (dilindungi oleh 'kutipan tunggal')
Peter.O
Jika Anda hanya peduli dengan tugas-tugas sederhana, maka Anda dapat menggunakan var=$(…)(tanpa tanda kutip ganda), dan itu akan berfungsi seperti (saya pikir) yang Anda harapkan. Ini masih “aman” karena nilai bagian dari tugas sederhana tidak tunduk kata membelah atau globbing (meskipun ini tidak mungkin benar dari tugas dilakukan melalui builtin (misalnya export, local, dll) di bawah semua kerang). Sayangnya, ini tidak melampaui penugasan sederhana karena tanda kutip ganda adalah cara untuk melindungi terhadap pemisahan kata dan penggumpalan sementara masih mendapatkan jenis ekspansi lain dalam konteks lain.
Chris Johnsen

Jawaban:

12

The !karakter memanggil substitusi sejarah bash. Ketika diikuti oleh string (seperti pada contoh Anda yang gagal) itu mencoba untuk memperluas ke peristiwa sejarah terakhir yang dimulai dengan string itu. Sama seperti $vardiperluas ke nilai string itu, !echoakan diperluas ke perintah gema terakhir dalam riwayat Anda.

Ruang adalah karakter yang melanggar dalam ekspansi seperti itu. Catatan pertama bagaimana ini akan bekerja dengan variabel:

# var="like"
# echo "$var"
like
# echo "$"
$
# echo "Do you $var frogs?"
Do you like frogs?       <- as expected, variable name broken at space
# echo "Do you $varfrogs?"
Do you?                  <- $varfrogs not defined, replaced with blank
# echo "Do you $ var frogs?"
Do you $ var frogs?      <- $ not a valid variable name, ignored

Hal yang sama akan terjadi pada ekspansi sejarah. Karakter bang ( !) dimulai dari urutan penggantian histori, tetapi hanya jika diikuti oleh string. Mengikutinya dengan spasi membuatnya menjadi bang bukan bagian dari urutan ganti.

Anda dapat menghindari penggantian semacam ini untuk ekspansi variabel dan riwayat dengan menggunakan tanda kutip tunggal. Contoh pertama Anda menggunakan tanda kutip tunggal dan berjalan dengan baik. Contoh terakhir Anda ada dalam tanda kutip ganda dan dengan demikian bash memindai mereka untuk urutan ekspansi sebelum melakukan hal lain. Satu-satunya alasan yang pertama tidak tersandung adalah bahwa ruang adalah karakter istirahat seperti yang ditunjukkan di atas.

Caleb
sumber
Terima kasih Caleb .. Praponsepsi saya yang lain dibubarkan ... Saya pikir bash parsing dilakukan dari kurung terdalam atau penjepit, dan kemudian bekerja ke luar ... Tampaknya bash mem-parsing berbeda dengan asumsi saya.
Peter.O
1
Mengutip cukup membingungkan dalam bash tanpa itu berubah dalam string bersarang. Karena itu, penggantian terjadi sangat awal dalam proses. Pertimbangkan contoh ini:var=word; echo "test '$var'"; echo 'test "$var"'
Caleb
.. Ya belum disantap. Saya menyadari adanya tanda kutip di dalam tanda kutip ... Kesalahpahaman saya adalah saya berpikir bahwa kode di dalam kurung substitusi perintah akan diuraikan secara terpisah, untuk apa yang mengelilingi tanda kurung itu; tapi ternyata tidak .. terima kasih.
Peter.O
6

Seperti yang sudah dikatakan oleh Caleb , !digunakan untuk menjalankan substitusi sejarah bash.

Jika seperti saya Anda merasa Anda tidak memerlukan fitur seperti itu, Anda dapat menonaktifkannya dengan memasukkan baris berikut di ~/.bashrc:

set +H

Saya tidak membutuhkannya karena riwayat dapat dipulihkan oleh panah atas dan Ctrl- rpencarian terbalik tambahan. Lihat halaman manual bash, bagian Perintah untuk Memanipulasi Sejarah untuk daftar pintasan terperinci.

enzotib
sumber
2
Bagaimana kamu hidup tanpanya !!?
Caleb
Terima kasih., Saya akan berpikir itu bisa menjadi masalah, portabilitas-bijaksana, tetapi menggunakan set +Hdalam skrip bekerja dengan baik :) +1
Peter.O
2
@ Fred: aneh, umumnya ekspansi sejarah "aktif" hanya untuk shell interaktif.
enzotib
@enzo .. Terima kasih lagi .. Saya telah mengujinya dari commandline .. Ah! Jika belajar tidak terlalu menyenangkan, itu akan membosankan ... apakah saya menyebutkan kopi? itu membantu juga :)
Peter.O
Ya, itu gotcha. Saya telah menyalin kode yang disisipkan dari skrip yang gagal pada baris perintah hanya karena alasan ini. Perluasan sejarah bukan masalah dalam skrip tetapi pada shell interaktif.
Caleb
2

contoh pertama Anda:

{ echo -e "foo\nbar" | sed -nre '/foo/! p'
    echo -e "foo\nbar" | sed -nre '/foo/!p'; }

dapat dikurangi menjadi

echo '! p' 
echo '!p'

Dalam tanda kutip tunggal, semua karakter mempertahankan nilai literalnya. Dengan demikian !telah kehilangan makna khusus dan ekspansi sejarah tidak terbentuk sebelumnya.

contoh kedua dan ketiga Anda:

var="$(echo -e "foo\nbar" | sed -nre '/foo/! p')"; echo "$var"

var="$(echo -e "foo\nbar" | sed -nre '/foo/!p')"; echo "$var"

dapat dikurangi menjadi

echo "'! p'"

echo "'!p'"

'! p'dan '!p'pada dasarnya adalah bagian dari string yang dikutip ganda.

Dalam tanda kutip ganda, semua karakter melestarikan nilai-nilai literal mereka kecuali $ , `, \dan !.

Itu menyiratkan kutipan tunggal '! p'dan '!p'telah kehilangan makna khusus mereka (yaitu: tidak dapat melarikan diri !) tetapi !masih mempertahankan makna khusus sehingga ekspansi sejarah dilakukan.

Namun, ketika !diikuti oleh karakter spasi, ekspansi sejarah tidak dilakukan.

Mengutip dari man bash:

Mengutip

[...]

Menutup karakter dalam tanda kutip tunggal mempertahankan nilai literal setiap karakter dalam tanda kutip. [...]

Menutup karakter dalam tanda kutip ganda menjaga nilai literal semua karakter dalam tanda kutip, dengan pengecualian $, `, \, dan, ketika ekspansi sejarah diaktifkan,!. [...] Jika diaktifkan, ekspansi riwayat akan dilakukan kecuali jika! muncul dalam tanda kutip ganda diloloskan menggunakan backslash. Garis miring terbalik sebelum! tidak dihapus.

EKSPANSI SEJARAH

[...]

Ekspansi sejarah diperkenalkan oleh penampilan karakter ekspansi sejarah, yaitu! secara default. Hanya backslash (\) dan tanda kutip tunggal yang dapat mengutip karakter ekspansi sejarah.

Beberapa karakter menghambat ekspansi sejarah jika ditemukan segera mengikuti karakter ekspansi sejarah, bahkan jika tidak dikutip: spasi, tab, baris baru, carriage return, dan =. Jika opsi extglob shell diaktifkan, (juga akan menghambat ekspansi.

cychoi
sumber