Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Shellの保守性に関するTips

Avatar for WHIsaiyo WHIsaiyo
July 28, 2025
1

 Shellの保守性に関するTips

7/3に開催した「CI/CDについて語る会」の資料です。
https://workshumanintelligence.connpass.com/event/359807/

GOAMI, Takaaki @goataka_

Avatar for WHIsaiyo

WHIsaiyo

July 28, 2025
Tweet

More Decks by WHIsaiyo

Transcript

  1. 自己紹介 役割 エンジニアリング・マネージャー DevOps ex. CI/CD, SET, DX 経歴 アプリケーション開発(10年)

    プロダクトオーナー(8年) DevOpsエンジニア(5年) 好きなライブラリ CodeceptJS AWS CDK WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 2
  2. Shell Scriptの重要性 実に様々な場面で利用されています。 目的: 環境構築、開発、テスト、CI/CD、運用自動化 工程: セットアップ、ビルド、デプロイ、テスト実行など 環境: ローカル、CI/CD、クラウドなど ツール:

    Jenkins、GitHub Actions、AWS CodeBuildなど CI/CDや運用自動化において、Shell Scriptは不可欠な存在です。 WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 3
  3. Shell Scriptの課題 主に保守性の観点で課題が多いです。 解析性:読みづらい。 ex. 縦長なソース 修正性:直しづらい。 ex. 変数のスコープが広い 試験性:テストしづらい。

    ex. テストコードが書けない モジュール性:影響範囲が広い。 ex. グローバル変数の多用 再利用性:再利用が難しい。 ex. コードの重複 WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 4
  4. 改善後 保守性を考えて改善をした結果です。 #!/bin/bash set -eu pipefail log() { local -r

    message="${1}" local -r prefix="INFO" echo "${prefix}: ${message}" } main() { local -r arg="${1:? "arg is required."}" local -r env="${2:- "${ENV:? "env is required."}"}" log "${arg}" log "${env}" } main "$@" 一見複雑に見えますが、保守性は大幅に向上しています。 WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 9
  5. 実行オプションを指定する 想定されない動作を防止する。 set -eu pipefail -e : エラー時にスクリプトを終了する。 -u :

    未定義の変数を参照した場合にエラーとする。 pipefail : パイプラインの最後のコマンドが失敗した場合にエラーとする。 WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 12
  6. 関数の利用 ブロックに分割する。 log() { # 処理 } 処理は関数内に記述する。 WHI LT

    CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 13
  7. main関数の利用 エントリポイントを明確にする。 main() { # 関数 } main "$@" main

    では関数の呼び出しのみをする。 main 関数のみ実行する。 $@ で、shellの引数をmain関数に渡す。 WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 14
  8. ローカル変数の利用 参照範囲を限定する。 log() { # 処理 local -r prefix="INFO" echo

    "${prefix}: ${message}" } 変数も関数内に記述する。 local で、ローカル変数とする。 関数内に限定される。 ${} で、変数部分を明示する。 NG: $prefix: 境界がわかりづらい。 WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 15
  9. 引数の明示 引数に命名し、不変にする。 log() { local -r message="${1}" } 引数はローカル変数として受け取る。 -r

    で、読み取り専用とする。 WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 16
  10. 引数の必須化 main() { local -r arg="${1:? "arg is required."}" }

    :? で、必須に指定する。 未定義の場合、エラーとする。 エラーメッセージに引数名を含める。 WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 17
  11. 環境変数の分離 グローバル変数の境界を分離する。 local -r env="${2:-"${ENV:? "env is required."}"}" 環境変数は引数で受け取れるようにする。 テスト時には引数で指定できる。

    :- で、省略時に環境変数から取得する。 実行時は環境変数から取得する。 :? で、未定義時にエラーとする。 WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 18
  12. 静的解析ツール の導入 既知の問題を検出し、修正を促す。 Shellcheck https://www.shellcheck.net/ VSCode拡張: ShellCheck https://marketplace.visualstudio.com/items? itemName=timonwong.shellcheck GitHub

    Actions: action-shellcheck - Run shellcheck with reviewdog https://github.com/reviewdog/action-shellcheck WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 21
  13. 変数展開の活用 記述量を減らし、不要な保守性を向上させます。 echo "${parameter}" # 参照 echo "${parameter:-word}" # デフォルト値(代入なし)

    echo "${parameter=word}" # 変数未定義時デフォルト値(代入あり) echo "${parameter:?word}" # 未定義時のエラー出力 echo "${parameter:offset:length}" # 部分展開(文字数指定あり) echo "${#parameter}" # 文字数カウント echo "${parameter#word}" # 前方一致除去(最短一致) echo "${parameter%word}" # 後方一致除去(最短一致) echo "${parameter/pattern/string}" # 文字列置換 echo "${parameter^^}" # 大文字化(全て) echo "${parameter,,}" # 小文字化(全て) 参考:[Qiita] 【シェル芸人への道】Bashの変数展開と真摯に向き合う WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 22
  14. ロングオプションの利用 意味が分かり易い表記で、可読性を向上させる。 # ショート curl -X GET "https://api.example.com/data" -H "Authorization:

    Bearer token" # ロング curl --request GET https://api.example.com/data" --header "Authorization: Bearer token" WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 23
  15. 引数が多い場合の改行 改行する事で可読性を向上させる。 curl --request GET \ "https://api.example.com/data" \ --header "Authorization:

    Bearer token" \ --header "Content-Type: application/json" \ --location \ --silent --show-error \ --output response.json WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 24
  16. 作成 ライブラリ用ファイルを作成する。 ex. logger.sh 読み込み時に実行されないようにする。 全て関数内に記載する log() { # ここに処理を書く

    } # ここには書かない set などは書かない。 set -eu などは呼び出し元で定義する。 汎用的な名前は避ける。 ex. read 呼び出し元と衝突する可能性がある。 WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 26
  17. 別リポジトリ 共通ライブラリを別リポジトリで管理する。 別リポジトリを作成する。 ex. shared-shell ライブラリ用のファイルを配置する。 ex. logger/logger.sh Git submodule

    を利用する git submodule add \ https://github.com/your-org/shared-shell.git \ .submodules/shared-shell source コマンドを利用する。 source "$(dirname "${BASH_SOURCE[0]}")/../../.submodules/shared-shell/logger/logger.sh" WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 28
  18. 呼び出し 読み込んだ関数を呼び出す。 log "This is a log message" 通常の関数と同様に呼び出す。 WHI

    LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 29
  19. ログレベルの管理 レベルを保持する変数は関数経由で扱う。 # logger.sh # ログレベルを取得する get_log_level() { echo "${LOG_LEVEL:-info}"

    } # ログレベルを設定する set_log_level() { local -r level="${1}" LOG_LEVEL="${level}" # ここはグローバル変数 } WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 31
  20. ログレベルの判定 保持しているレベルと比較して、ログ出力の可否を判定する。 # logger.sh # ログレベルを数値に変換する log_level_num() { local -r

    level="${1}" declare -Ar LOG_LEVEL_MAP=([debug]=0 [info]=1 [warn]=2 [error]=3) echo "${LOG_LEVEL_MAP[${level}]:-${LOG_LEVEL_MAP["info"]}}" } # ログレベルが指定レベル以上かを判定する should_log() { local -r level="${1}" [ "$(log_level_num "${level}")" -ge "$(log_level_num "${LOG_LEVEL}")" ] } WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 32
  21. ログの書き出し 時刻を付けて、ログを出力する。ファイル出力はなし。 # logger.sh # ログ出力関数 log() { local -r

    level="${1}" local -r msg="${2}" if ! should_log "${level}"; then return fi local -r ts="$(TZ=Asia/Tokyo date '+%Y-%m-%d %H:%M:%S')" if [[ "${level}" == "error" ]]; then echo "${ts} [${level}] ${msg}" >&2 # エラーログは標準エラー出力 else echo "${ts} [${level}] ${msg}" >&1 # 通常のログは標準出力 fi } debug() { local -r msg="${1}" log "debug" "${msg}" } # info, warn, error関数も同様に定義する WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 33
  22. テストコードの例 # test_logger.sh source "$(dirname "${BASH_SOURCE[0]}")/test_helper.sh" get_log_level_test() { ( source

    "$(dirname "${BASH_SOURCE[0]}")/logger.sh" # Given set_log_level "debug" # When local -r log_level="$(get_log_level)" # Then assert_equals "${log_level}" "debug" ) } main() { get_log_level_test } WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 35
  23. assert_equals関数 assert_equals() { local -r actual="${1}" local -r expected="${2}" if

    [[ "${expected}" == "${actual}" ]]; then return 0 fi print_test_failure \ "Expected: ${expected}" \ "Actually: ${actual}" } WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 36
  24. print_test_failure関数 source "$(dirname "${BASH_SOURCE[0]}")/logger.sh" # テスト失敗時のメッセージを出力する print_test_failure() { local -r

    func_name="$(get_external_funcname)" error "\033[31m%s\033[m\n" "Test failed: ${func_name}" # 赤色で出力 for arg in "$@"; do echo " ${arg}" done echo "" } # 呼び出し元の関数名を取得する get_external_funcname() { for i in "${!BASH_SOURCE[@]}"; do [[ "${BASH_SOURCE[$i]}" == "${BASH_SOURCE[0]}" ]] && continue echo "${FUNCNAME[$i]}" return done exit 1 } WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 37
  25. パラメータライズドテストの例 # test_logger.sh source "$(dirname "${BASH_SOURCE[0]}")/test_helper.sh" # パラメータライズドでのget_log_levelのテスト get_log_level_parameterized_test() {

    local -r log_level="${1}" local -r expected="${2}" ( source "$(dirname "${BASH_SOURCE[0]}")/logger.sh" # Given set_log_level "${log_level}" # When local -r actual="$(get_log_level)" # Then assert_equals "${actual}" "${expected}" ) } # パラメータライズドテストの例 get_log_level_test() { local -ar test_cases=( "debug,debug" "info,info" "warn,warn" "error,error" "unknown,info" # デフォルト値 ) execute_parameterized_test "get_log_level_parameterized_test" "${test_cases[@]}" } main() { get_log_level_test } WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 38
  26. execute_parameterized_test関数 # test_helper.sh # テストケースの出力 print_test_case() { local -r param_text="${1}"

    info "Case: ${param_text}" } # パラメータを指定してテストを実行する execute_parameterized_test() { local -r function_name="${1}" shift local -ar parameters=("$@") for parameter in "${parameters[@]}"; do local -a args IFS=, read -r -a args <<< "${parameter}" print_test_case "${args[*]}" "${function_name}" "${args[@]}" done } WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 39
  27. Mockの利用 関数をモックすることで、外部依存を排除し、テストを容易にします。 # test_example.sh source "$(dirname "${BASH_SOURCE[0]}")/test_helper.sh" example_test() { (

    # Mock curl() { echo "data" # 常にdataを返す } ) } WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 40
  28. 参考資料 Bash Reference Manual ShellCheck Google Shell Style Guide Bashの変数展開と真摯に向き合う

    WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 44
  29. 代表的な Shell 色々とあるが bash がデフォルトシェルとして広く使われている。 sh: Bourne Shell ・・・ 最初のUnixシェル

    bash: Bourne-Again shell ・・・ shの後継、デフォルトシェルとして広く利 用されている csh: C Shell ・・・ C言語風の構文、スクリプトにはあまり使われない tsch: TENEX C shell ・・・ C Shellの拡張、Bashの機能も取り入れた ksh: KornShell ・・・ Bourne ShellとC Shellの機能を統合 zsh: Z Shell ・・・ kshの拡張、Bashの機能も取り入れ、多機能 fish: Friendly interactive shell ・・・ ユーザーフレンドリーなインタラクティブ シェル WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 48
  30. Shellの実行環境 Shellスクリプトは様々な環境で実行される。 ローカル Linux macOS Windows(WSL) CI Jenkins AWS CodeBuild

    GitHub Actions WHI LT CI/CDについて語る会 - Jul. 3rd, 2025 © 2025 Works Human Intelligence Co., Ltd. 49