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


46

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

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スクリプトを作成する他の方法はありますか?


3
rm -rf "${build}/*引用符がどこにあるかに関係なく、理由はありません。 rm -rf "${build} のために同じことをしfます。
モンティハーダー

1
shellcheck.netによる静的チェックは、開始するのに非常に堅実な場所です。エディターの統合が利用できるので、まだ使用されているものの定義を削除するとすぐに、ツールから警告を受け取ることができます。
チャールズダフィー

@CharlesDuffyは私の恥に追加するには、私は、インストールされBashSupportとIDEAスタイルのIDEでそのスクリプトを書いた、その場合には警告します。はい、有効なポイントですが、ハードキャンセルが本当に必要でした。
ラファエル

2
わかった。BashFAQ#112の警告に注意してください。現状ではset -uほとんど眉をひそめているわけではありませんset -eが、まだ落とし穴があります。
チャールズダフィー

2
冗談の答え:#! /usr/bin/env rubyすべてのシェルスクリプトの先頭に置き、bashを忘れてください;)
ポッド

回答:


75
set -u

または

set -o nounset

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

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

set -uそして、set -o nounsetされているPOSIXシェルオプション

空の値はなりませんけれども、エラーを誘発します。

そのために、使用

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

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

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

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


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

のGNU実装にrmは、--one-file-systemマウントされたファイルシステムを再帰的に削除することを止めるオプションがありますがrm、実際に引数をチェックする関数で呼び出しをラップすることなく取得できると信じています。


追記として: 展開が文字列の一部として発生する場合${build}$build除いて、次の文字が変数名の有効な文字である場合とまったく同じです"${build}x"


とても良い、ありがとう!1)それはです${build:?msg}${build?msg}?2)ビルドスクリプトなどのコンテキストでは、rmとは異なるツールを使用してより安全に削除できると思います。現在のディレクトリ以外で作業したくないことがわかっているので、制限付きのコマンド。一般的にrmをより安全にする必要はありません。
ラファエル

1
@Raphael申し訳ありませんが、と一緒にいる必要があります。そのタイプミスを今すぐ修正し、後でコンピューターに戻ったときにその重要性について言及します。
クサラナナンダ

2
@Raphael OK、なしで何が起こるかについて短い文を追加しました:(変数が設定されていない場合にのみエラーをトリガーします)。あらゆる状況下で、特定のパスにあるファイルを排他的に削除するツールを作成しようとはしません。パスを解析し、シンボリックリンクなどを気にしない/気にしないのは、現時点では少し面倒です。
クサラナナンダ

1
ありがとう、説明が役立ちます!「ローカルrm」について:私はほとんどの場合、「確実に、それは<toolname>」を求めて釣りをしていましたが、確かにあなたをそれを書いて吸うようにしようとはしていません!オブジェクト指向すべて良い、あなたは十分に助けてくれました!:)
ラファエル

2
@MateuszKoniecznyすみません。これらのような安全対策は必要なときに使用すべきだと信じています。環境を安全にするすべてのものと同様に、最終的には人々をますます不注意にし、安全対策に依存するようになります。それぞれの安全対策が何をするのかを把握し、必要に応じて選択的に適用する方が良いでしょう。
クサラナナンダ

12

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

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

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

[ -n "${build}" ]チェック"${build}"され、非ゼロ長ストリング

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

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

これは、常に偽である${build}ため、すべての使用を削除した場合にも保護されます[ -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.

公正だが、少し扱いに​​くい。私は何かを見逃していますか、それとも${variable:?word}@Kusalanandaが提案するのとほとんど同じですか?
ラファエル

@Raphaelは、「文字列に値があるか」の場合でも同様に動作しますが、test(つまり[-d(引数はディレクトリです)、-f(引数はファイルです)、-O(ファイル現在のユーザーが所有しています)など。
Centimane

1
@Raphaelも、検証はコード/スクリプトの通常の部分である必要があります。また、ビルドのすべてのインスタンスを削除した場合、${build:?word}それも一緒に削除しませんでしたか?${variable:?word}変数のすべてのインスタンスを削除すると、この形式では保護されません。
Centimane

1
1)仮定を確実に保つ(ファイルが存在するなど)ことと、変数が設定されているかどうかを確認すること(コンパイラ/インタープリターの仕事)には違いがあると思います。後者を手でやらなければならない場合は、それのための超おしゃれな構文があります-本格的なものでifはありません。2)「$ {build:?word}も一緒に削除しませんか?」-シナリオは、変数の使用法を見逃したということです。使用${v:?w}すると、損傷から保護されます。すべての使用法を削除していた場合、プレーンアクセスであっても有害ではなかったことは明らかです。
ラファエル

すべては言ったことを、あなたの答えは、公正かつ一般的な名ばかりの質問にhelful応答である:確か仮定ホールドを作ること周りのスクリプトでその滞在重要。質問本文の特定の問題は、私見ですが、Kusalanandaがよりよく答えています。ありがとう!
ラファエル

4

あなたの特定のケースでは、私は過去に「削除」を作り直し、代わりにファイル/ディレクトリを移動しました(/ 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でクリーンアップできます。

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


2
「これにより、システムがアクティブに使用されている間、はるかに高速なクリーンアップが行われます」-それはありますか?両方とも、影響を受けるiノードだけを変更することだと思っていたでしょう。別のパーティション上にある場合、確かに高速ではありません/tmp(少なくともユーザー空間で実行されるスクリプトの場合は、常に私にとってはそうだと思います)。次に、ごみ箱フォルダを変更する必要があります(OSのからの利益はありません/tmp)。
ラファエル

1
mktemp -d別のプロセスのつま先を踏むことなく、一時ディレクトリを取得するために使用できます(そして、正しく尊重します$TMPDIR)。
トビースパイト

両方からの正確で役立つメモを追加しました、ありがとう。あなたが持ち出したポイントを考慮せずに、私は最初にこれをあまりにも早く投稿したと思います。
user117529

2

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

rm -rf $ {variable:-"/ nonexistent"}


1

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

-e手段は、「最初のuncathedで失敗のE rror」。

-u手段は、「最初の使用に失敗U ndeclared変数」。

詳細については、「非公式のBash厳密モードを使用する(Looove Debuggingを使用しない場合)」を参照してください。また、著者は使用することをお勧めしますset -o pipefail; IFS=$'\n\t'が、私の目的ではこれはやり過ぎです。


1

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

ほとんどの場合、$buildディレクトリのコンテンツを削除するためにディレクトリのコンテンツをグロブする必要はありませんが、実際の$buildディレクトリ自体は必要ありません。したがって、無関係なものをスキップ*するrm -rf /と、デフォルトでは過去10年間のほとんどのrm実装は実行を拒否します(GNU rmの--no-preserve-rootオプションでこの保護を無効にしない限り)。

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

rm: can't remove '': No such file or directory

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


-2

あなたしている言語の用語をプログラミングで思考が、bashはスクリプト言語です:-)だから、全く異なる使用命令全く異なるためパラダイムを言語のパラダイム。

この場合:

rmdir ${build}

以来rmdir非空のディレクトリを削除することを拒否します、あなたは最初のメンバーを削除する必要があります。それらのメンバーが何であるか知っていますよね?これらはおそらくスクリプトのパラメーターであるか、パラメーターから派生したものです。

rm -rf ${build}/${parameter}
rmdir ${build}

ここで、一時ファイルなどのその他のファイルまたはディレクトリを一時ファイルなどに配置するrmdirと、エラーがスローされます。それを適切に処理してから:

rmdir ${build} || build_dir_not_empty "${build}"

この考え方は私にとても役立ちました。


6
あなたの努力に感謝しますが、これは完全に外れています。「これらのメンバーが何であるかを知っている」という仮定は誤りです。コンパイラーの出力を考えてください。make cleanいくつかのワイルドカードを使用します(非常に勤勉なコンパイラーが作成するすべてのファイルのリストを作成しない限り)。また、rm -rf ${build}/${parameter}問題が少しだけ下に移動しているように思えます。
ラファエル

ふむ その読み方を見てください。「ありがとう。しかし、これは元の質問で明らかにしなかったものであるため、一般的な場合により適しているにもかかわらず、あなたの答えを拒否するだけでなく、反対票を投じるつもりです。」ワオ。
リッチ

「質問で書いたもの以外に追加の仮定を立ててから、特別な答えを書いてみましょう。」*肩をすくめ*(あなたのビジネスのことではありません:はダウン投票しませんでした。答えは(少なくとも私には)役に立たなかったので、おそらく持っているべきです。)
ラファエル

ふむ 私は何も仮定せず、一般的な答えを書きました。make問題には何もありません。唯一の適用可能な答えを書くmakeことは、非常に具体的で驚くべき仮定でしょう。私の答えはmake、ソフトウェア開発以外、あなたのものを削除することによってあなたが抱えていた特定の初心者の問題以外の場合に有効です。
リッチ

1
クサラナンダは釣り方を教えてくれました。あなたはやって来て、「平原に住んでいるので、代わりに牛肉を食べてみませんか?」と言いました。
ラファエル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.