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


31

一般的な質問があります。これは、Linuxでのプロセスの処理方法の誤解の結果である可能性があります。

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

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

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

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

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

ここで本当の質問がありますが、スクリプトAが実行中に編集する方法はありますか?または、実行が開始されると編集できませんか?


2
シェルに依存すると思います。ただし、bashを使用していると述べています。シェルがスクリプトを内部的にロードする方法に依存するようです。
-strugee

ファイルを実行する代わりにソースを指定すると、動作が変わる場合があります。
-strugee

1
bashは実行前にスクリプト全体をメモリに読み込むと思います。
w4etwetewtwet

2
@handuel、いいえ、そうではありません。プロンプトで「exit」と入力して、入力したコマンドの解釈を開始するまで待機しません。
ステファンシャゼル

1
@StephaneChazelasはい、ターミナルから読み取ることはありませんが、スクリプトの実行とは異なります。
w4etwetewtwet

回答:


21

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

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

これを実行すると、「No more」で始まり、-1に続き、負の数に無期限に進みます。
ダニエルハーシュコヴィッチ

私がない場合export beer=100の前にスクリプトを実行している予想通り、それは動作します。
ダニエルハーシュコヴィッチ

@DanielHershcovich:まったく正しい。私の側のずさんなテスト。修正したと思います。オプションのcountパラメーターを取ります。パラメータがキャッシュされたコピーと一致しない場合、より良い、より興味深い修正は自動的にリセットすることです。
リチ

18

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出力を見ると、数年前に同じことを試みたときよりもいくつかの凝ったこと(データを数回読み取って、シークするなど)を行うようです。新しいバージョンにはもう適用されません)。

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

{
  cmd1
  cmd2
  exit
}

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

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

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


4

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

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


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)"ます。それを書くと、実行中のスクリプトはまだ日付を表示します。trapキャプチャするように設定したシグナルを送信すると、スクリプトはexecコマンド$CMDと引数である(現在実行中のプロセスを指定されたコマンドに置き換えます)$@。これをkill -1 PID実行Date:するには、date次のコマンドを発行します(PIDは実行中のスクリプトのPIDです)。出力は、コマンド出力の前に表示されるように変更されます。

スクリプトの「状態」を外部ファイル(たとえば/ 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

スクリプトのキャプチャ時$0および$@スクリプトの開始時にこれらの変数をexec代わりに使用することにより、この問題を修正しました。
Dravスローン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.