Bagaimana cara membaca seluruh skrip shell sebelum menjalankannya?

35

Biasanya, jika Anda mengedit skrip, semua penggunaan skrip yang berjalan cenderung mengalami kesalahan.

Sejauh yang saya mengerti, bash (shell lain juga?) Membaca skrip secara bertahap, jadi jika Anda memodifikasi file skrip secara eksternal, ia mulai membaca hal-hal yang salah. Apakah ada cara untuk mencegahnya?

Contoh:

sleep 20

echo test

Jika Anda menjalankan skrip ini, bash akan membaca baris pertama (katakanlah 10 byte) dan pergi tidur. Ketika resume, mungkin ada konten yang berbeda dalam skrip mulai dari byte ke-10. Saya mungkin berada di tengah baris dalam skrip baru. Dengan demikian skrip yang berjalan akan rusak.

VasyaNovikov
sumber
Apa yang Anda maksud dengan "memodifikasi skrip secara eksternal"?
maulinglawns
1
Mungkin ada cara untuk membungkus semua konten dalam suatu fungsi atau sesuatu, jadi shell akan membaca seluruh skrip terlebih dahulu? Tetapi bagaimana dengan baris terakhir di mana Anda menjalankan fungsi, apakah akan dibaca sampai EOF? Mungkin menghilangkan yang terakhir \nakan berhasil? Mungkin subkulit ()akan lakukan? Saya tidak terlalu berpengalaman dengan itu, tolong bantu!
VasyaNovikov
@maulinglawns jika skrip memiliki konten seperti sleep 20 ;\n echo test ;\n sleep 20dan saya mulai menyuntingnya, mungkin akan terjadi kesalahan. Sebagai contoh, bash dapat membaca 10 byte pertama dari skrip, memahami sleepperintah dan tidur. Setelah dilanjutkan, akan ada konten yang berbeda dalam file mulai dari 10 byte.
VasyaNovikov
1
Jadi, apa yang Anda katakan adalah bahwa Anda mengedit skrip yang sedang dieksekusi? Hentikan skrip terlebih dahulu, lakukan pengeditan Anda, dan kemudian mulai lagi.
maulinglawns
@maulinglawns ya, pada dasarnya itu. Masalahnya adalah, tidak nyaman bagi saya untuk menghentikan skrip, dan sulit untuk selalu ingat untuk melakukan itu. Mungkin ada cara untuk memaksa bash membaca seluruh skrip terlebih dahulu?
VasyaNovikov

Jawaban:

43

Ya, kerang, dan bashkhususnya, berhati-hati untuk membaca file satu baris pada satu waktu, sehingga berfungsi sama seperti ketika Anda menggunakannya secara interaktif.

Anda akan melihat bahwa ketika file tidak dapat dicari (seperti pipa), bashbahkan membaca satu byte pada suatu waktu untuk memastikan tidak membaca melewati \nkarakter. Ketika file dicari, itu dioptimalkan dengan membaca blok penuh pada suatu waktu, tetapi mencari kembali setelah \n.

Itu berarti Anda dapat melakukan hal-hal seperti:

bash << \EOF
read var
var's content
echo "$var"
EOF

Atau tulis skrip yang memperbarui sendiri. Yang tidak akan bisa Anda lakukan jika itu tidak memberi Anda jaminan itu.

Sekarang, jarang sekali Anda ingin melakukan hal-hal seperti itu dan, seperti yang Anda ketahui, fitur itu cenderung lebih sering menghalangi daripada berguna.

Untuk menghindarinya, Anda dapat mencoba dan memastikan Anda tidak mengubah file di tempat (misalnya, memodifikasi salinan, dan memindahkan salinan di tempat (seperti sed -iatau perl -pidan beberapa editor lakukan misalnya)).

Atau Anda dapat menulis skrip Anda seperti:

{
  sleep 20
  echo test
}; exit

(perhatikan bahwa penting untuk exitberada di jalur yang sama }; meskipun Anda juga bisa meletakkannya di dalam kawat gigi tepat sebelum yang terakhir).

atau:

main() {
  sleep 20
  echo test
}
main "$@"; exit

Shell perlu membaca skrip sampai exitsebelum mulai melakukan apa pun. Itu memastikan shell tidak akan membaca dari skrip lagi.

Itu berarti seluruh skrip akan disimpan dalam memori.

Itu juga dapat mempengaruhi penguraian skrip.

Misalnya, di bash:

export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'

Akan menampilkan bahwa U + 00E9 dikodekan dalam UTF-8. Namun, jika Anda mengubahnya ke:

{
  export LC_ALL=fr_FR.UTF-8
  echo $'St\ue9phane'
}

The \ue9akan diperluas di charset yang berlaku pada saat itu bahwa perintah itu diurai yang dalam hal ini adalah sebelum yang exportperintah dijalankan.

Perhatikan juga bahwa jika perintah sourcealias .digunakan, dengan beberapa shell, Anda akan memiliki masalah yang sama untuk file-file yang bersumber.

Itu tidak terjadi bashmeskipun sourceperintah yang membaca file sepenuhnya sebelum menafsirkannya. Jika menulis secara bashspesifik, Anda sebenarnya bisa memanfaatkannya, dengan menambahkan di awal skrip:

if [[ ! $already_sourced ]]; then
  already_sourced=1
  source "$0"; exit
fi

(Saya tidak akan mengandalkan itu meskipun seperti yang Anda bayangkan versi masa depan bashdapat mengubah perilaku yang saat ini dapat dilihat sebagai batasan (bash dan AT&T ksh adalah satu-satunya kerang mirip POSIX yang berperilaku seperti itu sejauh yang bisa diceritakan) dan already_sourcedtriknya agak rapuh karena mengasumsikan bahwa variabel tidak ada di lingkungan, belum lagi itu mempengaruhi konten dari variabel BASH_SOURCE)

Stéphane Chazelas
sumber
@VasyaNovikov, sepertinya ada sesuatu yang salah dengan SE saat ini (atau setidaknya untuk saya). Hanya ada beberapa jawaban ketika saya menambahkan jawaban saya, dan komentar Anda tampaknya baru muncul sekarang meskipun dikatakan telah diposting 16 menit yang lalu (atau mungkin hanya saya yang kehilangan kelereng saya). Bagaimanapun, perhatikan "keluar" ekstra yang diperlukan di sini untuk menghindari masalah ketika ukuran file meningkat (seperti yang tercantum dalam komentar yang saya tambahkan ke jawaban Anda).
Stéphane Chazelas
Stéphane, saya pikir saya sudah menemukan solusi lain. Ini untuk digunakan }; exec true. Dengan cara ini, tidak ada persyaratan pada baris baru di akhir file, yang ramah untuk beberapa editor (seperti emacs). Semua tes yang saya bisa memikirkan bekerja dengan benar}; exec true
VasyaNovikov
@VasyaNovikov, tidak yakin apa yang Anda maksud. Bagaimana ini lebih baik daripada }; exit? Anda juga kehilangan status keluar.
Stéphane Chazelas
Seperti disebutkan pada pertanyaan yang berbeda: adalah umum untuk mem-parsing seluruh file dan kemudian mengeksekusi pernyataan majemuk jika perintah dot ( . script) digunakan.
schily
@ sungguh, ya saya menyebutkan bahwa dalam jawaban ini sebagai batasan AT&T ksh dan bash. Kerang tipe POSIX lainnya tidak memiliki batasan itu.
Stéphane Chazelas
12

Anda hanya perlu menghapus file (mis. Menyalinnya, menghapusnya, mengganti nama salinan kembali ke nama aslinya). Sebenarnya banyak editor dapat dikonfigurasi untuk melakukan ini untuk Anda. Saat Anda mengedit file dan menyimpan buffer yang diubah, alih-alih menimpa file itu akan mengubah nama file lama, membuat yang baru, dan meletakkan konten baru di file yang baru. Karenanya skrip yang berjalan harus dilanjutkan tanpa masalah.

Dengan menggunakan sistem kontrol versi sederhana seperti RCS yang sudah tersedia untuk vim dan emacs, Anda mendapatkan keuntungan ganda dari memiliki riwayat perubahan Anda, dan sistem checkout secara default harus menghapus file saat ini dan membuatnya kembali dengan mode yang benar. (Hati-hati dengan menautkan file seperti itu tentunya)

meuh
sumber
"delete" sebenarnya bukan bagian dari proses. Jika Anda ingin menjadikannya atomik dengan benar, Anda mengganti nama file tujuan - jika Anda memiliki langkah hapus, ada risiko proses Anda mati setelah penghapusan tetapi sebelum mengganti nama, tanpa meninggalkan file sama sekali ( atau pembaca mencoba mengakses file di jendela itu, dan tidak menemukan versi lama maupun baru).
Charles Duffy
11

Solusi paling sederhana:

{
  ... your code ...

  exit
}

Dengan cara ini, bash akan membaca seluruh {}blok sebelum menjalankannya, dan exitarahan akan memastikan tidak ada yang akan dibaca di luar blok kode.

Jika Anda tidak ingin "mengeksekusi" skrip, tetapi lebih ke "sumber" itu, Anda memerlukan solusi yang berbeda. Maka ini harus bekerja:

{
  ... your code ...

  return 2>/dev/null || exit
}

Atau jika Anda ingin kontrol langsung atas kode keluar:

{
  ... your code ...

  ret="$?";return "$ret" 2>/dev/null || exit "$ret"
}

Voa! Skrip ini aman untuk diedit, sumber dan jalankan. Anda masih harus memastikan bahwa Anda tidak memodifikasinya dalam milidetik ketika itu awalnya sedang dibaca.

VasyaNovikov
sumber
1
Apa yang saya temukan adalah bahwa ia tidak melihat EOF dan berhenti membaca file, tetapi ia terjerat dalam pemrosesan "buffered stream" dan akhirnya mencari melewati akhir file, itulah sebabnya mengapa terlihat baik-baik saja jika ukuran file bertambah tidak banyak, tetapi terlihat buruk ketika Anda membuat file lebih dari dua kali lebih besar dari sebelumnya. Saya akan melaporkan bug ke pengelola bash segera.
Stéphane Chazelas
1
bug dilaporkan sekarang , lihat juga tambalan .
Stéphane Chazelas
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
terdon
5

Bukti dari konsep. Berikut ini skrip yang memodifikasi sendiri:

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line overwites the on disk copy of the script
cat /tmp/scr2 > /tmp/scr
# this line ends up changed.
echo script content kept
EOF
chmod u+x /tmp/scr
/tmp/scr

kita melihat versi cetak berubah

Ini karena bash memuat membuat pegangan file agar terbuka ke skrip, sehingga perubahan pada file akan segera terlihat.

Jika Anda tidak ingin memperbarui salinan dalam memori, putuskan tautan file asli dan ganti.

Salah satu cara untuk melakukannya adalah dengan menggunakan sed -i.

sed -i '' filename

bukti dari konsep

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line unlinks the original and creates a new copy.
sed -i ''  /tmp/scr

# now overwriting it has no immediate effect
cat /tmp/scr2 > /tmp/scr
echo script content kept
EOF

chmod u+x /tmp/scr
/tmp/scr

Jika Anda menggunakan editor untuk mengubah skrip, mengaktifkan fitur "simpan salinan cadangan" mungkin yang diperlukan untuk menyebabkan editor menulis versi yang diubah ke file baru alih-alih menimpa yang sudah ada.

Jasen
sumber
2
Tidak, bashtidak membuka file dengan mmap(). Hanya berhati-hati untuk membaca satu baris pada satu waktu sesuai kebutuhan, seperti ketika mendapat perintah dari perangkat terminal saat interaktif.
Stéphane Chazelas
2

Membungkus skrip Anda dalam suatu blok {}kemungkinan merupakan opsi terbaik tetapi mengharuskan Anda mengubah skrip Anda.

F=$(mktemp) && cp test.sh $F && bash $F; rm $F;

akan menjadi pilihan terbaik kedua (dengan asumsi tmpfs ) kerugiannya adalah $ 0 rusak jika skrip Anda menggunakannya.

menggunakan sesuatu seperti F=test.sh; tail -n $(cat "$F" | wc -l) "$F" | bashini kurang ideal karena harus menyimpan seluruh file dalam memori dan merusak $ 0.

menyentuh file asli harus dihindari sehingga, waktu modifikasi terakhir, kunci baca, dan tautan keras tidak terganggu. dengan cara itu Anda dapat membiarkan editor terbuka saat menjalankan file dan rsync tidak akan perlu checksum file untuk fungsi backup dan hard link seperti yang diharapkan.

mengganti file yang sedang diedit akan berfungsi tetapi kurang kuat karena tidak dapat diterapkan pada skrip lain / pengguna / atau orang mungkin lupa. Dan lagi itu akan memutus tautan keras.

pengguna1133275
sumber
apa pun yang membuat salinan akan berhasil. tac test.sh | tac | bash
Jasen