実行中スクリプトの編集

実行中スクリプトの編集



まず、スクリプトの実行についておさらいしておきます。

スクリプトは、テキストを読んで命令文を逐次解釈し、記述された順番でコマンドを実行するものです。
これはコマンドインタープリタの標準的な挙動です。

そのため、シェルスクリプトも Windows バッチファイルも、実行中にファイルを書き換えたり、消したりするとエラー終了します。

検証方法

検証には、次のようなスクリプトを書くとよいでしょう。
#!/bin/bash
echo "Start"
sleep 30
echo "End"
sleep で待ち時間が発生している間に、End を書き換えたり、追記したり、ファイルを消したりすればよいわけです。

実際にやってみれば、書き換わった後に追記したコードが実行されたり、エラーが出たりと、予想できない挙動を示すでしょう。
そういった理由から誤って実行中にバージョンアップしてしまったり、自分自身を書き換えてしまうバグを作りこんでしまったときには、非常に困るわけです。

不意の修正に強くするためには

では、対策を考えてみます。

そもそもの仕様としてファイルへの読み込みが発生するので、実行中スクリプトの修正に書き換えを行わせないための策は、根本的にありません。

ですが、少しでも強くする方法はあります。
(1) 適切な権限を設定する
「書き込み権限があるので書き込めるのだ」という発想で、書き込み権限を消しておく方法です。

$ sudo chmod -w sample.sh

頻繁に修正の発生しない、もしくは発生しては困る本番環境では、考慮されているものと思われます。
デメリットとしては、修正を行う時は毎回権限を付与する、もしくは sudo を利用して書き換えるといった手間や権限の乱用に繋がる点でしょう。
(2) 書換を検知する
スクリプト自体に、自身の書き換えを検知させる方法としては、ls や file、stat といったコマンドで自身の更新時刻を管理させます。

頻繁に実行する内容になるでしょうから、関数として呼び出すのが自然でしょう。

Linux bash では、関数にすると環境変数としてセットされるようです。
何が環境変数としてセットされているかは set コマンドで確認できます。

環境変数はメモリに展開されるので、ファイル編集の影響をうけません。
ということは、関数はファイル編集だけならば影響をうけません。

環境変数を書き換える行為(関数の再定義)が必要です。

つまり、スクリプト始めにファイルの更新時刻を環境変数として持っておき、以後は適宜差分をみる方法がとれます。

差分自体は test コマンドで比較すれば、そう難しいロジックを組むことなく実装できるでしょう。


かなり荒々しいですが、次のようなサンプルでも差分検出としては要件を満たせるでしょう。

#!/bin/bash
before="$(ls -l $0)"

functionDiff() {
  after="$(ls -l $0)"
  [ "${before}" == "${after}" ] || echo "NG"
}

functionDiff
また、md5sum といった、ファイルの破損や編集を検知するためのチェックサム関係コマンドを利用するのも良いです。

(3) メモリに読み込む 1
関数はメモリに置かれた内容が実行されるので、影響を受けにくいことは前段で記述しました。
ならば、出来るだけ関数にしてしまえば影響を極力排除できるだろう、という発想です。

関数などは別ファイルに記述できるので、究極、制御系(判断分岐や関数実行)しかないスクリプトになります。
関数名に気を遣えば抽象的な構文になるので、かなり読みやすいスクリプトになると思います。

外部ファイル /tmp/example.conf

### example.conf

# Parameter or Variable
example="EXAMPLE"

# Function
exFunction() {
  echo "${example}"
}

実行ファイル /tmp/example.sh

#!/bin/bash
### Script name: example.sh
# Include
. ./example.conf

# Main
exFunction

(4) メモリに読み込む 2
スクリプトに記載されている内容を、一度メモリに読み込んでおくという手法も取れます。

実体としては、一度、実行したいスクリプトを変数に読み込んで、echo してあげて1行ずつ解釈していく方法になるでしょう。


実行したいスクリプト /tmp/example.sh
#!/bin/bash
# script name : example.sh
echo a
echo b
echo c

注意事項として bash の echo では、変数を展開するときダブルクォートがあるかどうかで、出力形式が変わります。
具体的には、次の通りです。

envbar=$(cat /tmp/example.sh)
$ echo ${envbar}
#!/bin/bash # script name : example.sh echo a echo b echo c
$
$
$ echo "${envbar}"
#!/bin/bash
# script name : example.sh
echo a
echo b
echo c

余談ですが、変数名のブラケット と は無くても大丈夫です。付けておくと、変数展開がしやすいのでお勧めです。
なお、シングルクォートでは変数展開が行われません。

サンプルとしては、次の通りです。

example.sh を実行するコマンド

envbar=$(cat /tmp/example.sh)
$ echo "${envbar}" | bash -
a
b
c

まとめ

実行中の書き換えに対する予防策は、アクセス権限だったり検知機構だったり、ファイル内容の取得だったりといろいろ示しましたが、意図して起こした場合には無力です。

複数の防御手段を併用するのが、一番強力です。

感想

変数への読み込みを行うメリットは、メモリに展開される関係で処理速度が段違いに早くなることも考慮できるでしょう。
特に、HDD といった比較的遅い媒体からの読み出しとオンメモリでは、比較になりません。

変数は int (32bit) で定義されていることが多いようなので 4GB までは対応していそう*1です。

変数への取り込みは、スクリプト内で変数へ読み込んでおいて、1行ずつデータを解析していくにも良さそう。

技術の引き出しが多いことに越したことはないので、他にも方法があれば教えていただきたいです。

*1 : メモリに展開する以上、メモリの最大容量にも依存するものと思われる。