実行中にスクリプトを編集するとどうなりますか?

実行中にスクリプトを編集するとどうなりますか?

Linuxでプロセスがどのように処理されるかについての誤解によって引き起こされる可能性がある一般的な質問があります。

私の目的に合わせて、「スクリプト」を現在のユーザーに実行権限が有効になっているテキストファイルに格納されているbashコードの一部として定義します。

互いに呼び出す一連のスクリプトがあります。簡単にするために、これをスクリプトA、B、Cと呼びます。スクリプトAは一連のステートメントを実行してから一時停止し、スクリプトBを実行してから一時停止してスクリプトCを実行します。つまり、この一連のステートメントステップは次のとおりです。

スクリプトAを実行します。

  1. シリーズ仕様
  2. 停止する
  3. スクリプトBの実行
  4. 停止する
  5. スクリプトCの実行

最初に一時停止するまでスクリプトAを実行してからスクリプトBで編集を実行すると、コードの再開を許可したときにその編集内容がコードの実行に反映されることを経験によって知っています。同様に、スクリプトAが一時停止中にスクリプトCを編集し、変更を保存して続行できるようにすると、その変更はコードの実行に反映されます。

したがって、実際の質問は、スクリプトAの実行中に編集する方法があるかどうかです。それとも実行が始まると編集できませんか?

答え1

Unixのほとんどの編集者が作業する方法は、編集内容を含む新しい一時ファイルを作成することです。編集したファイルを保存すると、元のファイルが削除され、一時ファイルの名前が元の名前に変更されます。 (もちろん、データの損失を防ぐためのさまざまな保護があります。)たとえば、(「in-place」)フラグとして使用または呼び出すときのスタイルですが、sed実際にはまったく「in-place」ではありません。 「新しい場所と古い名前」と呼ぶ必要があります。perl-i

これは、UNIXが(少なくともローカルファイルシステムの場合)開かれたファイルが「削除」され、同じ名前の新しいファイルが作成されても閉じるまで存在し続けることを保証するのでうまく機能します。 (ファイルを「削除」するためのUnixシステムコールが実際に「リンク解除」と呼ばれるのは偶然ではありません)。それでも元のファイルが開いているため、変更を見ることはできません。

[注:すべての標準ベースのコメントと同様に、上記は解釈の余地があり、NFSなどのさまざまなコーナーケースがあります。学界の追加コメントを歓迎しますが、例外もあります。 ]

もちろん、ファイルを直接変更することも可能です。ファイルのデータを上書きすることはできますが、後続のすべてのデータを移動しないと削除または挿入できないため、編集操作はやや不便になります。 。また、この変換を実行するとファイルの内容が予測できなくなり、ファイルを開くプロセスが影響を受けます。この問題(データベースシステムなど)を取り除くには、複雑な修正プロトコルセットと分散ロックが必要です。これは、一般的なファイル編集ユーティリティの範囲をはるかに超えています。

したがって、シェルがファイルを処理している間にファイルを編集するには、2つのオプションがあります。

  1. ファイルに追加できます。これは常に機能するはずです。

  2. 新しいコンテンツでファイルを上書きできます。長さは全く同じです。。これは、シェルがファイルの対応する部分を既に読み込んでいるかどうかに応じて機能する場合と動作しない場合があります。ほとんどのファイルI / Oにはバッファの読み取りが含まれており、私が知っているすべてのシェルは、実行前に完全な複合コマンドを読み取るため、この状況から抜け出すことはできません。これは確かに信頼できません。

私は実行時にスクリプトファイルに実際に追加される可能性を必要とするPosix標準のどんな表現も知らないので、ほとんどの場合、posixはもちろん、すべてのPosix互換シェルで動作しないことがあります。現在提供されているシェルと互換性があります。だからYMMV。しかし、私が知っている限り、bashでは安定して動作します。

証拠として、ここに上書きと追加を使用するbashの悪名高い99本ビールプログラムの「ループなし」の実装がありますdd(上書きは常にファイルである現在実行されている行を完全なコメントに置き換えるため、おそらく安全です。 )。同じ長さで、自己修正操作なしで最終結果を実行できるようにするには、これを行います。

#!/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}   #

答え2

bash実行する前にコマンドを読むことは非常に役立ちます。

たとえば、

cmd1
cmd2

シェルはスクリプトをチャンクとして読み取るので、2つのコマンドを読み、最初のコマンドを解釈してから、スクリプトの終わりに戻ってcmd1スクリプトを再読み込みしてcmd2実行できます。

次のように簡単に確認できます。

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

(しかし、出力を見ると、strace数年前に同じことをしようとしたときよりも少しクールな作業(たとえば、データを何度も読み取る、振り返るなど)を実行するようです。ではありません)。

ただし、スクリプトを次のように書くと:

{
  cmd1
  cmd2
  exit
}

シェルは最後まで読んで}メモリに保存して実行する必要があります。exitシェルはスクリプトを再読み込みしないため、シェルがスクリプトを解釈している間にスクリプトを安全に編集できます。

または、スクリプトを編集するときは、スクリプトの新しいコピーを作成する必要があります。シェルは元のファイルを読み続けます(削除または名前が変更されても)。

これを行うには、名前を変更、コピー、編集the-scriptします。the-script.oldthe-script.oldthe-script

答え3

実際、スクリプトの実行中にスクリプトを変更する安全な方法はありません。これは、シェルがバッファリングを使用してファイルを読み取ることができるためです。さらに、スクリプトを新しいファイルに置き換えて変更すると、シェルは通常、特定の操作を実行した後にのみ新しいファイルを読み込みます。

多くの場合、実行中にスクリプトが変更されると、シェルは構文エラーを報告します。これは、シェルがスクリプトファイルを閉じて再度開くと、ファイルのバイトオフセットを使用して戻り値の位置を変更するためです。

答え4

スクリプトにトラップを設定してからを使用してexec新しいスクリプトコンテンツをインポートすると、この問題を解決できます。ただし、このexec呼び出しは実行中の場所ではなく最初からスクリプトを開始するため、スクリプトBが呼び出されます(続き)。

#! /bin/bash

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

trap reexec 1

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

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

これにより、画面に日付が表示され続けます。その後、スクリプトを編集してdateに変更できますecho "Date: $(date)"。スクリプトを作成した後もスクリプトを実行すると、まだ日付のみが表示されます。ただし、キャプチャするシグナルセットを送信すると、スクリプトtrapexec(現在実行中のプロセスを指定されたコマンドに置き換えます)、つまりコマンドと$CMD引数を実行します$@kill -1 PIDこれを実行すると(ここでPIDは実行中のスクリプトのPIDです)、出力がコマンド出力のDate:前に表示されるように変更されますdate

スクリプトの「状態」を外部ファイル(例:/ tmp)に保存して内容を読み、プログラムが再実行されたときに「再開」する場所を知ることができます。その後、追加のトラップシャットダウン(SIGINT / SIGQUIT / SIGKILL / SIGTERM)を追加して、「スクリプトA」を停止してから再起動したときに最初から始まるようにtmpファイルを消去できます。ステートフルバージョンは次のとおりです。

#! /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

関連情報