web-dev-qa-db-ja.com

将来変更されたときに害を及ぼすことに対してbashスクリプトを強化するにはどうすればよいですか?

そこで、ホームフォルダー(より正確には、書き込みアクセス権があったすべてのファイル)を削除しました。起こったことは

build="build"
...
rm -rf "${build}/"*
...
<do other things with $build>

bashスクリプトで、$buildが不要になった後、宣言とその使用法をすべて削除しますが、rmは削除します。 Bashは喜んでrm -rf /*に拡張されます。うん。

私は愚かで、バックアップをインストールし、失った仕事をやり直しました。恥を乗り越えようとする。

さて、私は疑問に思います。そのような間違いが発生しないように、または少なくとも可能性が低くなるようにbashスクリプトを作成するための手法は何ですか?たとえば、私が書いた場合

FileUtils.rm_rf("#{build}/*")

Rubyスクリプトでは、インタプリタはbuildが宣言されていないことについて不平を言っていたでしょう。そのため、言語によって保護されています。

rmを囲う以外に、私がbashで検討したこと(関連する質問の多くの回答にあるように、問題がないわけではありません):

  1. rm -rf "./${build}/"*
    それは私の現在の仕事(Gitリポジトリ)を殺したでしょうが、他には何もありませんでした。
  2. rmのバリアント/パラメーター化。現在のディレクトリの外部で動作する場合は、相互作用が必要です。 (何も見つかりませんでした。)同様の効果。

それですか、この意味で「堅牢」なbashスクリプトを作成する他の方法はありますか?

46
Raphael
set -u

または

set -o nounset

これにより、現在のシェルは未設定の変数の展開をエラーとして扱います。

$ unset build
$ set -u
$ rm -rf "$build"/*
bash: build: unbound variable

set -uおよびset -o nounsetPOSIXシェルオプション です。

empty値はnotでもエラーをトリガーします。

そのためには、

$ rm -rf "${build:?Error, variable is empty or unset}"/*
bash: build: Error, variable is empty or unset

${variable:?word}を展開すると、空または未設定でない限り、variableの値に展開されます。空または未設定の場合、Wordは標準エラーに表示され、シェルは展開をエラーとして扱います(コマンドは実行されず、非インタラクティブシェルで実行されている場合、これは終了します)。 :をそのままにしておくと、set -uの場合と同様に、未設定の値に対してのみエラーがトリガーされます。

${variable:?word}POSIXパラメータ展開 です。

これらのどちらも、set -e(またはset -o errexit)も有効になっていない限り、対話型シェルを終了させません。 ${variable:?word}は、変数が空または未設定の場合にスクリプトを終了させます。 set -uset -eと一緒に使用すると、スクリプトが終了します。


2番目の質問についても。 rmが現在のディレクトリ以外で機能しないように制限する方法はありません。

GNU rmの実装には、マウントされたファイルシステムを再帰的に削除しないようにする--one-file-systemオプションがありますが、これは、rm呼び出しを実際に引数をチェックします。


補足:${build}は、拡張が$buildなどの変数名の有効な文字である文字列の一部として展開されない限り、"${build}x"とまったく同じです。 。

75
Kusalananda

test/[ ]を使用して通常の検証チェックを提案します

次のようにスクリプトを記述していれば、安全でした。

build="build"
...
[ -n "${build}" ] || exit 1
rm -rf "${build}/"*
...

[ -n "${build}" ]は、"${build}"ゼロ以外の長さの文字列 であることを確認します。

||は、bashの論理OR演算子です。最初のコマンドが失敗した場合、別のコマンドが実行されます。

このように、${build}が空/未定義/などでした。スクリプトは終了します(戻りコード1で、これは一般的なエラーです)。

${build}は常にfalseになるため、これにより、すべての使用法[ -n "" ]を削除した場合にも保護されます。


test/[ ]を使用する利点は、他にも多くの意味のあるチェックを使用できることです。

例えば:

[ -f FILE ] True if FILE exists and is a regular file.
[ -d FILE ] True if FILE exists and is a directory.
[ -O FILE ] True if FILE exists and is owned by the effective user ID.
12
Centimane

あなたの特定のケースでは、以前に「削除」をやり直して、代わりにファイル/ディレクトリを移動しました(/ tmpがディレクトリと同じパーティションにあると仮定):

# mktemp -d is also a good, reliable choice
trashdir=/tmp/.trash-$USER/trash-`date`
mkdir -p "$trashdir"
...
mv "${build}"/* "$trashdir"
...

これにより、バックグラウンドでトップレベルのファイル/ディレクトリ参照がソースから$trashdir宛先ディレクトリ構造にすべて同じパーティションに移動し、ディレクトリ構造を歩いてファイルごとのディスクブロックを解放するのに時間を費やす必要がなくなります。その後、そこに。これにより、システムがアクティブに使用されている間は、クリーンアップがはるかに速くなりますが、リブートが少し遅くなります(/ tmpはリブート時にクリーンアップされます)。

または、/ tmp/.trash- $ USERを定期的にクリーンアップするcronエントリは、多くのディスク領域を消費するプロセス(ビルドなど)の/ tmpがいっぱいにならないようにします。ディレクトリが/ tmpとは別のパーティションにある場合、パーティションに同様の/ tmpのようなディレクトリを作成し、代わりにcronでクリーンアップすることができます。

ただし、最も重要なのは、何らかの方法で変数を台無しにした場合、クリーンアップが行われる前に内容を回復できることです。

4
user117529

変数が初期化されていない場合、bashパラメータ置換を使用してデフォルトを割り当てます。例:

rm -rf $ {variable:-"/ non existent"}

2
rackandboneman

私は常にBashスクリプトを#!/bin/bash -ueという行で開始しようとします。

-eは、「最初の無傷errorで失敗する」を意味します。

-uは、「undeclared variableの最初の使用で失敗する」ことを意味します。

詳細については、すばらしい記事 非公式のBash厳格モードを使用する(デバッグが適切でない場合) を参照してください。また、著者はset -o pipefail; IFS=$'\n\t'の使用を推奨していますが、私の目的ではこれはやり過ぎです。

1
niya3

変数が設定されているかどうかを確認するための一般的なアドバイスは、この種の問題を防ぐのに役立つツールです。しかし、この場合はより簡単な解決策がありました。

_$build_ディレクトリ自体を削除するために、それらを削除するために_$build_ディレクトリ自体をグロブする必要はほとんどありません。したがって、無関係な_*_をスキップした場合、未設定の値は_rm -rf /_になり、デフォルトで過去10年間のほとんどのrm実装は実行を拒否します(GNU rmの_--no-preserve-root_オプション)。

末尾の_/_もスキップすると、_rm ''_となり、エラーメッセージが表示されます。

_rm: can't remove '': No such file or directory
_

これは、rmコマンドが_/_の保護を実装していない場合でも機能します。

1
eschwartz