Apa yang terjadi jika Anda mengedit skrip selama eksekusi?

31

Saya punya pertanyaan umum, yang mungkin merupakan hasil dari kesalahpahaman tentang bagaimana proses ditangani di Linux.

Untuk tujuan saya, saya akan mendefinisikan 'skrip' sebagai potongan kode bash yang disimpan ke file teks dengan izin eksekusi yang diaktifkan untuk pengguna saat ini.

Saya memiliki serangkaian skrip yang saling memanggil secara bersamaan. Demi kesederhanaan, saya akan memanggil mereka skrip A, B, dan C. Script A melakukan serangkaian pernyataan dan kemudian berhenti, kemudian mengeksekusi skrip B, kemudian berhenti, kemudian mengeksekusi skrip C. Dengan kata lain, seri langkah-langkahnya adalah seperti ini:

Jalankan Skrip A:

  1. Serangkaian pernyataan
  2. Jeda
  3. Jalankan Script B
  4. Jeda
  5. Jalankan Script C

Saya tahu dari pengalaman bahwa jika saya menjalankan skrip A hingga jeda pertama, kemudian melakukan pengeditan di skrip B, pengeditan tersebut tercermin dalam pelaksanaan kode ketika saya mengizinkannya untuk melanjutkan. Demikian juga jika saya mengedit skrip C sementara skrip A masih dijeda, kemudian mengizinkannya untuk melanjutkan setelah menyimpan perubahan, perubahan itu tercermin dalam pelaksanaan kode.

Inilah pertanyaan sebenarnya, apakah ada cara untuk mengedit Script A saat masih berjalan? Atau apakah pengeditan tidak mungkin setelah eksekusi dimulai?

CaffeineConnoisseur
sumber
2
saya akan berpikir itu tergantung pada shell. meskipun Anda menyatakan bahwa Anda menggunakan bash. Sepertinya itu akan tergantung pada cara shell memuat skrip secara internal.
strugee
perilaku juga dapat berubah jika Anda sumber file alih-alih menjalankannya.
strugee
1
Saya pikir bash membaca seluruh skrip ke dalam memori sebelum dieksekusi.
w4etwetewtwet
2
@ handuel, tidak, tidak. Seperti itu tidak menunggu sampai Anda mengetik "keluar" di prompt untuk mulai menafsirkan perintah yang Anda masukkan.
Stéphane Chazelas
1
@StephaneChazelas Ya membaca dari terminal itu tidak, namun itu berbeda dengan menjalankan skrip.
w4etwetewtwet

Jawaban:

21

Di Unix, kebanyakan editor bekerja dengan membuat file sementara baru yang berisi konten yang diedit. Ketika file yang diedit disimpan, file asli dihapus dan file sementara berganti nama menjadi nama asli. (Ada, tentu saja, berbagai perlindungan untuk mencegah dataloss.) Ini, misalnya, gaya yang digunakan oleh sedatau perlketika dipanggil dengan -ibendera ("di tempat"), yang sama sekali tidak "di tempat". Seharusnya disebut "tempat baru dengan nama lama".

Ini berfungsi dengan baik karena unix menjamin (setidaknya untuk sistem file lokal) bahwa file yang dibuka terus ada sampai ditutup, bahkan jika itu "dihapus" dan file baru dengan nama yang sama dibuat. (Bukan kebetulan bahwa panggilan sistem unix untuk "menghapus" file sebenarnya disebut "batalkan tautan".) Jadi, secara umum, jika penerjemah shell memiliki beberapa file sumber terbuka, dan Anda "mengedit" file dengan cara yang dijelaskan di atas , shell bahkan tidak akan melihat perubahan karena masih membuka file asli.

[Catatan: seperti halnya semua komentar berbasis standar, di atas tunduk pada beberapa interpretasi dan ada berbagai kasus sudut, seperti NFS. Pedant dipersilakan untuk mengisi komentar dengan pengecualian.]

Tentu saja dimungkinkan untuk memodifikasi file secara langsung; itu hanya sangat tidak nyaman untuk keperluan pengeditan, karena sementara Anda dapat menimpa data dalam file, Anda tidak dapat menghapus atau menyisipkan tanpa menggeser semua data berikut, yang akan menyiratkan banyak menulis ulang. Selain itu, ketika Anda melakukan pemindahan itu, konten file tidak dapat diprediksi dan proses yang membuat file terbuka akan menderita. Untuk menghindari hal ini (seperti halnya sistem basis data, misalnya), Anda memerlukan seperangkat protokol modifikasi yang canggih dan kunci yang didistribusikan; hal-hal yang jauh di luar cakupan utilitas pengeditan file yang khas.

Jadi, jika Anda ingin mengedit file saat sedang diproses oleh shell, Anda memiliki dua opsi:

  1. Anda dapat menambahkan ke file. Ini harus selalu berhasil.

  2. Anda dapat menimpa file dengan konten baru dengan panjang yang persis sama . Ini mungkin atau mungkin tidak berfungsi, tergantung pada apakah shell sudah membaca bagian file atau tidak. Karena sebagian besar file I / O melibatkan buffer baca, dan karena semua shell yang saya tahu membaca seluruh perintah majemuk sebelum menjalankannya, sangat tidak mungkin Anda bisa lolos dengan ini. Tentu tidak akan bisa diandalkan.

Saya tidak tahu adanya kata-kata dalam standar Posix yang sebenarnya membutuhkan kemungkinan untuk menambahkan ke file skrip saat file sedang dieksekusi, sehingga mungkin tidak bekerja dengan setiap shell yang sesuai dengan Posix, apalagi dengan penawaran saat ini dari hampir dan kadang-kadang shell yang sesuai dengan posix. Jadi YMMV. Tapi sejauh yang saya tahu, itu berfungsi baik dengan bash.

Sebagai bukti, inilah implementasi "bebas-loop" dari program bir 99 botol terkenal di bash, yang digunakan dduntuk menimpa dan menambahkan (penimpaan itu mungkin aman karena menggantikan garis yang saat ini sedang dijalankan, yang selalu merupakan baris terakhir dari file, dengan komentar yang persis sama panjangnya; saya melakukan itu sehingga hasil akhirnya dapat dieksekusi tanpa perilaku modifikasi diri.)

#!/bin/bash
if [[ $1 == reset ]]; then
  printf "%s\n%-16s#\n" '####' 'next ${1:-99}' |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^#### $0 | cut -f1 -d:) bs=1 2>/dev/null
  exit
fi

step() {
  s=s
  one=one
  case $beer in
    2) beer=1; unset s;;
    1) beer="No more"; one=it;;
    "No more") beer=99; return 1;;
    *) ((--beer));;
  esac
}
next() {
  step ${beer:=$(($1+1))}
  refrain |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^next\  $0 | cut -f1 -d:) bs=1 conv=notrunc 2>/dev/null
}
refrain() {
  printf "%-17s\n" "# $beer bottles"
  echo echo ${beer:-No more} bottle$s of beer on the wall, ${beer:-No more} bottle$s of beer.
  if step; then
    echo echo Take $one down, pass it around, $beer bottle$s of beer on the wall.
    echo echo
    echo next abcdefghijkl
  else
    echo echo Go to the store, buy some more, $beer bottle$s of beer on the wall.
  fi
}
####
next ${1:-99}   #
Rici
sumber
Ketika saya menjalankan ini, ini dimulai dengan "Tidak lebih", kemudian berlanjut ke -1 dan ke angka negatif tanpa batas.
Daniel Hershcovich
Jika saya lakukan export beer=100sebelum menjalankan skrip, itu berfungsi seperti yang diharapkan.
Daniel Hershcovich
@DanielHershcovich: cukup benar; pengujian ceroboh pada bagian saya. Saya pikir saya memperbaikinya; sekarang mengambil parameter jumlah opsional. Perbaikan yang lebih baik dan lebih menarik adalah mereset secara otomatis jika parameter tidak sesuai dengan salinan yang di-cache.
rici
18

bash berjalan jauh untuk memastikan itu membaca perintah sebelum menjalankannya.

Misalnya di:

cmd1
cmd2

Shell akan membaca skrip dengan blok, sehingga kemungkinan membaca kedua perintah, menafsirkan yang pertama dan kemudian mencari kembali ke akhir cmd1skrip dan membaca skrip lagi untuk membaca cmd2dan menjalankannya.

Anda dapat dengan mudah memverifikasi:

$ cat a
echo foo | dd 2> /dev/null bs=1 seek=50 of=a
echo bar
$ bash a
foo

(Meskipun melihat straceoutput pada itu, tampaknya ia melakukan beberapa hal yang lebih mewah (seperti membaca data beberapa kali, mencari kembali ...) daripada ketika saya mencoba yang sama beberapa tahun yang lalu, jadi pernyataan saya di atas tentang lseeking kembali mungkin tidak berlaku lagi pada versi yang lebih baru).

Namun jika Anda menulis skrip Anda sebagai:

{
  cmd1
  cmd2
  exit
}

Shell harus membaca hingga penutup }, simpan di memori dan jalankan. Karena itu exit, shell tidak akan membaca dari skrip lagi sehingga Anda dapat mengeditnya dengan aman ketika shell menafsirkannya.

Atau, saat mengedit skrip, pastikan Anda menulis salinan skrip baru. Shell akan terus membaca yang asli (bahkan jika itu dihapus atau diganti namanya).

Untuk melakukannya, ubah nama the-scriptmenjadi the-script.olddan salin the-script.oldke the-scriptdan edit.

Stéphane Chazelas
sumber
4

Sebenarnya tidak ada cara aman untuk memodifikasi skrip saat sedang berjalan karena shell dapat menggunakan buffering untuk membaca file. Selain itu, jika skrip dimodifikasi dengan menggantinya dengan file baru, shell biasanya hanya akan membaca file baru setelah melakukan operasi tertentu.

Seringkali, ketika skrip diubah saat dieksekusi, shell akhirnya melaporkan kesalahan sintaksis. Ini disebabkan oleh fakta bahwa, ketika shell menutup dan membuka kembali file skrip, ia menggunakan byte offset ke dalam file untuk memposisikan dirinya kembali.

Abu
sumber
4

Anda bisa menyiasatinya dengan memasang jebakan pada skrip Anda, dan kemudian menggunakan execuntuk mengambil konten skrip baru. Namun perhatikan, execpanggilan memulai skrip dari awal dan bukan dari tempat skrip tersebut telah berjalan dalam proses yang sedang berjalan, dan skrip B akan dipanggil (seterusnya).

#! /bin/bash

CMD="$0"
ARGS=("$@")

trap reexec 1

reexec() {
    exec "$CMD" "${ARGS[@]}"
}

while : ; do sleep 1 ; clear ; date ; done

Ini akan terus menampilkan tanggal di layar. Saya kemudian dapat mengedit skrip saya dan mengubahnya datemenjadi echo "Date: $(date)". Saat menulis itu, skrip yang berjalan masih hanya menampilkan tanggal. Bagaimana jika saya mengirim sinyal yang saya tetapkan trapuntuk ditangkap, skrip akan exec(menggantikan proses yang sedang berjalan dengan perintah yang ditentukan) yang merupakan perintah $CMDdan argumen $@. Anda dapat melakukan ini dengan mengeluarkan kill -1 PID- di mana PID adalah PID dari skrip yang sedang berjalan - dan output berubah untuk ditampilkan Date:sebelum dateoutput perintah.

Anda dapat menyimpan "keadaan" skrip Anda dalam file eksternal (dalam say / tmp), dan membaca konten untuk mengetahui di mana "melanjutkan" pada saat program dieksekusi kembali. Anda kemudian dapat menambahkan penghentian perangkap tambahan (SIGINT / SIGQUIT / SIGKILL / SIGTERM) untuk menghapus file tmp itu sehingga ketika Anda memulai kembali setelah menyela "Script A" itu akan mulai dari awal. Versi stateful akan seperti:

#! /bin/bash

trap reexec 1
trap cleanup 2 3 9 15

CMD="$0"
ARGS=("$@")
statefile='/tmp/scriptA.state'
EXIT=1

reexec() { echo "Restarting..." ; exec "$CMD" "${ARGS[@]}"; }
cleanup() { rm -f $statefile; exit $EXIT; }
run_scriptB() { /path/to/scriptB; echo "scriptC" > $statefile; }
run_scriptC() { /path/to/scriptC; echo "stop" > $statefile;  }

while [ "$state" != "stop" ] ; do

    if [ -f "$statefile" ] ; then
        state="$(cat "$statefile")"
    else
        state='starting'
    fi

    case "$state" in
        starting)         
            run_scriptB
        ;;
        scriptC)
            run_scriptC
        ;;
    esac
done

EXIT=0
cleanup
Drav Sloan
sumber
Saya telah memperbaiki masalah itu dengan menangkap $0dan $@pada awal skrip dan menggunakan variabel-variabel tersebut sebagai execgantinya.
Drav Sloan