web-dev-qa-db-ja.com

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

Linuxでのプロセスの処理方法に関する誤解の結果かもしれない一般的な質問があります。

私の目的のために、現在のユーザーに対して実行権限が有効になっているテキストファイルに保存されたbashコードのスニペットとして「スクリプト」を定義します。

私は互いに連携して呼び出す一連のスクリプトを持っています。簡単にするために、スクリプトA、B、Cと呼びます。スクリプトAは一連のステートメントを実行してから一時停止し、スクリプトBを実行してから一時停止し、スクリプトCを実行します。つまり、シリーズステップの数は次のようなものです:

スクリプトAを実行:

  1. 一連のステートメント
  2. 一時停止
  3. スクリプトBを実行
  4. 一時停止
  5. スクリプトCを実行

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

ここで本当の質問ですが、スクリプトAの実行中にスクリプトAを編集する方法はありますか?または、実行が始まると編集は不可能ですか?

32

Unixでは、ほとんどのエディタは、編集された内容を含む新しい一時ファイルを作成することによって機能します。編集したファイルを保存すると、元のファイルは削除され、一時ファイルの名前が元の名前に変更されます。 (もちろん、データ損失を防ぐためのさまざまな保護手段があります。)これは、たとえば、-i( "in-place")フラグで呼び出されたときにsedまたはPerlによって使用されるスタイルです。完全に」。それは「古い名前の新しい場所」と呼ばれるべきでした。

「削除」され、同じ名前の新しいファイルが作成された場合でも、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}   #
22
rici

bashは、コマンドを実行する直前に確実にコマンドを読み取るのに役立ちます。

たとえば、

cmd1
cmd2

シェルはブロックごとにスクリプトを読み取るため、両方のコマンドを読み取り、最初のコマンドを解釈してから、スクリプト内のcmd1の最後までシークし、再度スクリプトを読み取ってcmd2を読み取って実行します。 。

簡単に確認できます。

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

(それに関するstrace出力を見ると、数年前に同じことを試みたときよりも、いくつかの奇妙なこと(データを数回読み取って、シークバックなど)をしているようです。 lseekingについての上記のステートメントは、新しいバージョンではもう適用されない可能性があります)。

ただし、スクリプトを次のように記述した場合:

{
  cmd1
  cmd2
  exit
}

シェルは最後の}まで読み取り、それをメモリに格納して実行する必要があります。 exitがあるため、シェルはスクリプトから再度読み取ることはないため、シェルがスクリプトを解釈しているときに安全に編集できます。

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

そのためには、the-scriptの名前をthe-script.oldに変更し、the-script.oldthe-scriptにコピーして編集します。

19

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

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

5
ash

これを回避するには、スクリプトにトラップを設定し、execを使用して新しいスクリプトの内容を取得します。ただし、exec呼び出しは、実行中のプロセスで到達した場所からではなく、スクリプトを最初から開始するため、スクリプトBが呼び出されます(以降同様)。

_#! /bin/bash

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

trap reexec 1

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

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

これにより、画面に日付が表示され続けます。次に、スクリプトを編集して、dateecho "Date: $(date)"に変更します。それを書き込んでも、実行中のスクリプトは日付を表示します。 trapをキャプチャーするように設定した信号を送信すると、スクリプトはexec(現在実行中のプロセスを指定されたコマンドで置き換える)コマンド_$CMD_および引数_$@_。これを行うには、_kill -1 PID_-PIDは実行中のスクリプトのPID-を発行します。出力は、dateコマンド出力の前に_Date:_と表示されます。

スクリプトの "状態"を外部ファイル(/ tmpなど)に格納し、内容を読み取って、プログラムが再実行されたときに "再開"する場所を知ることができます。次に、追加のトラップ終了(SIGINT/SIGQUIT/SIGKILL/SIGTERM)を追加して、そのtmpファイルを消去し、「スクリプトA」を中断した後に再起動すると、最初から開始されるようにします。ステートフルバージョンは次のようになります。

_#! /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
_
4
Drav Sloan