実行前にシェルスクリプト全体を読み取る方法


35

通常、scrpitを編集する場合、スクリプトの実行中の使用はすべてエラーになりやすいです。

私の知る限り、bash(他のシェルも?)はスクリプトをインクリメンタルに読み取ります。したがって、スクリプトファイルを外部で変更すると、間違ったものの読み取りを開始します。それを防ぐ方法はありますか?

例:

sleep 20

echo test

このスクリプトを実行すると、bashは最初の行(10バイトなど)を読み取り、スリープ状態になります。再開すると、スクリプトには10​​バイト目から異なる内容が含まれることがあります。新しいスクリプトの行の途中にいる可能性があります。したがって、実行中のスクリプトは壊れます。


「スクリプトを外部で変更する」とはどういう意味ですか?
maulinglawns 16

1
関数や何かですべての内容をラップする方法があるかもしれないので、シェルは最初にスクリプト全体を読み取りますか?しかし、関数を呼び出す最後の行はどうですか、EOFまで読み込まれますか?たぶん最後\nを省略すると、トリックを行うでしょうか?サブシェルで十分()でしょうか?私はあまり経験がありません。助けてください!
VasyaNovikov 16

@maulinglawnsは、スクリプトに次のようなコンテンツが含まれてsleep 20 ;\n echo test ;\n sleep 20いて、編集を開始すると、誤動作することがあります。たとえば、bashはスクリプトの最初の10バイトを読み取り、sleepコマンドを理解し、スリープ状態になります。再開後、ファイルには10バイトから始まるさまざまな内容が含まれます。
VasyaNovikov

1
だから、あなたが言っていることは、実行中のスクリプトを編集しているということですか?最初にスクリプトを停止し、編集を行ってから再度開始します。
maulinglawns 16

@maulinglawnsはい、それは基本的にそれです。問題は、スクリプトを停止することは私にとって都合が悪いことであり、それを常に覚えておくのは難しいことです。多分、bashに最初にスクリプト全体を強制的に読み込む方法はありますか?
VasyaNovikov 16

回答:


43

はいbash、特にシェルはファイルを一度に1行ずつ読み取るように注意しているため、対話的に使用する場合と同じように機能します。

ファイルがシーク可能でない場合(パイプなど)、文字をbash超えて読み取らないように、一度に1バイトも読み取ります\n。ファイルがシーク可能な場合、一度に完全なブロックを読み取ることにより最適化されますが、後にシークし\nます。

つまり、次のようなことができるということです。

bash << \EOF
read var
var's content
echo "$var"
EOF

または、自分自身を更新するスクリプトを作成します。それがあなたにその保証を与えなかったなら、あなたはそれをすることができないでしょう。

さて、あなたがそのようなことをしたいということはまれであり、あなたが知ったように、その機能は有用であるよりも頻繁に邪魔になる傾向があります。

これを回避するには、ファイルをインプレースで変更しないようにしてください(たとえば、コピーを変更し、コピーをインプレースに移動します(sed -iまたはperl -pi、または一部のエディターがそうします))。

または、次のようなスクリプトを作成できます。

{
  sleep 20
  echo test
}; exit

(注:とexit同じ行にあることが重要であることに注意してください}

または:

main() {
  sleep 20
  echo test
}
main "$@"; exit

シェルは、exit何かを開始する前までスクリプトを読み取る必要があります。これにより、シェルがスクリプトから再度読み取ることがなくなります。

ただし、スクリプト全体がメモリに保存されます。

これは、スクリプトの解析にも影響する可能性があります。

例えば、中bash

export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'

UTF-8でエンコードされたU + 00E9を出力します。ただし、次のように変更すると:

{
  export LC_ALL=fr_FR.UTF-8
  echo $'St\ue9phane'
}

\ue9コマンドは、この場合、ある解析された時点で有効であった文字セットに展開する前に、exportコマンドが実行されます。

また、いくつかのシェルでsourceaka .コマンドを使用すると、ソースファイルにも同じ種類の問題が発生することに注意してください。

bashただし、sourceコマンドがファイルを解釈する前に完全に読み取る場合は、そうではありません。bash具体的に記述している場合、スクリプトの先頭に追加することで、実際にそれを利用できます。

if [[ ! $already_sourced ]]; then
  already_sourced=1
  source "$0"; exit
fi

(将来のバージョンでbashは、現在制限と見なされている動作を変更できると想像できるので、これに依存しません(bashとAT&T kshは、そのように動作する唯一のPOSIXのようなシェルです)already_sourced変数が環境にないことを前提としているため、このトリックは少し脆弱です。もちろん、BASH_SOURCE変数の内容に影響することは言うまでもありません)


@VasyaNovikov、現時点ではSEに何か問題があるようです(少なくとも私にとっては)。私が私のものを追加したときの答えは数個だけで、コメントは16分前に投稿されたと言っていたとしても、今では明らかになったようです(または、私がビー玉を失っただけかもしれません)。とにかく、ファイルのサイズが大きくなったときに問題を回避するために必要な追加の「終了」に注意してください(回答に追加したコメントに記載されています)。
ステファンシャゼル16

ステファン、私は別の解決策を見つけたと思う。使用すること}; exec trueです。この方法では、ファイルの最後に改行を追加する必要はありません。これは、一部のエディター(emacsなど)にとって使いやすいものです。私が考えることができるすべてのテストが正しく動作する}; exec true
VasyaNovikov

@VasyaNovikov、どういう意味かわかりません。それはどう}; exitですか?終了ステータスも失います。
ステファンシャゼラス

別の質問で述べたように、最初にファイル全体を解析してから、ドットコマンド(. script)を使用する場合に複合ステートメントを実行するのが一般的です。
気味悪い

@schily、はい、私はこの答えでAT&T kshとbashの制限として言及しています。他のPOSIXタイプのシェルには、この制限はありません。
ステファンシャゼラス

12

ファイルを削除するだけです(つまり、コピー、削除、コピーの名前を元の名前に戻します)。実際、多くのエディターはこれを行うように設定できます。ファイルを編集して変更されたバッファを保存すると、ファイルを上書きする代わりに、古いファイルの名前を変更し、新しいファイルを作成して、新しいファイルに新しい内容を入れます。したがって、実行中のスクリプトは問題なく続行するはずです。

vimやemacsですぐに使用できるRCSのようなシンプルなバージョン管理システムを使用することで、変更の履歴があるという二重の利点が得られます。チェックアウトシステムはデフォルトで現在のファイルを削除し、正しいモードで再作成する必要があります。(もちろん、そのようなファイルをハードリンクすることに注意してください)。


「削除」は実際にはプロセスの一部ではありません。適切にアトミックにしたい場合は、宛先ファイルの名前を変更します-削除ステップがある場合、削除後に名前が変更される前にプロセスが停止し、ファイルがまったく残らないというリスクがあります(または、そのウィンドウ内のファイルにアクセスしようとして、利用可能な古いバージョンも新しいバージョンも見つけられない場合)。
チャールズダフィー

11

最も簡単なソリューション:

{
  ... your code ...

  exit
}

この方法では、bashは{}実行する前にブロック全体を読み取り、exitディレクティブはコードブロックの外部で何も読み取られないようにします。

スクリプトを「実行」するのではなく、「ソース」にする場合は、別のソリューションが必要です。これでうまくいくはずです:

{
  ... your code ...

  return 2>/dev/null || exit
}

または、終了コードを直接制御する場合:

{
  ... your code ...

  ret="$?";return "$ret" 2>/dev/null || exit "$ret"
}

ほら!このスクリプトは、編集、ソース化、および実行しても安全です。まだ最初に読み取られているミリ秒単位で変更しないでください。


1
私が見つけたのは、EOFが表示されず、ファイルの読み取りが停止することですが、「バッファリングされたストリーム」処理に絡まり、ファイルの終わりを超えてシークすることになります。ファイルはそれほど増加しませんが、ファイルを以前の2倍以上大きくすると、見た目が悪くなります。バグをすぐにbashメンテナーに報告します。
ステファンシャゼル16


コメントは詳細なディスカッション用ではありません。この会話はチャットに移動さました
テルドン

5

コンセプトの証明。以下は、自分自身を変更するスクリプトです。

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line overwites the on disk copy of the script
cat /tmp/scr2 > /tmp/scr
# this line ends up changed.
echo script content kept
EOF
chmod u+x /tmp/scr
/tmp/scr

変更されたバージョンが表示されます

これは、bashがスクリプトを開くためのファイルハンドルを保持するため、ファイルへの変更がすぐに表示されるためです。

メモリ内のコピーを更新したくない場合は、元のファイルのリンクを解除して置き換えます。

それを行う1つの方法は、sed -iを使用することです。

sed -i '' filename

コンセプトの証明

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line unlinks the original and creates a new copy.
sed -i ''  /tmp/scr

# now overwriting it has no immediate effect
cat /tmp/scr2 > /tmp/scr
echo script content kept
EOF

chmod u+x /tmp/scr
/tmp/scr

エディターを使用してスクリプトを変更する場合、「バックアップコピーを保持」機能を有効にするだけで、エディターが既存のバージョンを上書きするのではなく、変更されたバージョンを新しいファイルに書き込むことができます。


2
いいえ、bashファイルをで開きませんmmap()。対話型で端末デバイスからコマンドを取得するときと同じように、必要に応じて一度に1行ずつ読み取るように注意してください。
ステファンシャゼル16

2

スクリプトをブロックにラップするの{}が最善の方法ですが、スクリプトを変更する必要があります。

F=$(mktemp) && cp test.sh $F && bash $F; rm $F;

2番目に最適なオプション(tmpfsを想定)は、スクリプトで使用すると$ 0が壊れるという欠点があります。

F=test.sh; tail -n $(cat "$F" | wc -l) "$F" | bashファイル全体をメモリに保持しなければならず、$ 0が壊れるため、次のようなものを使用するのは理想的ではありません。

最終変更時刻、読み取りロック、ハードリンクが邪魔されないように、元のファイルに触れることは避けてください。そうすれば、ファイルの実行中にエディターを開いたままにしておくことができ、rsyncはバックアップ用のファイルを不必要にチェックサムする必要がなく、ハードリンクが期待どおりに機能します。

編集時にファイルを置き換えると機能しますが、他のスクリプト/ユーザーに強制できないため、堅牢性が低下します。そして再び、それはハードリンクを壊します。


コピーを作成するものはすべて機能します。tac test.sh | tac | bash
Jasen
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.