bashスクリプティングのベストプラクティス:信頼性が高くパフォーマンスの高いbashスクリプティングのクイックガイド



manapiによるシェルの壁紙



bashスクリプトのデバッグは、特に構造、ロギング、信頼性の問題をタイムリーに考慮せずに既存のコードベースに新しい追加が表示される場合、干し草の山で針を探すようなものです。自分の間違いと、複雑なスクリプトの混乱を管理するときの両方が原因で、このような状況に陥ることがあります。 Mail.ru Cloud Solutions



チームは、スクリプトの作成、デバッグ、および保守を改善するのに役立つガイドラインを含む記事を翻訳しました。信じられないかもしれませんが、毎回機能するクリーンですぐに使用できるbashコードを作成することの満足度に勝るものはありません。



この記事では、著者は過去数年間に学んだことと、彼を不意を突かれたいくつかの一般的な間違いを共有しています。すべてのソフトウェア開発者は、キャリアのある時点でスクリプトを使用して日常の作業タスクを自動化するため、これは重要です。



トラップハンドラー



私が遭遇したほとんどのbashスクリプトは、スクリプトの実行中に予期しないことが発生したときに、効率的なクリーンアップメカニズムを使用したことがありません。



たとえば、カーネルから信号を受信するなど、予期しないことが外部から発生する可能性があります。このようなケースを処理することは、スクリプトが実稼働システムで実行するのに十分な堅牢性を確保するために非常に重要です。私はよく出口ハンドラーを使用して、次のようなシナリオに応答します。



function handle_exit() {
  // Add cleanup code here
  // for eg. rm -f "/tmp/${lock_file}.lock"
  // exit with an appropriate status code
}
  
// trap <HANDLER_FXN> <LIST OF SIGNALS TO TRAP>
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM


trap信号が発生した場合に呼び出されるクリーンアップ関数を登録するのに役立つ組み込みのシェルコマンドです。ただし、SIGINTスクリプトを中断するようなハンドラーには特別な注意を払う必要があります。



また、ほとんどの場合、キャッチするだけEXITで済みますが、実際には、個々の信号ごとにスクリプトの動作をカスタマイズできるという考え方です。



組み込み関数の設定-エラー時のクイック終了



エラーが発生したらすぐに対応し、実行を迅速に停止することが非常に重要です。次のようなコマンドを続行することほど悪いことはありません。



rm -rf ${directory_name}/*


変数directory_nameは未定義であることに注意してください



このようなシナリオに対処するには、組み込み関数を使用することが重要であるsetようなset -o errexitset -o pipefailまたはset -o nounsetスクリプトの先頭に。これらの関数は、ゼロ以外の終了コード、未定義の変数、無効なパイプコマンドなどが検出されるとすぐにスクリプトが終了することを保証します。



#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

function print_var() {
  echo "${var_value}"
}

print_var

$ ./sample.sh
./sample.sh: line 8: var_value: unbound variable


注:などの組み込み関数set -o errexitは、「生の」戻りコード(ゼロ以外)が表示されるとすぐにスクリプトを終了します。したがって、次のようなカスタムエラー処理を導入することをお勧めします。



#!/bin/bash
error_exit() {
  line=$1
  shift 1
  echo "ERROR: non zero return code from line: $line -- $@"
  exit 1
}
a=0
let a++ || error_exit "$LINENO" "let operation returned non 0 code"
echo "you will never see me"
# run it, now we have useful debugging output
$ bash foo.sh
ERROR: non zero return code from line: 9 -- let operation returned non 0 code


このようなスクリプトを使用すると、スクリプト内のすべてのコマンドの動作に注意を払い、予期しないエラーが発生する可能性を予測する必要があります。



開発中にエラーをキャッチするためのShellCheck



ShellCheckの ようなものを開発およびテストパイプラインに統合して、ベストプラクティスに対してbashコードを検証することは価値があります。



ローカルの開発環境でこれを使用して、開発中に見逃した可能性のある構文、セマンティクス、およびいくつかのコードエラーに関するレポートを取得します。これはbashスクリプトの静的分析ツールであり、使用することを強くお勧めします。



終了コードの使用



POSIXリターンコードは、ゼロまたは1だけでなく、ゼロまたは非ゼロです。これらの機能を使用して、さまざまなエラーケースのカスタムエラーコード(201〜254)を返します。



この情報は、発生したエラーの種類を正確に理解し、それに応じて対応するために、ユーザーをラップする他のスクリプトで使用できます。



#!/usr/bin/env bash

SUCCESS=0
FILE_NOT_FOUND=240
DOWNLOAD_FAILED=241

function read_file() {
  if ${file_not_found}; then
    return ${FILE_NOT_FOUND}
  fi
}


注:環境変数を誤ってオーバーライドしないように、定義する変数名には特に注意してください。



ロガー機能



スクリプト実行の結果を簡単に理解するには、適切で構造化されたロギングが重要です。他の高レベルのプログラミング言語と同様に、私は常に__msg_info__msg_errorなどのようなbashスクリプトで独自のロギング関数を使用します。



これは、次の1つの場所でのみ変更を加えることにより、標準化されたロギング構造を提供するのに役立ちます。



#!/usr/bin/env bash

function __msg_error() {
    [[ "${ERROR}" == "1" ]] && echo -e "[ERROR]: $*"
}

function __msg_debug() {
    [[ "${DEBUG}" == "1" ]] && echo -e "[DEBUG]: $*"
}

function __msg_info() {
    [[ "${INFO}" == "1" ]] && echo -e "[INFO]: $*"
}

__msg_error "File could not be found. Cannot proceed"

__msg_debug "Starting script execution with 276MB of available RAM"


私は通常__init、そのようなロガー変数や他のシステム変数が初期化されるか、デフォルト値に設定される、ある種のメカニズムをスクリプトに持たせようとします。これらの変数は、スクリプトの呼び出し中にコマンドラインパラメータから設定することもできます。



たとえば、次のようなものです。



$ ./run-script.sh --debug


このようなスクリプトが実行されると、必要に応じてシステム全体の設定がデフォルトに設定されるか、少なくとも必要に応じて適切なもので初期化されることが保証されます。



私は通常、初期化するものと、UIとユーザーが掘り下げることができる/すべき構成の詳細との間のトレードオフにならないものの選択に基づいています。



再利用とクリーンなシステム状態のためのアーキテクチャ



モジュラー/再利用可能なコード



├── framework
│   ├── common
│   │   ├── loggers.sh
│   │   ├── mail_reports.sh
│   │   └── slack_reports.sh
│   └── daily_database_operation.sh


開発したい新しいbashプロジェクト/スクリプトを初期化するために使用できる別のリポジトリを保持しています。再利用できるものはすべてリポジトリに保存し、この機能を使用したい他のプロジェクトで取得できます。このプロジェクトの編成により、他のスクリプトのサイズが大幅に削減され、コードベースが小さく、簡単にテストできるようになります。



上記の例のように、すべてのログ機能、等__msg_info__msg_error及びそのようなスラックによるレポートなどの他のものは、別々に保持common/*し、動的などの他のシナリオに接続しますdaily_database_operation.sh



クリーンなシステムを残します



スクリプトの実行中に一部のリソースをロードする場合は、そのようなすべてのデータを、たとえば、ランダムな名前の共有ディレクトリに保持することをお勧めし/tmp/AlRhYbD97/*ます。ランダムテキストジェネレータを使用して、ディレクトリ名を選択できます。



rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"


作業が完了すると、そのようなディレクトリのクリーニングは、上記のフックハンドラーで提供できます。一時ディレクトリの削除に注意を払わないと、一時ディレクトリが蓄積され、ある時点で、ディスクがいっぱいになるなど、ホストで予期しない問題が発生します。



ロックファイルの使用



多くの場合、スクリプトの1つのインスタンスのみが常にホスト上で実行されるようにする必要があります。これは、ロックファイルを使用して実行できます。



私は通常、ロックファイルを作成し/tmp/project_name/*.lock、スクリプトの最初でそれらの存在を確認します。これは、スクリプトを正しく終了し、並行して実行されている別のスクリプトによる予期しないシステム状態の変更を回避するのに役立ちます。特定のホストで同じスクリプトを並行して実行する必要がある場合は、ロックファイルは必要ありません。



測定と改善



多くの場合、毎日のデータベース操作など、長期間にわたって実行されるスクリプトを操作する必要があります。このような操作には通常、データのロード、異常のチェック、データのインポート、ステータスレポートの送信などの一連の手順が含まれます。



そのような場合、私は常にスクリプトを別々の小さなスクリプトに分割し、それらの状態と実行時間を次のように報告しようとします。



time source "${filepath}" "${args}">> "${LOG_DIR}/RUN_LOG" 2>&1


後で私はランタイムを見ることができます:



tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"


これは、最適化が必要なスクリプトの問題/遅い領域を特定するのに役立ちます。



幸運を!



他に読むべきこと:



  1. GoおよびGPUキャッシュ。
  2. Mail.ru Cloud SolutionsS3オブジェクトストレージ内のイベント駆動型Webhookベースのアプリケーションの例。
  3. デジタル変換に関するテレグラムチャネル。



All Articles