Shell Script

Shell Script 包括的概要ガイド

本記事では、Shell Script(主にBash)の機能・アーキテクチャ・設定例を網羅的に解説します。Linux/macOS環境でのシステム管理・自動化・DevOpsに不可欠なスキルとして、基礎から応用まで体系的にまとめています。


第1章: Shell Scriptとは

1.1 Shell Scriptの定義と歴史

Shell Scriptとは、UNIXシェル(コマンドラインインタプリタ)上で実行される一連のコマンドをファイルにまとめたプログラムのことです。対話的に入力するコマンドをスクリプトファイルとして自動化できるため、システム管理・運用自動化の要となる技術です。

歴史的経緯:

年代シェル開発者特徴
1971Thompson Shell (sh)Ken Thompson最初のUNIXシェル
1977Bourne Shell (sh)Stephen Bourneプログラミング機能の導入
1978C Shell (csh)Bill JoyC言語ライクな構文
1983Korn Shell (ksh)David KornBourne Shell + C Shell の長所を統合
1989Bash (bash)Brian FoxGNU Project、最も普及したシェル
1990Z Shell (zsh)Paul FalstadBashの上位互換、高機能補完
2005Fish (fish)Axel Liljencrantzユーザーフレンドリーな設計

1.2 Shell Scriptの位置づけ

Shell Scriptは以下の領域で中核的な役割を果たします:

┌─────────────────────────────────────────────────┐
│                 アプリケーション層               │
│  (Python, Go, Java, etc.)                       │
├─────────────────────────────────────────────────┤
│              Shell Script 層                     │
│  ┌──────────┐ ┌──────────┐ ┌──────────────────┐│
│  │システム管理│ │ビルド自動化│ │デプロイ/CI/CD    ││
│  └──────────┘ └──────────┘ └──────────────────┘│
│  ┌──────────┐ ┌──────────┐ ┌──────────────────┐│
│  │ログ解析  │ │監視/通知  │ │バックアップ      ││
│  └──────────┘ └──────────┘ └──────────────────┘│
├─────────────────────────────────────────────────┤
│              OS / カーネル層                      │
│  (Linux, macOS, BSD)                            │
└─────────────────────────────────────────────────┘

1.3 主要なシェルの比較

# 現在のシステムで利用可能なシェル一覧
cat /etc/shells

# 現在使用中のシェル確認
echo $SHELL
echo $0

# Bashのバージョン確認
bash --version

# zshのバージョン確認
zsh --version

各シェルの特徴比較:

機能shbashzshfishksh
POSIX準拠YesYesYesNoYes
配列NoYesYesYesYes
連想配列NoYes (4.0+)YesYesYes (93+)
プロセス置換NoYesYesYesYes
拡張globNoYesYesYesYes
自動補完基本基本高度高度基本
テーマ/プラグインNo限定的Oh My Zsh組込みNo
スクリプト互換性基準

1.4 なぜShell Scriptを学ぶのか

  1. システム管理の自動化: サーバー設定、ユーザー管理、パッケージ管理
  2. CI/CDパイプライン: Jenkins、GitHub Actions、GitLab CIのベース
  3. コンテナ技術: Dockerfileのエントリポイント、初期化スクリプト
  4. クラウド運用: AWS CLI、gcloud、az コマンドのオーケストレーション
  5. デバッグとトラブルシューティング: ログ分析、プロセス調査
  6. ポータビリティ: ほぼすべてのUnix/Linux環境で動作

第2章: Shell Scriptの基礎文法

2.1 スクリプトの基本構造

#!/bin/bash
# ============================================================
# スクリプト名: example.sh
# 説明: Shell Scriptの基本構造を示すサンプル
# 作成者: SRE Team
# 作成日: 2026-04-10
# バージョン: 1.0.0
# ============================================================

set -euo pipefail  # 厳格モード
IFS=$'\n\t'        # Internal Field Separator

# グローバル定数
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly LOG_FILE="/var/log/${SCRIPT_NAME%.sh}.log"

# メイン処理
main() {
    echo "Hello, Shell Script!"
    echo "Script: ${SCRIPT_NAME}"
    echo "Directory: ${SCRIPT_DIR}"
}

# エントリポイント
main "$@"

Shebang行の選択:

#!/bin/bash          # Bashを明示的に指定
#!/bin/sh            # POSIX準拠シェル(ポータブル)
#!/usr/bin/env bash  # PATHからbashを検索(推奨)
#!/usr/bin/env zsh   # zshを使用

2.2 set オプションによる厳格モード

#!/usr/bin/env bash

# 各オプションの詳細説明
set -e          # エラー発生時にスクリプトを即座に停止
set -u          # 未定義変数の参照をエラーにする
set -o pipefail # パイプライン内のエラーを検知
set -x          # デバッグモード(実行コマンドを表示)

# 一行でまとめて指定
set -euo pipefail

# 特定ブロックでのみデバッグを有効化
set -x
# デバッグしたいコマンド群
some_command
set +x  # デバッグを無効化

set -e の注意点:

# set -e でもスクリプトが止まらないケース
set -e

# 1. if文の条件式内
if ! some_command; then
    echo "コマンドが失敗しました"
fi

# 2. || や && で繋がれたコマンド
some_command || echo "失敗時のフォールバック"

# 3. サブシェル内のエラー(親シェルには伝播しない場合がある)
(
    false  # サブシェル内で失敗
)
echo "ここは実行される可能性がある"

# 安全な書き方
if ! result=$(some_command 2>&1); then
    echo "エラー: ${result}" >&2
    exit 1
fi

2.3 変数と型

#!/usr/bin/env bash

# --- 変数の宣言と代入 ---
# = の前後にスペースを入れてはいけない
name="Shell Script"       # 文字列
count=42                   # 数値(内部的には文字列)
readonly PI=3.14159        # 読み取り専用定数
export PATH_ADDITION="/opt/bin"  # 環境変数としてエクスポート

# --- declare による型指定 ---
declare -i integer_var=10  # 整数型(算術演算が自動適用)
declare -r constant="immutable"  # 読み取り専用
declare -l lower_str="HELLO"     # 小文字変換 → "hello"
declare -u upper_str="hello"     # 大文字変換 → "HELLO"
declare -a array_var=(1 2 3)     # インデックス配列
declare -A assoc_var=()          # 連想配列

# --- 変数のスコープ ---
global_var="global"

func_example() {
    local local_var="local"     # 関数内でのみ有効
    echo "${global_var}"        # グローバル変数にアクセス可能
    echo "${local_var}"
}

func_example
echo "${local_var:-undefined}"  # "undefined"(関数外では未定義)

# --- 特殊変数 ---
echo "スクリプト名: $0"
echo "引数の数: $#"
echo "全引数: $@"
echo "全引数(1つの文字列): $*"
echo "直前のコマンドの終了コード: $?"
echo "現在のシェルのPID: $$"
echo "最後のバックグラウンドプロセスのPID: $!"
echo "現在の関数名: ${FUNCNAME[0]}"
echo "現在の行番号: $LINENO"

2.4 文字列操作

#!/usr/bin/env bash

str="Hello, World! Hello, Bash!"

# --- 基本操作 ---
echo "文字列長: ${#str}"                    # 26
echo "部分文字列: ${str:7:5}"               # World
echo "末尾から: ${str: -5}"                 # Bash!

# --- パターン削除 ---
filepath="/home/user/documents/report.tar.gz"

echo "ファイル名: ${filepath##*/}"          # report.tar.gz
echo "ディレクトリ: ${filepath%/*}"          # /home/user/documents
echo "拡張子削除(最短): ${filepath%.*}"    # /home/user/documents/report.tar
echo "拡張子削除(最長): ${filepath%%.*}"   # /home/user/documents/report
echo "先頭パス削除(最短): ${filepath#*/}"  # home/user/documents/report.tar.gz
echo "先頭パス削除(最長): ${filepath##*/}" # report.tar.gz

# --- 置換 ---
echo "最初の一致を置換: ${str/Hello/Hi}"    # Hi, World! Hello, Bash!
echo "全一致を置換: ${str//Hello/Hi}"       # Hi, World! Hi, Bash!
echo "先頭一致を置換: ${str/#Hello/Hi}"     # Hi, World! Hello, Bash!
echo "末尾一致を置換: ${str/%Bash!/Zsh!}"   # Hello, World! Hello, Zsh!

# --- 大文字/小文字変換 (Bash 4.0+) ---
text="Hello World"
echo "全大文字: ${text^^}"     # HELLO WORLD
echo "全小文字: ${text,,}"     # hello world
echo "先頭大文字: ${text^}"    # Hello World
echo "先頭小文字: ${text,}"    # hello World

# --- デフォルト値と代入 ---
echo "${undefined_var:-default}"     # 未定義なら"default"を使用
echo "${undefined_var:=default}"     # 未定義なら"default"を代入
echo "${undefined_var:+替値}"        # 定義済みなら"替値"を使用
echo "${undefined_var:?エラーメッセージ}"  # 未定義ならエラーで終了

2.5 配列

#!/usr/bin/env bash

# --- インデックス配列 ---
# 宣言方法
fruits=("apple" "banana" "cherry" "date")
declare -a colors=("red" "green" "blue")

# アクセス
echo "最初の要素: ${fruits[0]}"      # apple
echo "3番目の要素: ${fruits[2]}"     # cherry
echo "全要素: ${fruits[@]}"          # apple banana cherry date
echo "全要素(1つの文字列): ${fruits[*]}"
echo "要素数: ${#fruits[@]}"         # 4
echo "インデックス一覧: ${!fruits[@]}"  # 0 1 2 3

# 追加・変更・削除
fruits+=("elderberry")               # 末尾に追加
fruits[1]="blueberry"                # 変更
unset 'fruits[3]'                    # 削除(インデックスは詰まらない)

# スライス
echo "2番目から3つ: ${fruits[@]:1:3}"

# ループ
for fruit in "${fruits[@]}"; do
    echo "Fruit: ${fruit}"
done

# インデックス付きループ
for i in "${!fruits[@]}"; do
    echo "Index ${i}: ${fruits[i]}"
done

# --- 連想配列 (Bash 4.0+) ---
declare -A user_info=(
    [name]="John Doe"
    [email]="john@example.com"
    [role]="admin"
    [department]="Engineering"
)

# アクセス
echo "Name: ${user_info[name]}"
echo "全キー: ${!user_info[@]}"
echo "全値: ${user_info[@]}"
echo "要素数: ${#user_info[@]}"

# キーの存在確認
if [[ -v user_info[email] ]]; then
    echo "emailキーは存在します"
fi

# ループ
for key in "${!user_info[@]}"; do
    echo "${key}: ${user_info[${key}]}"
done

# --- 配列操作の応用 ---
# 配列のフィルタリング
numbers=(1 2 3 4 5 6 7 8 9 10)
even=()
for n in "${numbers[@]}"; do
    if (( n % 2 == 0 )); then
        even+=("$n")
    fi
done
echo "偶数: ${even[@]}"  # 2 4 6 8 10

# 配列のソート
sorted=($(printf '%s\n' "${fruits[@]}" | sort))

# 配列の結合
joined=$(IFS=','; echo "${fruits[*]}")
echo "カンマ区切り: ${joined}"

# 文字列から配列へ
IFS=',' read -ra parts <<< "a,b,c,d,e"
echo "分割結果: ${parts[@]}"

2.6 算術演算

#!/usr/bin/env bash

# --- 算術展開 ---
a=10
b=3

echo "加算: $((a + b))"       # 13
echo "減算: $((a - b))"       # 7
echo "乗算: $((a * b))"       # 30
echo "除算: $((a / b))"       # 3(整数除算)
echo "剰余: $((a % b))"       # 1
echo "べき乗: $((a ** b))"    # 1000

# インクリメント/デクリメント
((a++))    # a = 11
((b--))    # b = 2
((a += 5)) # a = 16
((b *= 3)) # b = 6

# 三項演算子
max=$(( a > b ? a : b ))
echo "最大値: ${max}"

# --- let コマンド ---
let "result = 5 + 3"
let "result += 2"
echo "result: ${result}"  # 10

# --- expr コマンド(レガシー) ---
result=$(expr 5 + 3)
echo "expr result: ${result}"

# --- bc による浮動小数点演算 ---
# Bashは整数演算のみサポートするため、小数はbcを使用
pi=$(echo "scale=10; 4*a(1)" | bc -l)
echo "Pi: ${pi}"

area=$(echo "scale=4; 3.14159 * 5^2" | bc)
echo "円の面積(r=5): ${area}"

# 浮動小数点の比較
if (( $(echo "3.14 > 3.0" | bc -l) )); then
    echo "3.14は3.0より大きい"
fi

# --- 進数変換 ---
echo "16進数→10進数: $((16#FF))"     # 255
echo "8進数→10進数: $((8#77))"       # 63
echo "2進数→10進数: $((2#11010))"    # 26

# 10進数→他の基数
printf "10進数255 → 16進数: %x\n" 255    # ff
printf "10進数255 → 8進数: %o\n" 255     # 377
echo "10進数26 → 2進数: $(echo "obase=2; 26" | bc)"  # 11010

第3章: 制御構文

3.1 条件分岐 (if/elif/else)

#!/usr/bin/env bash

# --- 基本的なif文 ---
if [[ -f "/etc/passwd" ]]; then
    echo "/etc/passwdファイルが存在します"
fi

# --- if-elif-else ---
score=85

if (( score >= 90 )); then
    grade="A"
elif (( score >= 80 )); then
    grade="B"
elif (( score >= 70 )); then
    grade="C"
elif (( score >= 60 )); then
    grade="D"
else
    grade="F"
fi
echo "成績: ${grade}"

# --- テスト演算子の詳細 ---

# ファイルテスト
file="/etc/hosts"
[[ -e "$file" ]]   # 存在するか
[[ -f "$file" ]]   # 通常ファイルか
[[ -d "$file" ]]   # ディレクトリか
[[ -L "$file" ]]   # シンボリックリンクか
[[ -r "$file" ]]   # 読み取り可能か
[[ -w "$file" ]]   # 書き込み可能か
[[ -x "$file" ]]   # 実行可能か
[[ -s "$file" ]]   # サイズが0より大きいか
[[ -p "$file" ]]   # 名前付きパイプか
[[ -S "$file" ]]   # ソケットか
[[ "$file1" -nt "$file2" ]]  # file1がfile2より新しいか
[[ "$file1" -ot "$file2" ]]  # file1がfile2より古いか

# 文字列テスト
str="hello"
[[ -z "$str" ]]          # 空文字列か
[[ -n "$str" ]]          # 空でないか
[[ "$str" == "hello" ]]  # 等しいか
[[ "$str" != "world" ]]  # 等しくないか
[[ "$str" < "world" ]]   # 辞書順で小さいか
[[ "$str" > "abc" ]]     # 辞書順で大きいか
[[ "$str" =~ ^hel ]]     # 正規表現マッチ

# 数値比較
a=10; b=20
[[ "$a" -eq "$b" ]]  # 等しい (equal)
[[ "$a" -ne "$b" ]]  # 等しくない (not equal)
[[ "$a" -lt "$b" ]]  # より小さい (less than)
[[ "$a" -le "$b" ]]  # 以下 (less or equal)
[[ "$a" -gt "$b" ]]  # より大きい (greater than)
[[ "$a" -ge "$b" ]]  # 以上 (greater or equal)

# 論理演算
[[ condition1 && condition2 ]]  # AND
[[ condition1 || condition2 ]]  # OR
[[ ! condition ]]               # NOT

# --- [[ ]] と [ ] の違い ---
# [[ ]] はBash拡張(推奨)
# - ワード分割されない
# - パス展開されない
# - =~ で正規表現が使える
# - && || が使える

# [ ] はPOSIX準拠
# - ワード分割される(変数をクォートする必要あり)
# - -a -o で論理演算

# 推奨: 常に [[ ]] を使用
if [[ "$str" == "hello" && -f "$file" ]]; then
    echo "条件を満たしています"
fi

3.2 case文

#!/usr/bin/env bash

# --- 基本的なcase文 ---
read -rp "果物を入力してください: " fruit

case "$fruit" in
    apple|Apple|APPLE)
        echo "りんごが選択されました"
        echo "価格: 100円"
        ;;
    banana|Banana)
        echo "バナナが選択されました"
        echo "価格: 80円"
        ;;
    cherry)
        echo "さくらんぼが選択されました"
        echo "価格: 300円"
        ;;
    *)
        echo "不明な果物: ${fruit}"
        exit 1
        ;;
esac

# --- パターンマッチングの活用 ---
filename="report.tar.gz"

case "$filename" in
    *.tar.gz|*.tgz)
        echo "tar+gzip圧縮ファイル"
        tar xzf "$filename"
        ;;
    *.tar.bz2|*.tbz2)
        echo "tar+bzip2圧縮ファイル"
        tar xjf "$filename"
        ;;
    *.tar.xz|*.txz)
        echo "tar+xz圧縮ファイル"
        tar xJf "$filename"
        ;;
    *.zip)
        echo "ZIPファイル"
        unzip "$filename"
        ;;
    *.gz)
        echo "gzip圧縮ファイル"
        gunzip "$filename"
        ;;
    *)
        echo "未対応の形式: ${filename}"
        ;;
esac

# --- Bash 4.0+ の fall-through ---
value="yes"
case "$value" in
    yes|YES|y|Y)
        echo "肯定的な回答"
        ;;&  # 次のパターンも評価する(fall-through)
    yes|no)
        echo "完全な単語での回答"
        ;;
    *)
        echo "その他"
        ;;
esac

3.3 ループ

#!/usr/bin/env bash

# --- for ループ ---
# リストベース
for color in red green blue yellow; do
    echo "Color: ${color}"
done

# 範囲(ブレース展開)
for i in {1..10}; do
    echo "Number: ${i}"
done

# ステップ付き範囲
for i in {0..100..5}; do
    echo "Step: ${i}"
done

# C言語スタイル
for ((i = 0; i < 10; i++)); do
    echo "Index: ${i}"
done

# コマンド出力をループ
for file in /var/log/*.log; do
    [[ -f "$file" ]] || continue
    echo "ログファイル: ${file} ($(wc -l < "$file") 行)"
done

# 配列をループ
servers=("web01" "web02" "db01" "db02" "cache01")
for server in "${servers[@]}"; do
    echo "サーバー: ${server}"
done

# --- while ループ ---
counter=0
while (( counter < 5 )); do
    echo "Counter: ${counter}"
    ((counter++))
done

# ファイルを1行ずつ読み取り
while IFS= read -r line; do
    echo "Line: ${line}"
done < "/etc/hosts"

# コマンド出力を1行ずつ処理
while IFS= read -r proc; do
    echo "プロセス: ${proc}"
done < <(ps aux --sort=-%mem | head -5)

# CSVファイルの処理
while IFS=',' read -r name age city; do
    echo "名前: ${name}, 年齢: ${age}, 都市: ${city}"
done < "data.csv"

# 無限ループ(ポーリング用途)
while true; do
    if curl -sf "http://localhost:8080/health" > /dev/null 2>&1; then
        echo "サービスが起動しました"
        break
    fi
    echo "サービス起動待ち..."
    sleep 2
done

# --- until ループ ---
count=0
until (( count >= 5 )); do
    echo "Until count: ${count}"
    ((count++))
done

# --- ループ制御 ---
# break: ループを抜ける
# continue: 次の反復に進む
# break N: N段階外側のループを抜ける

for i in {1..10}; do
    if (( i == 3 )); then
        continue  # 3をスキップ
    fi
    if (( i == 7 )); then
        break     # 7で終了
    fi
    echo "Value: ${i}"
done

# ネストされたループでの break
for i in {1..3}; do
    for j in {1..3}; do
        if (( i == 2 && j == 2 )); then
            break 2  # 外側のループも抜ける
        fi
        echo "i=${i}, j=${j}"
    done
done

# --- select ループ(メニュー作成) ---
PS3="選択してください: "  # プロンプト文字列

select option in "開始" "停止" "状態確認" "終了"; do
    case "$option" in
        "開始")
            echo "サービスを開始します"
            ;;
        "停止")
            echo "サービスを停止します"
            ;;
        "状態確認")
            echo "状態を確認します"
            ;;
        "終了")
            echo "終了します"
            break
            ;;
        *)
            echo "無効な選択です"
            ;;
    esac
done

3.4 パターンマッチングとGlob

#!/usr/bin/env bash

# --- 基本的なGlobパターン ---
# *    : 任意の文字列(0文字以上)
# ?    : 任意の1文字
# [...]  : 文字クラス
# [!...] : 否定文字クラス

ls *.txt           # .txtで終わるファイル
ls file?.log       # file + 任意の1文字 + .log
ls [abc]*          # a, b, c で始まるファイル
ls [!0-9]*         # 数字以外で始まるファイル

# --- 拡張Glob (Bash: shopt -s extglob) ---
shopt -s extglob

# ?(pattern): 0回または1回の一致
# *(pattern): 0回以上の一致
# +(pattern): 1回以上の一致
# @(pattern): ちょうど1回の一致
# !(pattern): パターン以外に一致

ls *.@(jpg|png|gif)       # 画像ファイルのみ
ls !(*.log|*.tmp)          # .logと.tmp以外のファイル
rm -f *.?(bak|old|tmp)    # バックアップファイルを削除

# --- Globstar (Bash 4.0+) ---
shopt -s globstar

# 再帰的にマッチ
for f in **/*.py; do
    echo "Pythonファイル: ${f}"
done

# --- nullglob ---
shopt -s nullglob

# マッチしない場合、パターン文字列自体ではなく空になる
files=(*.nonexistent)
echo "ファイル数: ${#files[@]}"  # 0

第4章: 関数

4.1 関数の定義と呼び出し

#!/usr/bin/env bash

# --- 関数定義方法 ---
# 方法1: function キーワード
function greet() {
    echo "Hello, ${1:-World}!"
}

# 方法2: POSIX互換(推奨)
say_goodbye() {
    echo "Goodbye, ${1:-World}!"
}

# 呼び出し
greet "Alice"      # Hello, Alice!
greet              # Hello, World!
say_goodbye "Bob"  # Goodbye, Bob!

4.2 引数の処理

#!/usr/bin/env bash

process_args() {
    echo "関数名: ${FUNCNAME[0]}"
    echo "引数の数: $#"
    echo "第1引数: ${1:-なし}"
    echo "第2引数: ${2:-なし}"
    echo "全引数: $@"
    
    # 全引数をループ
    local i=1
    for arg in "$@"; do
        echo "  引数${i}: ${arg}"
        ((i++))
    done
}

process_args "hello" "world" "foo" "bar"

# --- shift を使った引数処理 ---
parse_options() {
    local verbose=false
    local output=""
    local input=""
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -v|--verbose)
                verbose=true
                shift
                ;;
            -o|--output)
                output="$2"
                shift 2
                ;;
            -*)
                echo "不明なオプション: $1" >&2
                return 1
                ;;
            *)
                input="$1"
                shift
                ;;
        esac
    done
    
    echo "Verbose: ${verbose}"
    echo "Output: ${output}"
    echo "Input: ${input}"
}

parse_options -v --output result.txt input.dat

4.3 戻り値とスコープ

#!/usr/bin/env bash

# --- return による終了コード ---
is_positive() {
    local num=$1
    if (( num > 0 )); then
        return 0  # 成功(真)
    else
        return 1  # 失敗(偽)
    fi
}

if is_positive 5; then
    echo "5は正の数です"
fi

# --- コマンド置換による値の返却 ---
calculate_sum() {
    local -i a=$1 b=$2
    echo $((a + b))  # 標準出力に値を出力
}

result=$(calculate_sum 10 20)
echo "合計: ${result}"  # 30

# --- グローバル変数による返却(非推奨だが高速) ---
declare -g RESULT=""

fast_operation() {
    RESULT="計算結果: $((${1} * ${2}))"
}

fast_operation 5 3
echo "${RESULT}"

# --- nameref による参照渡し (Bash 4.3+) ---
swap() {
    local -n ref1=$1
    local -n ref2=$2
    local temp="${ref1}"
    ref1="${ref2}"
    ref2="${temp}"
}

x="Hello"
y="World"
swap x y
echo "x=${x}, y=${y}"  # x=World, y=Hello

# --- 複数の値を返す ---
get_system_info() {
    local -n _hostname=$1
    local -n _kernel=$2
    local -n _uptime=$3
    
    _hostname=$(hostname)
    _kernel=$(uname -r)
    _uptime=$(uptime -p 2>/dev/null || uptime)
}

declare host kern up
get_system_info host kern up
echo "Host: ${host}, Kernel: ${kern}"

4.4 高度な関数パターン

#!/usr/bin/env bash

# --- ライブラリパターン ---
# lib/logging.sh
LOG_LEVEL="${LOG_LEVEL:-INFO}"

log() {
    local level="$1"
    shift
    local message="$*"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    # ログレベルの優先度
    declare -A levels=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3 [FATAL]=4)
    
    if (( ${levels[$level]:-0} >= ${levels[$LOG_LEVEL]:-0} )); then
        printf "[%s] [%-5s] %s\n" "$timestamp" "$level" "$message" >&2
    fi
}

log_debug() { log "DEBUG" "$@"; }
log_info()  { log "INFO"  "$@"; }
log_warn()  { log "WARN"  "$@"; }
log_error() { log "ERROR" "$@"; }
log_fatal() { log "FATAL" "$@"; exit 1; }

# 使用例
log_info "スクリプトを開始しました"
log_warn "ディスク使用率が80%を超えています"
log_error "接続に失敗しました"

# --- デコレータパターン ---
with_retry() {
    local max_retries=$1
    local delay=$2
    shift 2
    local cmd=("$@")
    
    for ((i = 1; i <= max_retries; i++)); do
        if "${cmd[@]}"; then
            return 0
        fi
        log_warn "試行 ${i}/${max_retries} 失敗。${delay}秒後にリトライ..."
        sleep "$delay"
    done
    
    log_error "最大リトライ回数(${max_retries})に達しました"
    return 1
}

# 使用例
check_service() {
    curl -sf "http://localhost:8080/health" > /dev/null
}

with_retry 5 3 check_service

# --- トラップを使ったクリーンアップ ---
cleanup() {
    local exit_code=$?
    log_info "クリーンアップを実行中..."
    
    # 一時ファイルの削除
    [[ -n "${TEMP_DIR:-}" ]] && rm -rf "$TEMP_DIR"
    
    # ロックファイルの解除
    [[ -f "${LOCK_FILE:-}" ]] && rm -f "$LOCK_FILE"
    
    log_info "クリーンアップ完了(終了コード: ${exit_code})"
    exit "$exit_code"
}

trap cleanup EXIT
trap 'log_warn "SIGINTを受信しました"; exit 130' INT
trap 'log_warn "SIGTERMを受信しました"; exit 143' TERM

# 安全な一時ディレクトリ作成
TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/script.XXXXXXXXXX")
log_info "一時ディレクトリ: ${TEMP_DIR}"

第5章: 入出力とリダイレクション

5.1 ファイルディスクリプタとリダイレクション

#!/usr/bin/env bash

# --- 基本的なリダイレクション ---
# 標準出力(fd=1)をファイルに書き込み(上書き)
echo "Hello" > output.txt

# 標準出力をファイルに追記
echo "World" >> output.txt

# 標準エラー出力(fd=2)をファイルに書き込み
ls /nonexistent 2> error.log

# 標準出力と標準エラーを同じファイルに
command > all_output.log 2>&1    # 伝統的な方法
command &> all_output.log         # Bash省略形

# 標準出力と標準エラーを別々のファイルに
command > stdout.log 2> stderr.log

# 標準入力をファイルから読み込み
sort < unsorted.txt

# 標準入力をファイルから読み込み、結果を別ファイルに
sort < unsorted.txt > sorted.txt

# /dev/null に捨てる(出力を抑制)
command > /dev/null 2>&1
command &> /dev/null

# --- ヒアドキュメント ---
cat << 'EOF'
変数展開されないヒアドキュメント
$HOME はそのまま表示される
EOF

cat << EOF
変数展開されるヒアドキュメント
HOME: $HOME
Date: $(date)
EOF

# インデント付きヒアドキュメント(タブが除去される)
cat <<-EOF
	この行のタブは除去される
	$(whoami)
	EOF

# ヒアストリング
grep "pattern" <<< "search in this string"

# --- カスタムファイルディスクリプタ ---
# fd 3 を書き込み用に開く
exec 3> custom_output.txt
echo "Line 1" >&3
echo "Line 2" >&3
exec 3>&-  # fd 3 を閉じる

# fd 4 を読み取り用に開く
exec 4< input_file.txt
while IFS= read -r line <&4; do
    echo "Read: ${line}"
done
exec 4<&-  # fd 4 を閉じる

# fd 3 を読み書き用に開く
exec 3<> readwrite_file.txt
echo "new content" >&3
read -r content <&3
exec 3>&-

# --- 高度なリダイレクション ---
# 標準出力のバックアップとリストア
exec 3>&1                # fd 3 にstdoutをバックアップ
exec > /tmp/captured.log # stdoutをファイルに
echo "This goes to file"
exec 1>&3                # stdoutをリストア
exec 3>&-
echo "This goes to terminal"

# tee による分岐出力
echo "ログメッセージ" | tee -a logfile.txt  # 画面とファイル両方に
echo "エラー" | tee -a logfile.txt >&2       # stderrとファイル両方に

# プロセス置換を使った高度なtee
command | tee >(gzip > output.gz) >(wc -l > linecount.txt) > output.txt

5.2 パイプライン

#!/usr/bin/env bash

# --- 基本的なパイプ ---
# コマンドの出力を次のコマンドの入力に
ls -la | grep "\.txt$" | sort -k5 -n | head -10

# --- 名前付きパイプ (FIFO) ---
mkfifo /tmp/mypipe

# プロデューサー(バックグラウンド)
echo "data through pipe" > /tmp/mypipe &

# コンシューマー
cat < /tmp/mypipe

rm /tmp/mypipe

# --- パイプラインのエラーハンドリング ---
set -o pipefail

# PIPESTATUS で各コマンドの終了コードを確認
false | true | false
echo "パイプラインの終了コード: $?"
echo "各コマンドのコード: ${PIPESTATUS[0]} ${PIPESTATUS[1]} ${PIPESTATUS[2]}"
# 出力: 1 0 1

# --- プロセス置換 ---
# <(command) : コマンド出力をファイルとして扱う
# >(command) : ファイルへの書き込みをコマンド入力として扱う

# 2つのコマンドの出力を比較
diff <(ls dir1/) <(ls dir2/)

# ソートされたファイル同士のjoin
join <(sort file1.txt) <(sort file2.txt)

# 複数のログファイルを同時に処理
paste <(cut -d' ' -f1 access.log) <(cut -d' ' -f7 access.log)

5.3 テキスト処理コマンド

#!/usr/bin/env bash

# === grep ===
# パターン検索
grep "error" /var/log/syslog          # 基本検索
grep -i "error" /var/log/syslog       # 大文字小文字無視
grep -r "TODO" ./src/                  # 再帰検索
grep -n "pattern" file.txt             # 行番号表示
grep -c "pattern" file.txt             # マッチ数
grep -l "pattern" *.txt                # マッチするファイル名のみ
grep -v "debug" log.txt                # 非マッチ行
grep -E "error|warn|fatal" log.txt     # 拡張正規表現(egrep)
grep -P "(?<=error:)\s+\w+" log.txt    # Perl正規表現
grep -A 3 "ERROR" log.txt              # マッチ行の後3行も表示
grep -B 2 "ERROR" log.txt              # マッチ行の前2行も表示
grep -C 2 "ERROR" log.txt              # マッチ行の前後2行

# === sed ===
# ストリームエディタ
sed 's/old/new/' file.txt              # 最初の一致を置換
sed 's/old/new/g' file.txt             # 全一致を置換
sed -i 's/old/new/g' file.txt          # ファイルを直接編集
sed -i.bak 's/old/new/g' file.txt      # バックアップ付き
sed -n '5,10p' file.txt                # 5-10行目を表示
sed '3d' file.txt                      # 3行目を削除
sed '/^$/d' file.txt                   # 空行を削除
sed '/^#/d' file.txt                   # コメント行を削除
sed 's/^/    /' file.txt               # 各行の先頭にインデント追加
sed -n '/START/,/END/p' file.txt       # 範囲内を表示

# 複数の置換を一度に
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt

# === awk ===
# テキスト処理言語
awk '{print $1}' file.txt                         # 1番目のフィールド
awk -F',' '{print $1, $3}' data.csv               # CSVの1,3列目
awk '{sum += $1} END {print sum}' numbers.txt      # 合計
awk '{sum += $1} END {print sum/NR}' numbers.txt   # 平均
awk 'NR >= 5 && NR <= 10' file.txt                 # 5-10行目
awk 'length > 80' file.txt                         # 80文字超の行
awk '!seen[$0]++' file.txt                         # 重複行を削除

# awk スクリプトの例
awk '
BEGIN {
    FS = ","
    OFS = "\t"
    print "Name", "Score", "Grade"
}
NR > 1 {
    name = $1
    score = $2
    if (score >= 90) grade = "A"
    else if (score >= 80) grade = "B"
    else if (score >= 70) grade = "C"
    else grade = "F"
    print name, score, grade
}
END {
    print "Total records:", NR - 1
}' grades.csv

# === sort, uniq, cut, paste, join ===
sort file.txt                    # アルファベット順
sort -n file.txt                 # 数値順
sort -k2 -t',' file.csv          # 2列目でソート(CSV)
sort -r file.txt                 # 逆順
sort -u file.txt                 # ソート + 重複排除

uniq file.txt                    # 連続する重複行を排除
uniq -c file.txt                 # 出現回数を表示
uniq -d file.txt                 # 重複行のみ表示

cut -d',' -f1,3 data.csv         # 1,3列目を抽出
cut -c1-10 file.txt              # 各行の1-10文字目

paste file1.txt file2.txt        # 横に結合
paste -d',' file1.txt file2.txt  # カンマ区切りで結合

# === xargs ===
find . -name "*.tmp" | xargs rm -f
find . -name "*.txt" -print0 | xargs -0 grep "pattern"   # NULL区切り
echo "1 2 3 4 5" | xargs -n2 echo                        # 2つずつ処理
cat urls.txt | xargs -P4 -I{} curl -sf {}                 # 4並列実行

5.4 ユーザー入力の処理

#!/usr/bin/env bash

# --- read コマンド ---
read -rp "名前を入力: " name
echo "こんにちは、${name}さん"

# タイムアウト付き
if read -rt 5 -p "5秒以内に入力: " answer; then
    echo "入力: ${answer}"
else
    echo "タイムアウトしました"
fi

# パスワード入力(エコーバック無効)
read -rsp "パスワード: " password
echo  # 改行
echo "パスワードの長さ: ${#password}"

# 複数の値を読み取り
read -rp "名前 年齢 都市: " name age city
echo "名前=${name}, 年齢=${age}, 都市=${city}"

# 配列として読み取り
read -ra items -p "スペース区切りで入力: "
echo "入力数: ${#items[@]}"

# --- 確認プロンプト ---
confirm() {
    local message="${1:-続行しますか?}"
    local response
    read -rp "${message} [y/N]: " response
    case "$response" in
        [yY]|[yY][eE][sS])
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}

if confirm "ファイルを削除しますか?"; then
    echo "削除を実行します"
else
    echo "キャンセルされました"
fi

第6章: プロセス管理

6.1 プロセスの基礎

#!/usr/bin/env bash

# --- プロセス情報 ---
echo "現在のPID: $$"
echo "親プロセスのPID: $PPID"
echo "シェル名: $0"

# プロセス一覧
ps aux                          # 全プロセス
ps aux --sort=-%mem | head -10  # メモリ使用量トップ10
ps aux --sort=-%cpu | head -10  # CPU使用量トップ10
ps -ef --forest                 # ツリー表示

# プロセスの詳細
ps -p $$ -o pid,ppid,cmd,%mem,%cpu

# /proc ファイルシステム
cat /proc/$$/status    # プロセスのステータス
cat /proc/$$/cmdline   # コマンドライン
ls -la /proc/$$/fd/    # 開いているファイルディスクリプタ

6.2 ジョブ制御

#!/usr/bin/env bash

# --- バックグラウンド実行 ---
long_running_task &
bg_pid=$!
echo "バックグラウンドPID: ${bg_pid}"

# ジョブの管理
jobs              # ジョブ一覧
jobs -l           # PID付きジョブ一覧
fg %1             # ジョブ1をフォアグラウンドに
bg %1             # ジョブ1をバックグラウンドに
kill %1           # ジョブ1を終了

# --- wait ---
pid1=$!
pid2=$!

# 特定のPIDを待機
wait "$pid1"
echo "PID ${pid1} の終了コード: $?"

# 全バックグラウンドプロセスを待機
wait
echo "全プロセスが完了しました"

# --- nohup ---
nohup long_running_script.sh > output.log 2>&1 &
echo "PID: $!"
disown  # シェルからジョブを切り離す

6.3 シグナル処理

#!/usr/bin/env bash

# --- シグナル一覧 ---
# kill -l  で一覧表示
# 主要なシグナル:
# SIGHUP  (1)  : 端末切断
# SIGINT  (2)  : Ctrl+C
# SIGQUIT (3)  : Ctrl+\
# SIGKILL (9)  : 強制終了(トラップ不可)
# SIGTERM (15) : 正常終了要求
# SIGSTOP (19) : 一時停止(トラップ不可)
# SIGUSR1 (10) : ユーザー定義1
# SIGUSR2 (12) : ユーザー定義2

# --- trap コマンド ---
# シグナルハンドラの設定

# 基本的なトラップ
trap 'echo "SIGINT受信"; exit 1' INT
trap 'echo "SIGTERM受信"; exit 0' TERM
trap 'echo "スクリプト終了"; cleanup' EXIT

# 一時ファイルのクリーンアップ
TEMP_FILES=()

create_temp() {
    local tmpfile
    tmpfile=$(mktemp)
    TEMP_FILES+=("$tmpfile")
    echo "$tmpfile"
}

cleanup() {
    echo "クリーンアップ中..."
    for f in "${TEMP_FILES[@]}"; do
        [[ -f "$f" ]] && rm -f "$f"
    done
}

trap cleanup EXIT

# シグナルを使ったグレースフルシャットダウン
RUNNING=true

graceful_shutdown() {
    echo "シャットダウン開始..."
    RUNNING=false
}

trap graceful_shutdown SIGTERM SIGINT

while $RUNNING; do
    echo "処理中... (PID: $$)"
    sleep 1
done

echo "正常終了しました"

# --- トラップの応用例 ---
# デバッグトラップ
trap 'echo "DEBUG: ${BASH_SOURCE[0]}:${LINENO}: ${BASH_COMMAND}"' DEBUG

# ERRトラップ(エラー時に実行)
trap 'echo "ERROR at line ${LINENO}: command \"${BASH_COMMAND}\" failed with exit code $?"' ERR

# RETURNトラップ(関数からの復帰時)
trap 'echo "Function returned"' RETURN

6.4 並列処理

#!/usr/bin/env bash

# --- バックグラウンドプロセスによる並列処理 ---
urls=(
    "https://example.com/api/1"
    "https://example.com/api/2"
    "https://example.com/api/3"
    "https://example.com/api/4"
    "https://example.com/api/5"
)

pids=()

for url in "${urls[@]}"; do
    curl -sf "$url" -o "/tmp/result_$(basename "$url")" &
    pids+=($!)
done

# 全プロセスの完了を待つ
failures=0
for pid in "${pids[@]}"; do
    if ! wait "$pid"; then
        ((failures++))
    fi
done

echo "完了: $((${#pids[@]} - failures))成功, ${failures}失敗"

# --- 最大同時実行数の制御 ---
MAX_PARALLEL=4
running=0

process_item() {
    local item=$1
    echo "処理開始: ${item}"
    sleep $((RANDOM % 5 + 1))  # シミュレーション
    echo "処理完了: ${item}"
}

items=({1..20})

for item in "${items[@]}"; do
    process_item "$item" &
    ((running++))
    
    if (( running >= MAX_PARALLEL )); then
        wait -n  # いずれか1つの完了を待つ (Bash 4.3+)
        ((running--))
    fi
done

wait  # 残りを全て待つ

# --- GNU Parallel の活用 ---
# インストール: brew install parallel (macOS) / apt install parallel (Ubuntu)

# 基本的な並列実行
# parallel echo ::: A B C D

# ファイル処理の並列化
# find . -name "*.gz" | parallel gzip -d {}

# 引数の組み合わせ
# parallel echo {1} {2} ::: A B C ::: 1 2 3

# 最大並列数の指定
# parallel -j4 process_file {} ::: file1 file2 file3 file4 file5

# --- xargs による並列実行 ---
# 4プロセスで並列実行
find . -name "*.log" -print0 | xargs -0 -P4 -I{} gzip {}

# --- coproc (Bash 4.0+) ---
# コプロセス: 双方向パイプでコマンドとやりとり
coproc WORKER {
    while IFS= read -r line; do
        echo "processed: ${line}"
    done
}

echo "hello" >&"${WORKER[1]}"
read -r result <&"${WORKER[0]}"
echo "結果: ${result}"

# コプロセスの終了
exec {WORKER[1]}>&-
wait "$WORKER_PID"

6.5 ロック機構

#!/usr/bin/env bash

# --- flock によるファイルロック ---
LOCK_FILE="/var/lock/my_script.lock"

# 排他ロックを取得してブロック内を実行
(
    flock -n 200 || { echo "ロックの取得に失敗(別のインスタンスが実行中)"; exit 1; }
    
    echo "ロックを取得しました"
    # クリティカルセクション
    echo "処理中..."
    sleep 10
    echo "処理完了"
    
) 200>"$LOCK_FILE"

# --- PIDファイルによるロック ---
PID_FILE="/var/run/my_script.pid"

acquire_lock() {
    if [[ -f "$PID_FILE" ]]; then
        local old_pid
        old_pid=$(cat "$PID_FILE")
        if kill -0 "$old_pid" 2>/dev/null; then
            echo "既に実行中です (PID: ${old_pid})" >&2
            return 1
        else
            echo "古いPIDファイルを削除します"
            rm -f "$PID_FILE"
        fi
    fi
    echo $$ > "$PID_FILE"
    return 0
}

release_lock() {
    rm -f "$PID_FILE"
}

trap release_lock EXIT

if ! acquire_lock; then
    exit 1
fi

# --- mkdir によるアトミックロック ---
LOCK_DIR="/tmp/my_script.lock.d"

if mkdir "$LOCK_DIR" 2>/dev/null; then
    trap 'rmdir "$LOCK_DIR"' EXIT
    echo "ロック取得成功"
else
    echo "ロック取得失敗" >&2
    exit 1
fi

第7章: 正規表現

7.1 基本正規表現 (BRE) と拡張正規表現 (ERE)

#!/usr/bin/env bash

# --- 正規表現のメタ文字 ---
# .     : 任意の1文字
# ^     : 行の先頭
# $     : 行の末尾
# *     : 直前の文字の0回以上の繰り返し
# +     : 直前の文字の1回以上の繰り返し (ERE)
# ?     : 直前の文字の0回または1回 (ERE)
# {n}   : 直前の文字のn回の繰り返し
# {n,m} : 直前の文字のn~m回の繰り返し
# [...]   : 文字クラス
# [^...] : 否定文字クラス
# (...)  : グループ化 (ERE)
# |     : 選択(OR) (ERE)
# \     : エスケープ

# --- [[ =~ ]] によるBash正規表現 ---
# IPアドレスの検証
validate_ip() {
    local ip=$1
    local regex='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
    
    if [[ "$ip" =~ $regex ]]; then
        # BASH_REMATCH でキャプチャグループにアクセス
        echo "有効なIPアドレス形式: ${ip}"
        echo "マッチ全体: ${BASH_REMATCH[0]}"
        echo "最後のオクテットグループ: ${BASH_REMATCH[1]}"
        return 0
    else
        echo "無効なIPアドレス: ${ip}"
        return 1
    fi
}

validate_ip "192.168.1.100"
validate_ip "999.999.999.999"  # 形式は合うが値は不正

# メールアドレスの検証
validate_email() {
    local email=$1
    local regex='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    
    if [[ "$email" =~ $regex ]]; then
        echo "有効: ${email}"
    else
        echo "無効: ${email}"
    fi
}

validate_email "user@example.com"
validate_email "invalid@"

# 日付の検証とパース
parse_date() {
    local date_str=$1
    local regex='^([0-9]{4})-([0-9]{2})-([0-9]{2})$'
    
    if [[ "$date_str" =~ $regex ]]; then
        echo "年: ${BASH_REMATCH[1]}"
        echo "月: ${BASH_REMATCH[2]}"
        echo "日: ${BASH_REMATCH[3]}"
    fi
}

parse_date "2026-04-10"

# --- grep での正規表現 ---
# BRE (Basic Regular Expression)
grep 'error\|warning' log.txt            # OR(BREではエスケープ必要)
grep 'error.\{3\}' log.txt               # 繰り返し(BREではエスケープ必要)

# ERE (Extended Regular Expression)
grep -E 'error|warning' log.txt          # OR
grep -E '[0-9]{1,3}\.[0-9]{1,3}' log.txt # IPアドレスパターン

# PCRE (Perl Compatible Regular Expression)
grep -P '(?<=error: ).*(?= at)' log.txt  # 先読み・後読み
grep -P '\d{4}-\d{2}-\d{2}' log.txt      # \d(数字)

# --- sed での正規表現 ---
# キャプチャグループによる置換
echo "2026-04-10" | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\2\/\3\/\1/'
# 出力: 04/10/2026

# 行内の特定パターンを抽出
echo "Error code: 404, Message: Not Found" | \
    sed -E 's/.*Error code: ([0-9]+).*/\1/'
# 出力: 404

7.2 実用的な正規表現パターン集

#!/usr/bin/env bash

# --- ログ解析 ---
# Apacheアクセスログの解析
log_regex='^([^ ]+) [^ ]+ [^ ]+ \[([^\]]+)\] "([A-Z]+) ([^ ]+) [^"]*" ([0-9]+) ([0-9]+)'

while IFS= read -r line; do
    if [[ "$line" =~ $log_regex ]]; then
        ip="${BASH_REMATCH[1]}"
        date="${BASH_REMATCH[2]}"
        method="${BASH_REMATCH[3]}"
        path="${BASH_REMATCH[4]}"
        status="${BASH_REMATCH[5]}"
        size="${BASH_REMATCH[6]}"
        
        if (( status >= 400 )); then
            echo "ERROR: ${ip} ${method} ${path} -> ${status}"
        fi
    fi
done < access.log

# --- 設定ファイルのパース ---
parse_config() {
    local config_file=$1
    declare -gA CONFIG=()
    
    while IFS= read -r line; do
        # コメントと空行をスキップ
        [[ "$line" =~ ^[[:space:]]*# ]] && continue
        [[ "$line" =~ ^[[:space:]]*$ ]] && continue
        
        # key = value のパース
        if [[ "$line" =~ ^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]]; then
            local key="${BASH_REMATCH[1]}"
            local value="${BASH_REMATCH[2]}"
            # 引用符を除去
            value="${value#\"}"
            value="${value%\"}"
            CONFIG["$key"]="$value"
        fi
    done < "$config_file"
}

# --- URLのパース ---
parse_url() {
    local url=$1
    local regex='^(https?):\/\/([^:\/]+)(:([0-9]+))?(\/[^?]*)?(\?(.*))?$'
    
    if [[ "$url" =~ $regex ]]; then
        echo "Protocol: ${BASH_REMATCH[1]}"
        echo "Host: ${BASH_REMATCH[2]}"
        echo "Port: ${BASH_REMATCH[4]:-default}"
        echo "Path: ${BASH_REMATCH[5]:-/}"
        echo "Query: ${BASH_REMATCH[7]:-none}"
    fi
}

parse_url "https://example.com:8080/api/v1/users?page=1&limit=10"

# --- セマンティックバージョニングの検証 ---
validate_semver() {
    local version=$1
    local regex='^v?([0-9]+)\.([0-9]+)\.([0-9]+)(-([a-zA-Z0-9.]+))?(\+([a-zA-Z0-9.]+))?$'
    
    if [[ "$version" =~ $regex ]]; then
        echo "Major: ${BASH_REMATCH[1]}"
        echo "Minor: ${BASH_REMATCH[2]}"
        echo "Patch: ${BASH_REMATCH[3]}"
        echo "Pre-release: ${BASH_REMATCH[5]:-none}"
        echo "Build: ${BASH_REMATCH[7]:-none}"
        return 0
    fi
    return 1
}

validate_semver "v2.1.3-beta.1+build.123"

第8章: エラーハンドリングとデバッグ

8.1 エラーハンドリング戦略

#!/usr/bin/env bash
set -euo pipefail

# --- 基本的なエラーハンドリング ---
# 方法1: || によるフォールバック
cd /some/directory || { echo "ディレクトリの移動に失敗" >&2; exit 1; }

# 方法2: if文によるチェック
if ! output=$(some_command 2>&1); then
    echo "コマンド失敗: ${output}" >&2
    exit 1
fi

# 方法3: サブシェルでのエラー封じ込め
if result=$(
    set -e
    step1
    step2
    step3
); then
    echo "全ステップ成功: ${result}"
else
    echo "いずれかのステップで失敗"
fi

# --- 包括的なエラーハンドリングフレームワーク ---
# スタックトレースの表示
stacktrace() {
    local frame=0
    echo "Stack trace:" >&2
    while caller "$frame"; do
        ((frame++))
    done 2>&1 | while IFS=' ' read -r line func file; do
        echo "  at ${func}() in ${file}:${line}" >&2
    done
}

# ERRトラップとスタックトレース
error_handler() {
    local exit_code=$?
    local line_number=$1
    local command=$2
    
    echo "============================================" >&2
    echo "ERROR: コマンド '${command}' が失敗しました" >&2
    echo "  終了コード: ${exit_code}" >&2
    echo "  行番号: ${line_number}" >&2
    echo "  ファイル: ${BASH_SOURCE[1]:-unknown}" >&2
    echo "============================================" >&2
    stacktrace
    
    exit "$exit_code"
}

trap 'error_handler ${LINENO} "${BASH_COMMAND}"' ERR

# --- try-catch パターン ---
try() {
    [[ $- = *e* ]]
    SAVED_OPT_E=$?
    set +e
}

catch() {
    local exit_code=$?
    (( SAVED_OPT_E )) && set +e
    return $exit_code
}

throw() {
    exit "$1"
}

throwErrors() {
    set -e
}

ignoreErrors() {
    set +e
}

# 使用例
try
(
    throwErrors
    risky_command_1
    risky_command_2
)
catch || {
    case $? in
        1) echo "一般的なエラー" ;;
        2) echo "構文エラー" ;;
        126) echo "実行権限なし" ;;
        127) echo "コマンドが見つかりません" ;;
        *) echo "不明なエラー: $?" ;;
    esac
}

# --- リトライ機構 ---
retry() {
    local max_attempts=$1
    local delay=$2
    local backoff_multiplier=${3:-2}
    shift 3
    local cmd=("$@")
    
    local attempt=1
    local current_delay=$delay
    
    while (( attempt <= max_attempts )); do
        echo "試行 ${attempt}/${max_attempts}: ${cmd[*]}" >&2
        
        if "${cmd[@]}"; then
            return 0
        fi
        
        if (( attempt < max_attempts )); then
            echo "  ${current_delay}秒後にリトライ..." >&2
            sleep "$current_delay"
            current_delay=$((current_delay * backoff_multiplier))
        fi
        
        ((attempt++))
    done
    
    echo "全試行が失敗しました" >&2
    return 1
}

# 使用例: 最大5回、初期遅延2秒、倍率2倍
retry 5 2 2 curl -sf "https://api.example.com/health"

8.2 デバッグ手法

#!/usr/bin/env bash

# --- set -x によるトレース ---
set -x  # デバッグ開始
echo "このコマンドは表示される"
set +x  # デバッグ終了

# カスタムPS4プロンプト
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x

# --- デバッグ関数 ---
DEBUG=${DEBUG:-false}

debug() {
    if [[ "$DEBUG" = true ]]; then
        echo "[DEBUG $(date '+%H:%M:%S')] $*" >&2
    fi
}

# 使用: DEBUG=true ./script.sh
debug "変数xの値: ${x:-undefined}"

# --- 変数の状態表示 ---
dump_vars() {
    echo "=== Variable Dump ===" >&2
    for var in "$@"; do
        if declare -p "$var" 2>/dev/null; then
            :
        else
            echo "  ${var}: (未定義)" >&2
        fi
    done
    echo "=====================" >&2
}

name="Alice"
count=42
dump_vars name count undefined_var

# --- bash -x による外部からのデバッグ ---
# bash -x script.sh        # デバッグモードで実行
# bash -n script.sh        # 構文チェックのみ(実行しない)
# bash -v script.sh        # 読み取った行を表示

# --- bashdb (Bash Debugger) ---
# インストール: apt install bashdb
# 使用: bashdb script.sh
# コマンド: step, next, print, break, continue, list

# --- shellcheck による静的解析 ---
# インストール: brew install shellcheck (macOS)
# 使用: shellcheck script.sh
# CI統合: shellcheck --format=json script.sh

# shellcheckの指摘例と対策:
# SC2086: Double quote to prevent globbing and word splitting
# 修正前: echo $variable
# 修正後: echo "$variable"

# SC2046: Quote this to prevent word splitting
# 修正前: files=$(find . -name "*.txt")
# 修正後: mapfile -t files < <(find . -name "*.txt")

# SC2034: Variable appears unused
# 修正前: unused_var="value"
# 修正後: export unused_var="value" (意図的な場合)

# SC2155: Declare and assign separately
# 修正前: local result=$(command)
# 修正後: local result; result=$(command)

8.3 ログ管理

#!/usr/bin/env bash

# --- 構造化ログシステム ---
LOG_FILE="${LOG_FILE:-/var/log/my_script.log}"
LOG_LEVEL="${LOG_LEVEL:-INFO}"
LOG_FORMAT="${LOG_FORMAT:-text}"  # text or json

declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3 [FATAL]=4)

_log() {
    local level=$1
    shift
    local message="$*"
    local timestamp
    timestamp=$(date -u '+%Y-%m-%dT%H:%M:%S.%3NZ')
    
    # レベルチェック
    (( ${LOG_LEVELS[$level]:-0} < ${LOG_LEVELS[$LOG_LEVEL]:-0} )) && return
    
    if [[ "$LOG_FORMAT" == "json" ]]; then
        printf '{"timestamp":"%s","level":"%s","pid":%d,"message":"%s","source":"%s:%d"}\n' \
            "$timestamp" "$level" $$ \
            "$(echo "$message" | sed 's/"/\\"/g')" \
            "${BASH_SOURCE[2]:-unknown}" "${BASH_LINENO[1]:-0}"
    else
        printf "[%s] [%-5s] [PID:%d] %s\n" \
            "$timestamp" "$level" $$ "$message"
    fi | tee -a "$LOG_FILE" >&2
}

log_debug() { _log DEBUG "$@"; }
log_info()  { _log INFO  "$@"; }
log_warn()  { _log WARN  "$@"; }
log_error() { _log ERROR "$@"; }
log_fatal() { _log FATAL "$@"; exit 1; }

# ログローテーション
rotate_log() {
    local max_size=${1:-10485760}  # 10MB
    local max_files=${2:-5}
    
    [[ ! -f "$LOG_FILE" ]] && return
    
    local file_size
    file_size=$(stat -f%z "$LOG_FILE" 2>/dev/null || stat -c%s "$LOG_FILE" 2>/dev/null)
    
    if (( file_size > max_size )); then
        for ((i = max_files - 1; i >= 1; i--)); do
            [[ -f "${LOG_FILE}.${i}" ]] && mv "${LOG_FILE}.${i}" "${LOG_FILE}.$((i + 1))"
        done
        mv "$LOG_FILE" "${LOG_FILE}.1"
        touch "$LOG_FILE"
        log_info "ログローテーションを実行しました"
    fi
}

# 使用例
log_info "スクリプトを開始しました"
log_debug "デバッグ情報: variable=${variable:-undefined}"
log_warn "ディスク使用率が80%を超えました"
log_error "データベース接続に失敗しました"

第9章: 環境設定とカスタマイズ

9.1 シェルの設定ファイル

ログインシェル起動時の読み込み順序:
┌─────────────────────────────────────────────────────┐
│  /etc/profile                                        │
│  ↓                                                   │
│  ~/.bash_profile  (存在しなければ)                     │
│  ~/.bash_login    (存在しなければ)                     │
│  ~/.profile                                          │
│  ↓                                                   │
│  ~/.bashrc (bash_profileから明示的にsourceされる)      │
└─────────────────────────────────────────────────────┘

非ログインシェル(ターミナルエミュレータ等):
┌─────────────────────────────────────────────────────┐
│  ~/.bashrc のみ                                      │
└─────────────────────────────────────────────────────┘

ログアウト時:
┌─────────────────────────────────────────────────────┐
│  ~/.bash_logout                                      │
└─────────────────────────────────────────────────────┘

9.2 .bashrc / .bash_profile の設定例

# ~/.bash_profile
# ============================================================
# ログインシェルの設定
# ============================================================

# .bashrcを読み込む
if [[ -f ~/.bashrc ]]; then
    source ~/.bashrc
fi

# 環境変数の設定(ログインシェルで1回だけ設定)
export EDITOR="vim"
export VISUAL="vim"
export PAGER="less"
export LANG="en_US.UTF-8"
export LC_ALL="en_US.UTF-8"

# PATHの設定
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"
export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"
export PATH="$HOME/.cargo/bin:$PATH"

# ヒストリの設定
export HISTSIZE=50000
export HISTFILESIZE=100000
export HISTCONTROL=ignoreboth:erasedups
export HISTTIMEFORMAT="%Y-%m-%d %H:%M:%S "
export HISTIGNORE="ls:ll:cd:pwd:bg:fg:history:clear:exit"
# ~/.bashrc
# ============================================================
# インタラクティブシェルの設定
# ============================================================

# 非インタラクティブシェルでは何もしない
[[ $- != *i* ]] && return

# --- シェルオプション ---
shopt -s histappend      # ヒストリを追記モードに
shopt -s checkwinsize    # ウィンドウサイズの自動調整
shopt -s cdspell         # cdコマンドのスペルミス自動修正
shopt -s dirspell        # ディレクトリ名のスペルミス自動修正
shopt -s globstar        # ** による再帰マッチ
shopt -s extglob         # 拡張グロブパターン
shopt -s nocaseglob      # グロブの大文字小文字無視
shopt -s dotglob         # * でドットファイルもマッチ
shopt -s autocd          # ディレクトリ名だけでcd

# --- エイリアス ---
# 安全なデフォルト
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'

# ls のカスタマイズ
alias ls='ls --color=auto'
alias ll='ls -alFh'
alias la='ls -A'
alias l='ls -CF'

# grep のカスタマイズ
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'

# ナビゲーション
alias ..='cd ..'
alias ...='cd ../..'
alias ....='cd ../../..'
alias ~='cd ~'

# Git エイリアス
alias g='git'
alias gs='git status'
alias ga='git add'
alias gc='git commit'
alias gp='git push'
alias gl='git log --oneline -20'
alias gd='git diff'
alias gb='git branch'
alias gco='git checkout'

# Docker エイリアス
alias d='docker'
alias dc='docker compose'
alias dps='docker ps'
alias dpsa='docker ps -a'
alias di='docker images'
alias dex='docker exec -it'

# Kubernetes エイリアス
alias k='kubectl'
alias kgp='kubectl get pods'
alias kgs='kubectl get services'
alias kgd='kubectl get deployments'
alias kl='kubectl logs'
alias kex='kubectl exec -it'

# ネットワーク
alias ports='netstat -tulanp'
alias myip='curl -s ifconfig.me'

# --- 便利な関数 ---
# ディレクトリ作成と移動
mkcd() {
    mkdir -p "$1" && cd "$1" || return
}

# ファイル検索
ff() {
    find . -type f -iname "*$1*" 2>/dev/null
}

# プロセス検索
psg() {
    ps aux | grep -v grep | grep -i "$1"
}

# 圧縮ファイルの展開
extract() {
    if [[ -f "$1" ]]; then
        case "$1" in
            *.tar.bz2) tar xjf "$1" ;;
            *.tar.gz)  tar xzf "$1" ;;
            *.tar.xz)  tar xJf "$1" ;;
            *.bz2)     bunzip2 "$1" ;;
            *.gz)       gunzip "$1" ;;
            *.tar)     tar xf "$1" ;;
            *.tbz2)    tar xjf "$1" ;;
            *.tgz)     tar xzf "$1" ;;
            *.zip)     unzip "$1" ;;
            *.Z)       uncompress "$1" ;;
            *.7z)      7z x "$1" ;;
            *.rar)     unrar x "$1" ;;
            *)         echo "展開できない形式: $1" ;;
        esac
    else
        echo "ファイルが見つかりません: $1"
    fi
}

# JSONの整形表示
jsonpp() {
    if [[ -p /dev/stdin ]]; then
        python3 -m json.tool
    elif [[ -f "$1" ]]; then
        python3 -m json.tool "$1"
    else
        echo "$1" | python3 -m json.tool
    fi
}

# --- プロンプトのカスタマイズ ---
# Gitブランチ名を表示
parse_git_branch() {
    git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ (\1)/'
}

# カラフルなプロンプト
export PS1='\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[33m\]$(parse_git_branch)\[\033[00m\]\$ '

# --- 補完の設定 ---
# bash-completion
if [[ -f /etc/bash_completion ]]; then
    source /etc/bash_completion
elif [[ -f /usr/local/etc/bash_completion ]]; then
    source /usr/local/etc/bash_completion
fi

# カスタム補完の追加
if [[ -d ~/.bash_completion.d ]]; then
    for f in ~/.bash_completion.d/*; do
        source "$f"
    done
fi

9.3 環境変数の管理

#!/usr/bin/env bash

# --- 環境変数の操作 ---
# 設定
export MY_APP_ENV="production"
export MY_APP_PORT=8080
export MY_APP_DEBUG=false

# 確認
env | sort                      # 全環境変数をソートして表示
printenv MY_APP_ENV             # 特定の変数の値
echo "${MY_APP_ENV}"            # 変数展開

# 削除
unset MY_APP_ENV

# --- .env ファイルの読み込み ---
# .env ファイルの例:
# DATABASE_URL=postgres://user:pass@localhost/dbname
# REDIS_URL=redis://localhost:6379
# SECRET_KEY=my-secret-key-123

load_env() {
    local env_file="${1:-.env}"
    
    if [[ ! -f "$env_file" ]]; then
        echo "警告: ${env_file} が見つかりません" >&2
        return 1
    fi
    
    while IFS= read -r line; do
        # 空行とコメントをスキップ
        [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
        
        # key=value をパース
        if [[ "$line" =~ ^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*=[[:space:]]*(.*) ]]; then
            local key="${BASH_REMATCH[1]}"
            local value="${BASH_REMATCH[2]}"
            
            # 引用符を除去
            value="${value%\"}"
            value="${value#\"}"
            value="${value%\'}"
            value="${value#\'}"
            
            export "${key}=${value}"
        fi
    done < "$env_file"
}

load_env ".env"
load_env ".env.local"  # ローカル上書き用

# --- XDG Base Directory ---
# 設定ファイルの標準的な配置場所
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"

# アプリケーション設定の配置
APP_CONFIG_DIR="${XDG_CONFIG_HOME}/myapp"
APP_DATA_DIR="${XDG_DATA_HOME}/myapp"
APP_CACHE_DIR="${XDG_CACHE_HOME}/myapp"

mkdir -p "$APP_CONFIG_DIR" "$APP_DATA_DIR" "$APP_CACHE_DIR"

9.4 カスタム補完スクリプト

# ~/.bash_completion.d/myapp-completion.bash

_myapp_completion() {
    local cur prev opts
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    
    # サブコマンド一覧
    local commands="start stop restart status deploy rollback logs config"
    
    case "$prev" in
        myapp)
            COMPREPLY=($(compgen -W "$commands" -- "$cur"))
            return 0
            ;;
        deploy|rollback)
            # 環境名を補完
            local environments="production staging development"
            COMPREPLY=($(compgen -W "$environments" -- "$cur"))
            return 0
            ;;
        logs)
            # ログレベルを補完
            local levels="debug info warn error"
            COMPREPLY=($(compgen -W "$levels" -- "$cur"))
            return 0
            ;;
        config)
            # 設定操作を補完
            local config_ops="get set list"
            COMPREPLY=($(compgen -W "$config_ops" -- "$cur"))
            return 0
            ;;
    esac
    
    # --で始まるオプションの補完
    if [[ "$cur" == -* ]]; then
        local options="--verbose --quiet --help --version --config"
        COMPREPLY=($(compgen -W "$options" -- "$cur"))
        return 0
    fi
}

complete -F _myapp_completion myapp

第10章: ファイルシステム操作

10.1 ファイルとディレクトリの操作

#!/usr/bin/env bash

# --- 基本操作 ---
# ファイル作成
touch newfile.txt                    # 空ファイル作成
echo "content" > newfile.txt         # 内容付きで作成
cat > config.ini << 'EOF'
[database]
host = localhost
port = 5432
name = mydb
EOF

# ディレクトリ操作
mkdir -p /path/to/deep/directory     # 再帰的に作成
mkdir -m 750 secure_dir              # パーミッション指定で作成

# コピー
cp -a source/ dest/                  # アーカイブモード(属性保持)
cp -r dir1/ dir2/                    # 再帰的コピー
cp --backup=numbered file.txt dest/  # 番号付きバックアップ

# 移動/リネーム
mv old_name.txt new_name.txt
mv -n source.txt dest/               # 上書き防止

# 削除
rm -f unnecessary.txt                # 強制削除
rm -rf temp_directory/               # ディレクトリごと再帰削除

# リンク
ln source.txt hardlink.txt           # ハードリンク
ln -s /path/to/source symlink.txt    # シンボリックリンク
readlink -f symlink.txt              # シンボリックリンクの実体パス

# --- ファイル情報 ---
stat file.txt                        # 詳細情報
file unknown_file                    # ファイルタイプの判定
du -sh directory/                    # ディレクトリサイズ
df -h                                # ディスク使用量

10.2 パーミッションとオーナーシップ

#!/usr/bin/env bash

# --- パーミッション ---
# 数値表記
chmod 755 script.sh      # rwxr-xr-x
chmod 644 config.txt     # rw-r--r--
chmod 600 secret.key     # rw-------
chmod 700 private_dir/   # rwx------

# シンボリック表記
chmod u+x script.sh      # ユーザーに実行権限追加
chmod g+rw shared.txt    # グループに読み書き権限追加
chmod o-rwx private.txt  # その他から全権限削除
chmod a+r public.txt     # 全員に読み取り権限追加

# 再帰的に適用
chmod -R 755 web_dir/
find web_dir/ -type f -exec chmod 644 {} \;  # ファイルのみ
find web_dir/ -type d -exec chmod 755 {} \;  # ディレクトリのみ

# --- 特殊パーミッション ---
chmod 4755 suid_program   # SUID: 実行時にオーナーの権限で実行
chmod 2755 sgid_dir/      # SGID: ディレクトリ内のファイルはグループを継承
chmod 1777 /tmp/sticky/   # Sticky bit: オーナーのみ削除可能

# --- オーナーシップ ---
chown user:group file.txt
chown -R www-data:www-data /var/www/
chgrp developers project/

# --- umask ---
umask 022     # デフォルト: ファイル644, ディレクトリ755
umask 077     # 制限的: ファイル600, ディレクトリ700

# --- ACL (Access Control List) ---
# 特定ユーザーに追加のアクセス権を付与
setfacl -m u:bob:rwx shared_file.txt
setfacl -m g:developers:rx project/
getfacl shared_file.txt

# デフォルトACL(ディレクトリ内の新規ファイルに適用)
setfacl -d -m g:developers:rwx project/

10.3 findコマンドの活用

#!/usr/bin/env bash

# --- 基本的な検索 ---
find /var/log -name "*.log"                    # 名前で検索
find . -iname "readme*"                        # 大文字小文字無視
find . -type f                                 # ファイルのみ
find . -type d                                 # ディレクトリのみ
find . -type l                                 # シンボリックリンクのみ

# --- 属性による検索 ---
find . -size +100M                             # 100MB以上
find . -size -1k                               # 1KB未満
find . -mtime -7                               # 7日以内に変更
find . -mtime +30                              # 30日以上前に変更
find . -mmin -60                               # 60分以内に変更
find . -newer reference_file                   # 参照ファイルより新しい
find . -user root                              # root所有
find . -group www-data                         # www-dataグループ
find . -perm 644                               # パーミッション完全一致
find . -perm -644                              # 最低限のパーミッション
find . -empty                                  # 空のファイル/ディレクトリ

# --- 複合条件 ---
find . -name "*.log" -size +10M -mtime +7
find . \( -name "*.txt" -o -name "*.md" \) -type f
find . -name "*.tmp" -not -name "important.tmp"

# --- アクション ---
find . -name "*.bak" -delete                         # 削除
find . -name "*.sh" -exec chmod +x {} \;             # 各ファイルに実行
find . -name "*.txt" -exec grep -l "TODO" {} +       # まとめてgrep
find . -name "*.log" -exec sh -c 'gzip "$1"' _ {} \; # シェル経由で実行

# --- 実用的なパターン ---
# 古い一時ファイルの削除
find /tmp -type f -mtime +7 -delete

# 大きなファイルの一覧
find / -type f -size +500M -exec ls -lh {} + 2>/dev/null | sort -k5 -h

# 特定の内容を含むファイルの検索
find . -type f -name "*.py" -exec grep -l "import requests" {} +

# ディスク使用量の多いディレクトリ
find / -type d -exec du -s {} + 2>/dev/null | sort -rn | head -20

# 壊れたシンボリックリンクの検出
find . -type l ! -exec test -e {} \; -print

10.4 一時ファイルとディレクトリ

#!/usr/bin/env bash

# --- mktemp による安全な一時ファイル作成 ---
# 一時ファイル
tmpfile=$(mktemp)
echo "一時ファイル: ${tmpfile}"
echo "data" > "$tmpfile"

# 一時ディレクトリ
tmpdir=$(mktemp -d)
echo "一時ディレクトリ: ${tmpdir}"

# テンプレート指定
tmpfile=$(mktemp /tmp/myapp.XXXXXXXXXX)
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/myapp.XXXXXXXXXX")

# クリーンアップ
trap 'rm -rf "$tmpfile" "$tmpdir"' EXIT

# --- プロセス置換による一時ファイル不要パターン ---
# 一時ファイルを使わずに2つのコマンドの出力を比較
diff <(sort file1.txt) <(sort file2.txt)

# --- Here String / Here Document ---
# 一時ファイルなしで入力データを渡す
mysql -u root <<< "SHOW DATABASES;"

python3 << 'PYTHON'
import json
data = {"key": "value"}
print(json.dumps(data, indent=2))
PYTHON

第11章: ネットワーク操作

11.1 ネットワーク診断コマンド

#!/usr/bin/env bash

# --- 接続テスト ---
ping -c 4 google.com                    # ICMP ping
ping -c 4 -W 2 192.168.1.1             # タイムアウト指定

# --- DNS ---
host example.com                        # 名前解決
dig example.com A                       # Aレコード
dig example.com MX +short               # MXレコード(簡潔表示)
nslookup example.com                    # 名前解決(レガシー)

# --- ポートスキャンと接続テスト ---
# ncコマンド(netcat)
nc -zv example.com 80                   # TCPポート接続テスト
nc -zv example.com 80-443               # ポート範囲スキャン
echo "GET / HTTP/1.0\r\n\r\n" | nc example.com 80  # HTTP手動リクエスト

# /dev/tcp によるポートチェック(Bash組込み)
check_port() {
    local host=$1 port=$2
    if (echo >/dev/tcp/"$host"/"$port") 2>/dev/null; then
        echo "OPEN: ${host}:${port}"
    else
        echo "CLOSED: ${host}:${port}"
    fi
}

check_port "localhost" 22
check_port "localhost" 80

# --- ネットワーク情報 ---
ip addr show                            # IPアドレス表示
ip route show                           # ルーティングテーブル
ss -tuln                                # リスニングポート
ss -tunap                               # 全ソケット(プロセス付き)

# --- トレースルート ---
traceroute example.com
mtr --report example.com                # 統合トレースルート

# --- 帯域幅テスト ---
# iperf3 -s                             # サーバー側
# iperf3 -c server_ip                   # クライアント側

11.2 HTTPリクエスト (curl / wget)

#!/usr/bin/env bash

# === curl ===
# --- 基本的なGETリクエスト ---
curl https://api.example.com/data
curl -s https://api.example.com/data          # サイレントモード
curl -sS https://api.example.com/data         # エラーのみ表示
curl -sf https://api.example.com/data         # 失敗時に非ゼロ終了

# --- レスポンスヘッダーの表示 ---
curl -I https://api.example.com/              # HEADリクエスト
curl -i https://api.example.com/data          # ヘッダーとボディ両方

# --- POSTリクエスト ---
# フォームデータ
curl -X POST -d "username=admin&password=secret" \
    https://api.example.com/login

# JSONデータ
curl -X POST \
    -H "Content-Type: application/json" \
    -d '{"name":"John","email":"john@example.com"}' \
    https://api.example.com/users

# ファイルからJSONデータ
curl -X POST \
    -H "Content-Type: application/json" \
    -d @payload.json \
    https://api.example.com/users

# --- 認証 ---
curl -u username:password https://api.example.com/    # Basic認証
curl -H "Authorization: Bearer TOKEN" https://api.example.com/  # Bearer Token

# --- ファイルのアップロード ---
curl -F "file=@/path/to/file.pdf" https://api.example.com/upload
curl -F "file=@photo.jpg;type=image/jpeg" -F "name=vacation" \
    https://api.example.com/upload

# --- ファイルのダウンロード ---
curl -o output.html https://example.com/page.html     # ファイル名指定
curl -O https://example.com/file.tar.gz                # 元のファイル名
curl -L -O https://example.com/redirect                # リダイレクト追従

# --- 高度なオプション ---
curl -s -w "\nHTTP Status: %{http_code}\nTotal Time: %{time_total}s\n" \
    https://api.example.com/health

# リトライ
curl --retry 3 --retry-delay 2 https://api.example.com/data

# タイムアウト設定
curl --connect-timeout 5 --max-time 30 https://api.example.com/data

# --- APIスクリプティングの例 ---
api_call() {
    local method=$1
    local endpoint=$2
    local data=${3:-}
    local base_url="https://api.example.com"
    local token="${API_TOKEN:?API_TOKENが設定されていません}"
    
    local curl_opts=(
        -s
        -f
        -w "\n%{http_code}"
        -H "Authorization: Bearer ${token}"
        -H "Content-Type: application/json"
        -H "Accept: application/json"
    )
    
    if [[ -n "$data" ]]; then
        curl_opts+=(-d "$data")
    fi
    
    local response
    response=$(curl "${curl_opts[@]}" -X "$method" "${base_url}${endpoint}")
    local http_code
    http_code=$(echo "$response" | tail -1)
    local body
    body=$(echo "$response" | sed '$d')
    
    if (( http_code >= 200 && http_code < 300 )); then
        echo "$body"
        return 0
    else
        echo "API Error (HTTP ${http_code}): ${body}" >&2
        return 1
    fi
}

# 使用例
# users=$(api_call GET "/v1/users?limit=10")
# new_user=$(api_call POST "/v1/users" '{"name":"Alice"}')

# === wget ===
wget https://example.com/file.tar.gz                # 基本ダウンロード
wget -q -O - https://example.com/script.sh | bash   # パイプ実行
wget -r -l 2 -nd https://example.com/docs/           # 再帰的ダウンロード
wget --mirror -p --convert-links https://example.com/ # サイトミラーリング
wget -c https://example.com/large_file.iso            # レジューム

11.3 SSH操作の自動化

#!/usr/bin/env bash

# --- SSH設定ファイル (~/.ssh/config) ---
cat << 'EOF'
# ~/.ssh/config
Host web-prod
    HostName 192.168.1.100
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_ed25519_deploy
    ForwardAgent yes

Host web-staging
    HostName 192.168.1.200
    User deploy
    IdentityFile ~/.ssh/id_ed25519_deploy

Host db-*
    User dbadmin
    IdentityFile ~/.ssh/id_ed25519_db

Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    AddKeysToAgent yes
    Compression yes
EOF

# --- リモートコマンド実行 ---
ssh web-prod "uptime"
ssh web-prod "df -h && free -m"

# 複数コマンド
ssh web-prod << 'REMOTE'
cd /var/www/html
git pull origin main
sudo systemctl restart nginx
echo "デプロイ完了"
REMOTE

# --- scp ---
scp local_file.txt web-prod:/tmp/              # アップロード
scp web-prod:/var/log/app.log ./               # ダウンロード
scp -r local_dir/ web-prod:/opt/               # ディレクトリコピー

# --- rsync ---
rsync -avz ./src/ web-prod:/var/www/html/      # 同期
rsync -avz --delete ./src/ web-prod:/var/www/  # 削除も同期
rsync -avz --exclude='*.log' --exclude='.git' \
    ./project/ web-prod:/opt/project/           # 除外パターン付き

# --- SSHトンネル ---
# ローカルポートフォワーディング
ssh -L 8080:localhost:80 web-prod              # local:8080 → remote:80
ssh -L 5432:db-server:5432 jump-host           # ジャンプホスト経由

# リモートポートフォワーディング
ssh -R 9090:localhost:3000 web-prod            # remote:9090 → local:3000

# SOCKSプロキシ
ssh -D 1080 web-prod                           # ローカルSOCKS5プロキシ

# --- 複数サーバーへの一括実行 ---
servers=("web01" "web02" "web03" "app01" "app02")

run_on_all() {
    local cmd=$1
    for server in "${servers[@]}"; do
        echo "=== ${server} ==="
        ssh -o ConnectTimeout=5 "$server" "$cmd" 2>&1 || \
            echo "ERROR: ${server} に接続できません"
        echo
    done
}

run_on_all "uptime && df -h /"

# 並列実行版
run_on_all_parallel() {
    local cmd=$1
    local pids=()
    
    for server in "${servers[@]}"; do
        (
            echo "=== ${server} ==="
            ssh -o ConnectTimeout=5 "$server" "$cmd" 2>&1 || \
                echo "ERROR: ${server}"
        ) &
        pids+=($!)
    done
    
    for pid in "${pids[@]}"; do
        wait "$pid"
    done
}

run_on_all_parallel "uname -a"

第12章: システム管理スクリプト

12.1 システム監視

#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# システム監視スクリプト
# CPU, メモリ, ディスク, プロセスの監視とアラート
# ============================================================

# 設定
readonly CPU_THRESHOLD=80
readonly MEM_THRESHOLD=85
readonly DISK_THRESHOLD=90
readonly ALERT_EMAIL="admin@example.com"
readonly HOSTNAME=$(hostname)

# CPU使用率の取得
get_cpu_usage() {
    local cpu_idle
    cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}' | cut -d'.' -f1)
    echo $((100 - cpu_idle))
}

# メモリ使用率の取得
get_memory_usage() {
    free | awk '/^Mem:/ {printf "%.0f", ($3/$2)*100}'
}

# ディスク使用率の取得
get_disk_usage() {
    df -h "$1" | awk 'NR==2 {print $5}' | tr -d '%'
}

# ロードアベレージ
get_load_average() {
    uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' '
}

# プロセス数
get_process_count() {
    ps aux | wc -l
}

# アラート送信
send_alert() {
    local subject=$1
    local body=$2
    echo "$body" | mail -s "[ALERT] ${HOSTNAME}: ${subject}" "$ALERT_EMAIL"
}

# システムヘルスチェック
health_check() {
    local alerts=()
    local status="OK"
    
    # CPU チェック
    local cpu
    cpu=$(get_cpu_usage)
    if (( cpu > CPU_THRESHOLD )); then
        alerts+=("CPU使用率が高い: ${cpu}% (閾値: ${CPU_THRESHOLD}%)")
        status="WARNING"
    fi
    
    # メモリチェック
    local mem
    mem=$(get_memory_usage)
    if (( mem > MEM_THRESHOLD )); then
        alerts+=("メモリ使用率が高い: ${mem}% (閾値: ${MEM_THRESHOLD}%)")
        status="WARNING"
    fi
    
    # ディスクチェック
    while IFS= read -r line; do
        local mount usage
        mount=$(echo "$line" | awk '{print $6}')
        usage=$(echo "$line" | awk '{print $5}' | tr -d '%')
        if (( usage > DISK_THRESHOLD )); then
            alerts+=("ディスク使用率が高い: ${mount} ${usage}% (閾値: ${DISK_THRESHOLD}%)")
            status="CRITICAL"
        fi
    done < <(df -h | tail -n +2 | grep -v "tmpfs")
    
    # レポート出力
    echo "============================================"
    echo " System Health Report - ${HOSTNAME}"
    echo " $(date '+%Y-%m-%d %H:%M:%S')"
    echo "============================================"
    echo "Status: ${status}"
    echo "CPU Usage: ${cpu}%"
    echo "Memory Usage: ${mem}%"
    echo "Load Average: $(get_load_average)"
    echo "Process Count: $(get_process_count)"
    echo "--------------------------------------------"
    
    if (( ${#alerts[@]} > 0 )); then
        echo "ALERTS:"
        for alert in "${alerts[@]}"; do
            echo "  ! ${alert}"
        done
        send_alert "${status}" "$(printf '%s\n' "${alerts[@]}")"
    else
        echo "No alerts. All systems normal."
    fi
}

health_check

12.2 バックアップスクリプト

#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# バックアップスクリプト
# 増分バックアップ、世代管理、圧縮、リモート転送
# ============================================================

# 設定
readonly BACKUP_SOURCE="/var/www/html /etc /home"
readonly BACKUP_DEST="/backup"
readonly REMOTE_DEST="backup-server:/mnt/backups"
readonly RETENTION_DAYS=30
readonly MAX_BACKUPS=10
readonly DATE_FORMAT=$(date '+%Y%m%d_%H%M%S')
readonly BACKUP_NAME="backup_${HOSTNAME}_${DATE_FORMAT}"
readonly BACKUP_FILE="${BACKUP_DEST}/${BACKUP_NAME}.tar.gz"
readonly BACKUP_LOG="${BACKUP_DEST}/logs/${BACKUP_NAME}.log"
readonly EXCLUDE_FILE="${BACKUP_DEST}/.backup_exclude"

# 除外パターンファイル
setup_excludes() {
    cat > "$EXCLUDE_FILE" << 'EOF'
*.tmp
*.swp
*.log
.cache/
node_modules/
__pycache__/
.git/
*.pid
EOF
}

# バックアップの実行
perform_backup() {
    echo "=== バックアップ開始: $(date) ==="
    
    # 前回のバックアップとの増分を使用
    local last_backup="${BACKUP_DEST}/.last_backup_time"
    local newer_than=""
    
    if [[ -f "$last_backup" ]]; then
        newer_than="--newer-mtime=$(cat "$last_backup")"
        echo "増分バックアップ: $(cat "$last_backup") 以降"
    else
        echo "フルバックアップ"
    fi
    
    # tarによるバックアップ
    # shellcheck disable=SC2086
    tar czf "$BACKUP_FILE" \
        --exclude-from="$EXCLUDE_FILE" \
        ${newer_than} \
        $BACKUP_SOURCE 2>&1 | tee -a "$BACKUP_LOG"
    
    # チェックサム
    sha256sum "$BACKUP_FILE" > "${BACKUP_FILE}.sha256"
    
    # タイムスタンプの更新
    date -u '+%Y-%m-%d %H:%M:%S' > "$last_backup"
    
    # サイズ表示
    local size
    size=$(du -sh "$BACKUP_FILE" | cut -f1)
    echo "バックアップサイズ: ${size}"
    echo "ファイル: ${BACKUP_FILE}"
}

# リモート転送
sync_to_remote() {
    echo "=== リモート転送開始 ==="
    rsync -avz --progress \
        "$BACKUP_FILE" \
        "${BACKUP_FILE}.sha256" \
        "$REMOTE_DEST/"
    echo "リモート転送完了"
}

# 古いバックアップの削除
cleanup_old_backups() {
    echo "=== 古いバックアップのクリーンアップ ==="
    
    # 日数ベースの削除
    find "$BACKUP_DEST" -name "backup_*.tar.gz" -mtime "+${RETENTION_DAYS}" -delete
    find "$BACKUP_DEST" -name "backup_*.sha256" -mtime "+${RETENTION_DAYS}" -delete
    
    # 世代数ベースの削除
    local count
    count=$(find "$BACKUP_DEST" -name "backup_*.tar.gz" | wc -l)
    if (( count > MAX_BACKUPS )); then
        local to_delete=$((count - MAX_BACKUPS))
        find "$BACKUP_DEST" -name "backup_*.tar.gz" -printf '%T@ %p\n' | \
            sort -n | head -"$to_delete" | awk '{print $2}' | \
            xargs rm -f
        echo "${to_delete}件の古いバックアップを削除しました"
    fi
}

# メイン処理
main() {
    mkdir -p "${BACKUP_DEST}/logs"
    setup_excludes
    perform_backup
    sync_to_remote
    cleanup_old_backups
    echo "=== バックアップ完了: $(date) ==="
}

main 2>&1 | tee -a "$BACKUP_LOG"

12.3 ユーザー管理スクリプト

#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# ユーザー管理スクリプト
# ============================================================

readonly USERS_CSV="users.csv"
readonly LOG_FILE="/var/log/user_management.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

# CSVからユーザーを一括作成
create_users_from_csv() {
    # CSV形式: username,fullname,group,shell
    while IFS=',' read -r username fullname group shell; do
        # ヘッダー行をスキップ
        [[ "$username" == "username" ]] && continue
        [[ -z "$username" ]] && continue
        
        # ユーザーの存在確認
        if id "$username" &>/dev/null; then
            log "SKIP: ユーザー ${username} は既に存在します"
            continue
        fi
        
        # グループの作成(存在しない場合)
        if ! getent group "$group" &>/dev/null; then
            groupadd "$group"
            log "CREATE GROUP: ${group}"
        fi
        
        # ユーザーの作成
        useradd \
            -m \
            -c "$fullname" \
            -g "$group" \
            -s "${shell:-/bin/bash}" \
            "$username"
        
        # 初期パスワード設定(初回ログイン時に変更を要求)
        local temp_password
        temp_password=$(openssl rand -base64 12)
        echo "${username}:${temp_password}" | chpasswd
        chage -d 0 "$username"
        
        log "CREATE USER: ${username} (${fullname}) in group ${group}"
        echo "ユーザー作成: ${username} / パスワード: ${temp_password}"
        
    done < "$USERS_CSV"
}

# ユーザーの無効化
disable_user() {
    local username=$1
    
    if ! id "$username" &>/dev/null; then
        log "ERROR: ユーザー ${username} が見つかりません"
        return 1
    fi
    
    # アカウントのロック
    usermod -L "$username"
    
    # シェルをnologinに変更
    usermod -s /usr/sbin/nologin "$username"
    
    # 現在のセッションを強制終了
    pkill -KILL -u "$username" 2>/dev/null || true
    
    log "DISABLE USER: ${username}"
}

# ディスク使用量レポート
user_disk_report() {
    echo "=== ユーザーディスク使用量レポート ==="
    echo "$(date '+%Y-%m-%d %H:%M:%S')"
    echo "========================================"
    printf "%-20s %10s\n" "ユーザー" "使用量"
    echo "----------------------------------------"
    
    for home_dir in /home/*/; do
        [[ -d "$home_dir" ]] || continue
        local user
        user=$(basename "$home_dir")
        local usage
        usage=$(du -sh "$home_dir" 2>/dev/null | cut -f1)
        printf "%-20s %10s\n" "$user" "$usage"
    done | sort -k2 -h -r
}

# メイン処理
case "${1:-}" in
    create)
        create_users_from_csv
        ;;
    disable)
        disable_user "${2:?ユーザー名を指定してください}"
        ;;
    report)
        user_disk_report
        ;;
    *)
        echo "Usage: $0 {create|disable <username>|report}"
        exit 1
        ;;
esac

12.4 サービス管理

#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# サービス管理ラッパースクリプト
# systemd サービスの統合管理
# ============================================================

readonly SERVICES=("nginx" "postgresql" "redis-server" "myapp")
readonly HEALTH_ENDPOINTS=(
    "http://localhost:80/health"
    ""  # PostgreSQLはポートチェック
    ""  # Redisはポートチェック
    "http://localhost:8080/api/health"
)

service_status() {
    echo "=== サービスステータス ==="
    printf "%-20s %-12s %-10s %s\n" "サービス" "状態" "PID" "稼働時間"
    echo "================================================================"
    
    for service in "${SERVICES[@]}"; do
        local status pid uptime_info
        if systemctl is-active --quiet "$service"; then
            status="RUNNING"
            pid=$(systemctl show -p MainPID --value "$service")
            uptime_info=$(systemctl show -p ActiveEnterTimestamp --value "$service")
        else
            status="STOPPED"
            pid="-"
            uptime_info="-"
        fi
        printf "%-20s %-12s %-10s %s\n" "$service" "$status" "$pid" "$uptime_info"
    done
}

service_start_all() {
    for service in "${SERVICES[@]}"; do
        echo -n "Starting ${service}... "
        if systemctl start "$service"; then
            echo "OK"
        else
            echo "FAILED"
        fi
    done
}

service_stop_all() {
    # 逆順で停止(依存関係を考慮)
    for ((i = ${#SERVICES[@]} - 1; i >= 0; i--)); do
        local service="${SERVICES[i]}"
        echo -n "Stopping ${service}... "
        if systemctl stop "$service"; then
            echo "OK"
        else
            echo "FAILED"
        fi
    done
}

health_check_all() {
    echo "=== ヘルスチェック ==="
    local all_healthy=true
    
    for i in "${!SERVICES[@]}"; do
        local service="${SERVICES[i]}"
        local endpoint="${HEALTH_ENDPOINTS[i]}"
        local healthy=false
        
        if [[ -n "$endpoint" ]]; then
            if curl -sf "$endpoint" > /dev/null 2>&1; then
                healthy=true
            fi
        else
            if systemctl is-active --quiet "$service"; then
                healthy=true
            fi
        fi
        
        if $healthy; then
            echo "  [OK]   ${service}"
        else
            echo "  [FAIL] ${service}"
            all_healthy=false
        fi
    done
    
    $all_healthy
}

case "${1:-status}" in
    status)     service_status ;;
    start)      service_start_all ;;
    stop)       service_stop_all ;;
    restart)    service_stop_all; sleep 2; service_start_all ;;
    health)     health_check_all ;;
    *)          echo "Usage: $0 {status|start|stop|restart|health}" ;;
esac

第13章: CI/CD とDevOps活用

13.1 ビルドスクリプト

#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# ビルド・デプロイスクリプト
# ============================================================

readonly PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
readonly BUILD_DIR="${PROJECT_ROOT}/build"
readonly DIST_DIR="${PROJECT_ROOT}/dist"
readonly VERSION_FILE="${PROJECT_ROOT}/VERSION"

# バージョン管理
get_version() {
    if [[ -f "$VERSION_FILE" ]]; then
        cat "$VERSION_FILE"
    else
        git describe --tags --always 2>/dev/null || echo "0.0.0-dev"
    fi
}

bump_version() {
    local current version_parts major minor patch new_version
    current=$(get_version)
    IFS='.' read -ra version_parts <<< "${current%-*}"
    major="${version_parts[0]}"
    minor="${version_parts[1]}"
    patch="${version_parts[2]}"
    
    case "${1:-patch}" in
        major) new_version="$((major + 1)).0.0" ;;
        minor) new_version="${major}.$((minor + 1)).0" ;;
        patch) new_version="${major}.${minor}.$((patch + 1))" ;;
    esac
    
    echo "$new_version" > "$VERSION_FILE"
    echo "Version: ${current} -> ${new_version}"
}

# ビルド
build() {
    local version
    version=$(get_version)
    echo "Building version ${version}..."
    
    rm -rf "$BUILD_DIR"
    mkdir -p "$BUILD_DIR"
    
    # アプリケーションのビルド(例: Go)
    CGO_ENABLED=0 go build \
        -ldflags "-X main.Version=${version} -X main.BuildTime=$(date -u +%Y%m%d%H%M%S)" \
        -o "${BUILD_DIR}/myapp" \
        ./cmd/myapp/
    
    echo "Build complete: ${BUILD_DIR}/myapp"
}

# テスト
test_suite() {
    echo "=== Running Tests ==="
    
    # ユニットテスト
    echo "--- Unit Tests ---"
    go test -v -race -coverprofile=coverage.out ./...
    
    # カバレッジレポート
    go tool cover -html=coverage.out -o coverage.html
    local coverage
    coverage=$(go tool cover -func=coverage.out | tail -1 | awk '{print $3}')
    echo "Total coverage: ${coverage}"
    
    # リンター
    echo "--- Lint Check ---"
    golangci-lint run ./...
    
    echo "All tests passed!"
}

# パッケージング
package() {
    local version
    version=$(get_version)
    
    mkdir -p "$DIST_DIR"
    
    # Linux AMD64
    GOOS=linux GOARCH=amd64 go build \
        -ldflags "-X main.Version=${version}" \
        -o "${BUILD_DIR}/myapp-linux-amd64" \
        ./cmd/myapp/
    
    # Linux ARM64
    GOOS=linux GOARCH=arm64 go build \
        -ldflags "-X main.Version=${version}" \
        -o "${BUILD_DIR}/myapp-linux-arm64" \
        ./cmd/myapp/
    
    # macOS ARM64
    GOOS=darwin GOARCH=arm64 go build \
        -ldflags "-X main.Version=${version}" \
        -o "${BUILD_DIR}/myapp-darwin-arm64" \
        ./cmd/myapp/
    
    # tarball作成
    for binary in "${BUILD_DIR}"/myapp-*; do
        local name
        name=$(basename "$binary")
        tar czf "${DIST_DIR}/${name}-${version}.tar.gz" \
            -C "$BUILD_DIR" "$name"
        sha256sum "${DIST_DIR}/${name}-${version}.tar.gz" >> \
            "${DIST_DIR}/checksums.txt"
    done
    
    echo "Packages created in ${DIST_DIR}/"
}

# メイン
case "${1:-build}" in
    build)     build ;;
    test)      test_suite ;;
    package)   package ;;
    bump)      bump_version "${2:-patch}" ;;
    version)   get_version ;;
    clean)     rm -rf "$BUILD_DIR" "$DIST_DIR" ;;
    all)       test_suite && build && package ;;
    *)         echo "Usage: $0 {build|test|package|bump [major|minor|patch]|version|clean|all}" ;;
esac

13.2 デプロイスクリプト

#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# ブルーグリーンデプロイスクリプト
# ============================================================

readonly APP_NAME="myapp"
readonly DEPLOY_DIR="/opt/${APP_NAME}"
readonly BLUE_DIR="${DEPLOY_DIR}/blue"
readonly GREEN_DIR="${DEPLOY_DIR}/green"
readonly CURRENT_LINK="${DEPLOY_DIR}/current"
readonly ROLLBACK_FILE="${DEPLOY_DIR}/.last_deployment"
readonly NGINX_UPSTREAM_CONF="/etc/nginx/conf.d/upstream.conf"

# 現在のアクティブ環境を取得
get_active() {
    if [[ -L "$CURRENT_LINK" ]]; then
        basename "$(readlink -f "$CURRENT_LINK")"
    else
        echo "none"
    fi
}

# 非アクティブ環境を取得
get_inactive() {
    local active
    active=$(get_active)
    if [[ "$active" == "blue" ]]; then
        echo "green"
    else
        echo "blue"
    fi
}

# デプロイ
deploy() {
    local artifact=$1
    local target
    target=$(get_inactive)
    local target_dir="${DEPLOY_DIR}/${target}"
    
    echo "デプロイ先: ${target}"
    echo "アーティファクト: ${artifact}"
    
    # 新バージョンのデプロイ
    mkdir -p "$target_dir"
    tar xzf "$artifact" -C "$target_dir"
    
    # 設定ファイルのコピー
    cp "${DEPLOY_DIR}/shared/config.env" "${target_dir}/"
    
    # ヘルスチェック(デプロイ先でアプリを起動テスト)
    echo "ヘルスチェック中..."
    "${target_dir}/myapp" --health-check || {
        echo "ヘルスチェック失敗!デプロイを中止します" >&2
        return 1
    }
    
    # 切り替え前の状態を保存
    echo "$(get_active)" > "$ROLLBACK_FILE"
    
    # シンボリックリンクの切り替え(アトミック操作)
    ln -sfn "$target_dir" "${CURRENT_LINK}.new"
    mv -T "${CURRENT_LINK}.new" "$CURRENT_LINK"
    
    # Nginx設定の更新
    update_nginx_upstream "$target"
    
    # サービスの再起動
    systemctl restart "$APP_NAME"
    
    # 最終ヘルスチェック
    sleep 5
    if ! curl -sf "http://localhost:8080/health" > /dev/null; then
        echo "デプロイ後のヘルスチェック失敗!ロールバックします" >&2
        rollback
        return 1
    fi
    
    echo "デプロイ成功: ${target}"
}

# ロールバック
rollback() {
    if [[ ! -f "$ROLLBACK_FILE" ]]; then
        echo "ロールバック情報が見つかりません" >&2
        return 1
    fi
    
    local previous
    previous=$(cat "$ROLLBACK_FILE")
    local previous_dir="${DEPLOY_DIR}/${previous}"
    
    echo "ロールバック先: ${previous}"
    
    ln -sfn "$previous_dir" "${CURRENT_LINK}.new"
    mv -T "${CURRENT_LINK}.new" "$CURRENT_LINK"
    
    update_nginx_upstream "$previous"
    systemctl restart "$APP_NAME"
    
    echo "ロールバック完了: ${previous}"
}

# Nginx upstream設定の更新
update_nginx_upstream() {
    local target=$1
    local port
    
    if [[ "$target" == "blue" ]]; then
        port=8081
    else
        port=8082
    fi
    
    cat > "$NGINX_UPSTREAM_CONF" << EOF
upstream ${APP_NAME}_backend {
    server 127.0.0.1:${port};
}
EOF
    
    nginx -t && systemctl reload nginx
}

case "${1:-}" in
    deploy)   deploy "${2:?アーティファクトを指定してください}" ;;
    rollback) rollback ;;
    status)   echo "Active: $(get_active)" ;;
    *)        echo "Usage: $0 {deploy <artifact>|rollback|status}" ;;
esac

13.3 GitHub Actions / CI連携

#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# CI/CD パイプラインヘルパースクリプト
# GitHub Actions, Jenkins, GitLab CI で使用
# ============================================================

# --- 環境変数の設定(CI環境に応じて) ---
detect_ci_environment() {
    if [[ -n "${GITHUB_ACTIONS:-}" ]]; then
        CI_PLATFORM="github"
        CI_BRANCH="${GITHUB_REF_NAME}"
        CI_COMMIT="${GITHUB_SHA}"
        CI_BUILD_NUMBER="${GITHUB_RUN_NUMBER}"
        CI_PR_NUMBER="${GITHUB_EVENT_PULL_REQUEST_NUMBER:-}"
    elif [[ -n "${GITLAB_CI:-}" ]]; then
        CI_PLATFORM="gitlab"
        CI_BRANCH="${CI_COMMIT_REF_NAME}"
        CI_COMMIT="${CI_COMMIT_SHA}"
        CI_BUILD_NUMBER="${CI_PIPELINE_IID}"
        CI_PR_NUMBER="${CI_MERGE_REQUEST_IID:-}"
    elif [[ -n "${JENKINS_URL:-}" ]]; then
        CI_PLATFORM="jenkins"
        CI_BRANCH="${GIT_BRANCH:-unknown}"
        CI_COMMIT="${GIT_COMMIT:-unknown}"
        CI_BUILD_NUMBER="${BUILD_NUMBER}"
        CI_PR_NUMBER="${CHANGE_ID:-}"
    else
        CI_PLATFORM="local"
        CI_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
        CI_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
        CI_BUILD_NUMBER="local"
        CI_PR_NUMBER=""
    fi
    
    export CI_PLATFORM CI_BRANCH CI_COMMIT CI_BUILD_NUMBER CI_PR_NUMBER
}

# --- GitHub Actions 出力ヘルパー ---
gh_set_output() {
    local name=$1 value=$2
    echo "${name}=${value}" >> "${GITHUB_OUTPUT:-/dev/null}"
}

gh_set_env() {
    local name=$1 value=$2
    echo "${name}=${value}" >> "${GITHUB_ENV:-/dev/null}"
}

gh_group_start() {
    echo "::group::$1"
}

gh_group_end() {
    echo "::endgroup::"
}

gh_warning() {
    echo "::warning::$1"
}

gh_error() {
    echo "::error::$1"
}

# --- Docker イメージビルド ---
build_docker_image() {
    local image_name=$1
    local tag=${2:-latest}
    local dockerfile=${3:-Dockerfile}
    
    echo "Building Docker image: ${image_name}:${tag}"
    
    docker build \
        --file "$dockerfile" \
        --tag "${image_name}:${tag}" \
        --tag "${image_name}:${CI_COMMIT:0:7}" \
        --build-arg "VERSION=${tag}" \
        --build-arg "BUILD_TIME=$(date -u +%Y%m%dT%H%M%SZ)" \
        --build-arg "GIT_COMMIT=${CI_COMMIT}" \
        --label "org.opencontainers.image.version=${tag}" \
        --label "org.opencontainers.image.revision=${CI_COMMIT}" \
        .
    
    echo "Docker image built successfully"
}

# --- Slack通知 ---
notify_slack() {
    local status=$1  # success, failure, started
    local message=${2:-}
    local webhook_url="${SLACK_WEBHOOK_URL:?SLACK_WEBHOOK_URLが設定されていません}"
    
    local color
    case "$status" in
        success) color="#36a64f" ;;
        failure) color="#ff0000" ;;
        started) color="#ffcc00" ;;
        *)       color="#808080" ;;
    esac
    
    local payload
    payload=$(cat << EOF
{
    "attachments": [{
        "color": "${color}",
        "title": "CI/CD Pipeline - ${status^^}",
        "fields": [
            {"title": "Platform", "value": "${CI_PLATFORM}", "short": true},
            {"title": "Branch", "value": "${CI_BRANCH}", "short": true},
            {"title": "Build", "value": "#${CI_BUILD_NUMBER}", "short": true},
            {"title": "Commit", "value": "${CI_COMMIT:0:7}", "short": true}
        ],
        "text": "${message}",
        "ts": $(date +%s)
    }]
}
EOF
)
    
    curl -sf -X POST \
        -H "Content-Type: application/json" \
        -d "$payload" \
        "$webhook_url"
}

detect_ci_environment

第14章: Docker / Kubernetes との連携

14.1 Dockerスクリプティング

#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# Docker管理スクリプト
# ============================================================

# --- Dockerfileのエントリポイントスクリプト ---
# docker-entrypoint.sh
cat << 'ENTRYPOINT_SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

# 環境変数のデフォルト値
export APP_PORT="${APP_PORT:-8080}"
export APP_ENV="${APP_ENV:-production}"
export LOG_LEVEL="${LOG_LEVEL:-info}"
export DB_HOST="${DB_HOST:-localhost}"
export DB_PORT="${DB_PORT:-5432}"

# データベースの接続待ち
wait_for_db() {
    local max_attempts=30
    local attempt=1
    
    echo "Waiting for database at ${DB_HOST}:${DB_PORT}..."
    
    while (( attempt <= max_attempts )); do
        if pg_isready -h "$DB_HOST" -p "$DB_PORT" -q 2>/dev/null; then
            echo "Database is ready!"
            return 0
        fi
        echo "  Attempt ${attempt}/${max_attempts}..."
        sleep 2
        ((attempt++))
    done
    
    echo "ERROR: Database not available" >&2
    return 1
}

# マイグレーションの実行
run_migrations() {
    echo "Running database migrations..."
    ./migrate -path ./migrations -database \
        "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" \
        up
}

# シグナルハンドリング
_term() {
    echo "Received SIGTERM, shutting down gracefully..."
    kill -TERM "$CHILD_PID" 2>/dev/null
    wait "$CHILD_PID"
}

trap _term SIGTERM SIGINT

# 起動処理
echo "Starting application..."
echo "  Environment: ${APP_ENV}"
echo "  Port: ${APP_PORT}"

wait_for_db

if [[ "${RUN_MIGRATIONS:-false}" == "true" ]]; then
    run_migrations
fi

# アプリケーション起動
exec "$@" &
CHILD_PID=$!
wait "$CHILD_PID"
ENTRYPOINT_SCRIPT

# --- Docker Compose管理 ---
readonly COMPOSE_FILE="docker-compose.yml"
readonly COMPOSE_OVERRIDE="docker-compose.override.yml"

dc_up() {
    local env="${1:-development}"
    
    local compose_files=("-f" "$COMPOSE_FILE")
    
    if [[ -f "docker-compose.${env}.yml" ]]; then
        compose_files+=("-f" "docker-compose.${env}.yml")
    fi
    
    if [[ "$env" == "development" && -f "$COMPOSE_OVERRIDE" ]]; then
        compose_files+=("-f" "$COMPOSE_OVERRIDE")
    fi
    
    docker compose "${compose_files[@]}" up -d --build
    docker compose "${compose_files[@]}" ps
}

dc_logs() {
    docker compose -f "$COMPOSE_FILE" logs -f --tail=100 "$@"
}

dc_cleanup() {
    echo "Cleaning up Docker resources..."
    
    # 停止したコンテナの削除
    docker container prune -f
    
    # 未使用イメージの削除
    docker image prune -f
    
    # 未使用ボリュームの削除
    docker volume prune -f
    
    # 未使用ネットワークの削除
    docker network prune -f
    
    # ダングリングイメージの削除
    docker images -f "dangling=true" -q | xargs -r docker rmi -f
    
    echo "Cleanup complete"
    docker system df
}

# --- イメージ管理 ---
build_and_push() {
    local image=$1
    local tag=$2
    local registry="${DOCKER_REGISTRY:-ghcr.io/myorg}"
    local full_image="${registry}/${image}:${tag}"
    
    echo "Building: ${full_image}"
    docker build -t "$full_image" .
    
    echo "Pushing: ${full_image}"
    docker push "$full_image"
    
    echo "Done: ${full_image}"
}

14.2 Kubernetes操作スクリプト

#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# Kubernetes管理スクリプト
# ============================================================

readonly NAMESPACE="${K8S_NAMESPACE:-default}"
readonly CONTEXT="${K8S_CONTEXT:-}"

# コンテキスト設定
k() {
    local args=("$@")
    if [[ -n "$CONTEXT" ]]; then
        kubectl --context "$CONTEXT" --namespace "$NAMESPACE" "${args[@]}"
    else
        kubectl --namespace "$NAMESPACE" "${args[@]}"
    fi
}

# Pod一覧と状態確認
pod_status() {
    echo "=== Pod Status (${NAMESPACE}) ==="
    k get pods -o wide
    echo
    
    # 異常なPodの検出
    local unhealthy
    unhealthy=$(k get pods --field-selector='status.phase!=Running,status.phase!=Succeeded' \
        -o name 2>/dev/null || true)
    
    if [[ -n "$unhealthy" ]]; then
        echo "=== Unhealthy Pods ==="
        echo "$unhealthy"
        echo
        for pod in $unhealthy; do
            echo "--- ${pod} ---"
            k describe "$pod" | tail -20
            echo
        done
    fi
}

# ローリングデプロイ
rolling_deploy() {
    local deployment=$1
    local image=$2
    
    echo "Deploying ${image} to ${deployment}..."
    
    # イメージの更新
    k set image "deployment/${deployment}" "${deployment}=${image}"
    
    # ロールアウト状態の監視
    echo "Waiting for rollout..."
    if k rollout status "deployment/${deployment}" --timeout=300s; then
        echo "Deployment successful!"
        k get pods -l "app=${deployment}"
    else
        echo "Deployment failed! Rolling back..."
        k rollout undo "deployment/${deployment}"
        k rollout status "deployment/${deployment}" --timeout=120s
        return 1
    fi
}

# ログ収集
collect_logs() {
    local label=$1
    local since=${2:-1h}
    local output_dir="/tmp/k8s-logs-$(date +%Y%m%d_%H%M%S)"
    
    mkdir -p "$output_dir"
    
    local pods
    pods=$(k get pods -l "$label" -o name)
    
    for pod in $pods; do
        local pod_name
        pod_name=$(echo "$pod" | cut -d'/' -f2)
        echo "Collecting logs from ${pod_name}..."
        
        # 各コンテナのログ
        local containers
        containers=$(k get "$pod" -o jsonpath='{.spec.containers[*].name}')
        
        for container in $containers; do
            k logs "$pod" -c "$container" --since="$since" \
                > "${output_dir}/${pod_name}_${container}.log" 2>&1 || true
        done
    done
    
    echo "Logs collected in: ${output_dir}"
    tar czf "${output_dir}.tar.gz" -C "$(dirname "$output_dir")" "$(basename "$output_dir")"
    echo "Archive: ${output_dir}.tar.gz"
}

# リソース使用量
resource_usage() {
    echo "=== Node Resources ==="
    k top nodes 2>/dev/null || echo "Metrics server not available"
    echo
    echo "=== Pod Resources (${NAMESPACE}) ==="
    k top pods 2>/dev/null || echo "Metrics server not available"
}

# Secret管理
manage_secret() {
    local action=$1
    local name=$2
    
    case "$action" in
        create)
            local env_file="${3:?env fileを指定してください}"
            k create secret generic "$name" --from-env-file="$env_file"
            ;;
        view)
            k get secret "$name" -o json | \
                jq -r '.data | to_entries[] | "\(.key): \(.value | @base64d)"'
            ;;
        delete)
            k delete secret "$name"
            ;;
    esac
}

case "${1:-status}" in
    status)    pod_status ;;
    deploy)    rolling_deploy "${2:?deployment名}" "${3:?イメージ名}" ;;
    logs)      collect_logs "${2:?ラベル}" "${3:-1h}" ;;
    resources) resource_usage ;;
    secret)    manage_secret "${2:?action}" "${3:?name}" "${4:-}" ;;
    *)         echo "Usage: $0 {status|deploy|logs|resources|secret}" ;;
esac

第15章: セキュリティ

15.1 セキュアなシェルスクリプティング

#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# セキュリティのベストプラクティス
# ============================================================

# --- 1. 入力のバリデーション ---
validate_input() {
    local input=$1
    local type=$2
    
    case "$type" in
        alphanumeric)
            if [[ ! "$input" =~ ^[a-zA-Z0-9]+$ ]]; then
                echo "無効な入力: 英数字のみ許可されます" >&2
                return 1
            fi
            ;;
        filename)
            # パストラバーサル攻撃の防止
            if [[ "$input" == *".."* || "$input" == *"/"* ]]; then
                echo "無効なファイル名: ディレクトリ操作は許可されません" >&2
                return 1
            fi
            ;;
        integer)
            if [[ ! "$input" =~ ^-?[0-9]+$ ]]; then
                echo "無効な入力: 整数のみ許可されます" >&2
                return 1
            fi
            ;;
        email)
            if [[ ! "$input" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
                echo "無効なメールアドレス" >&2
                return 1
            fi
            ;;
        ip)
            if [[ ! "$input" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
                echo "無効なIPアドレス" >&2
                return 1
            fi
            ;;
    esac
    return 0
}

# --- 2. コマンドインジェクションの防止 ---
# 悪い例(コマンドインジェクション脆弱性)
# user_input="; rm -rf /"
# eval "echo $user_input"        # 危険!
# bash -c "echo $user_input"     # 危険!

# 良い例
safe_echo() {
    local user_input=$1
    echo "$user_input"  # クォートで保護
    printf '%s\n' "$user_input"  # printfを使用
}

# --- 3. 一時ファイルのセキュリティ ---
# 悪い例
# tmpfile="/tmp/myapp_$$"  # 予測可能で競合状態の可能性

# 良い例
tmpfile=$(mktemp "${TMPDIR:-/tmp}/myapp.XXXXXXXXXX")
chmod 600 "$tmpfile"  # オーナーのみ読み書き
trap 'rm -f "$tmpfile"' EXIT

# 一時ディレクトリ
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/myapp.XXXXXXXXXX")
chmod 700 "$tmpdir"
trap 'rm -rf "$tmpdir"' EXIT

# --- 4. 機密情報の取り扱い ---
# パスワード/トークンをコマンドライン引数で渡さない
# 悪い例: curl -u "user:$PASSWORD" https://api.example.com
# psコマンドで他のユーザーから見える

# 良い例: 環境変数 or ファイルから読み取り
read_secret() {
    local secret_file=$1
    
    if [[ ! -f "$secret_file" ]]; then
        echo "Secret file not found: ${secret_file}" >&2
        return 1
    fi
    
    # パーミッションチェック
    local perms
    perms=$(stat -f '%Lp' "$secret_file" 2>/dev/null || stat -c '%a' "$secret_file" 2>/dev/null)
    if [[ "$perms" != "600" && "$perms" != "400" ]]; then
        echo "警告: ${secret_file} のパーミッションが緩すぎます (${perms})" >&2
        echo "chmod 600 ${secret_file} を実行してください" >&2
        return 1
    fi
    
    cat "$secret_file"
}

# curlで安全にパスワードを渡す
api_token=$(read_secret "$HOME/.api_token")
curl -sf -H "Authorization: Bearer ${api_token}" \
    https://api.example.com/data

# --- 5. メモリ上の機密情報のクリア ---
# 変数を上書きして削除
secure_unset() {
    local varname=$1
    # 変数を空文字列で上書き(メモリ上のデータ消去)
    eval "${varname}=''"
    unset "$varname"
}

password="sensitive_data"
# 使用後
secure_unset password

# --- 6. 安全なPATH設定 ---
# スクリプト冒頭でPATHを明示的に設定
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# --- 7. umask の適切な設定 ---
umask 077  # 新規ファイルはオーナーのみ読み書き

15.2 SSL/TLS証明書管理

#!/usr/bin/env bash
set -euo pipefail

# --- 証明書情報の確認 ---
check_certificate() {
    local host=$1
    local port=${2:-443}
    
    echo "=== Certificate Info: ${host}:${port} ==="
    
    echo | openssl s_client -servername "$host" -connect "${host}:${port}" 2>/dev/null | \
        openssl x509 -noout \
        -subject -issuer -dates -fingerprint -serial
    
    # 有効期限チェック
    local expiry_date
    expiry_date=$(echo | openssl s_client -servername "$host" \
        -connect "${host}:${port}" 2>/dev/null | \
        openssl x509 -noout -enddate | cut -d= -f2)
    
    local expiry_epoch
    expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null || \
        date -j -f "%b %d %H:%M:%S %Y %Z" "$expiry_date" +%s 2>/dev/null)
    local now_epoch
    now_epoch=$(date +%s)
    local days_until_expiry=$(( (expiry_epoch - now_epoch) / 86400 ))
    
    echo "Days until expiry: ${days_until_expiry}"
    
    if (( days_until_expiry < 30 )); then
        echo "WARNING: Certificate expires in less than 30 days!" >&2
        return 1
    fi
}

# --- 自己署名証明書の生成 ---
generate_self_signed() {
    local domain=$1
    local days=${2:-365}
    local output_dir="${3:-.}"
    
    openssl req -x509 -newkey rsa:4096 \
        -keyout "${output_dir}/${domain}.key" \
        -out "${output_dir}/${domain}.crt" \
        -sha256 -days "$days" -nodes \
        -subj "/CN=${domain}" \
        -addext "subjectAltName=DNS:${domain},DNS:*.${domain}"
    
    chmod 600 "${output_dir}/${domain}.key"
    chmod 644 "${output_dir}/${domain}.crt"
    
    echo "証明書: ${output_dir}/${domain}.crt"
    echo "秘密鍵: ${output_dir}/${domain}.key"
}

# --- 複数ホストの証明書期限監視 ---
monitor_certificates() {
    local hosts_file=$1
    local warning_days=${2:-30}
    
    echo "=== Certificate Expiry Monitor ==="
    printf "%-30s %-12s %s\n" "Host" "Days Left" "Status"
    echo "================================================================"
    
    while IFS= read -r host; do
        [[ -z "$host" || "$host" =~ ^# ]] && continue
        
        local days status
        days=$(echo | openssl s_client -servername "$host" \
            -connect "${host}:443" 2>/dev/null | \
            openssl x509 -noout -enddate 2>/dev/null | \
            cut -d= -f2)
        
        if [[ -z "$days" ]]; then
            printf "%-30s %-12s %s\n" "$host" "N/A" "UNREACHABLE"
            continue
        fi
        
        local expiry_epoch now_epoch days_left
        expiry_epoch=$(date -d "$days" +%s 2>/dev/null || echo 0)
        now_epoch=$(date +%s)
        days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
        
        if (( days_left < 0 )); then
            status="EXPIRED"
        elif (( days_left < warning_days )); then
            status="WARNING"
        else
            status="OK"
        fi
        
        printf "%-30s %-12d %s\n" "$host" "$days_left" "$status"
    done < "$hosts_file"
}

15.3 監査とコンプライアンス

#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# セキュリティ監査スクリプト
# ============================================================

audit_report() {
    local report_file="/tmp/security_audit_$(date +%Y%m%d).txt"
    
    {
        echo "============================================"
        echo " Security Audit Report"
        echo " Date: $(date)"
        echo " Host: $(hostname)"
        echo "============================================"
        echo
        
        # 1. SUID/SGIDファイルの検出
        echo "=== SUID/SGID Files ==="
        find / -type f \( -perm -4000 -o -perm -2000 \) \
            -exec ls -la {} \; 2>/dev/null
        echo
        
        # 2. ワールドライタブルファイル
        echo "=== World-Writable Files ==="
        find / -type f -perm -o+w -not -path "/proc/*" \
            -not -path "/sys/*" 2>/dev/null | head -50
        echo
        
        # 3. 所有者なしファイル
        echo "=== Orphaned Files ==="
        find / -nouser -o -nogroup 2>/dev/null | head -20
        echo
        
        # 4. パスワードポリシー
        echo "=== Password Policy ==="
        grep -E "^PASS_" /etc/login.defs 2>/dev/null || echo "N/A"
        echo
        
        # 5. sudoers設定
        echo "=== Sudoers (NOPASSWD entries) ==="
        grep -r "NOPASSWD" /etc/sudoers /etc/sudoers.d/ 2>/dev/null || echo "None"
        echo
        
        # 6. リスニングポート
        echo "=== Listening Ports ==="
        ss -tuln 2>/dev/null || netstat -tuln 2>/dev/null
        echo
        
        # 7. ファイアウォールルール
        echo "=== Firewall Rules ==="
        iptables -L -n 2>/dev/null || echo "iptables not available"
        echo
        
        # 8. 最近のログイン失敗
        echo "=== Recent Login Failures ==="
        lastb 2>/dev/null | head -20 || echo "N/A"
        echo
        
        # 9. SSH設定チェック
        echo "=== SSH Configuration ==="
        grep -E "^(PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|Port)" \
            /etc/ssh/sshd_config 2>/dev/null || echo "N/A"
        
    } > "$report_file"
    
    echo "監査レポート: ${report_file}"
}

audit_report

第16章: 実践的なスクリプトパターン

16.1 CLIツールのテンプレート

#!/usr/bin/env bash
# ============================================================
# mytool - 多機能CLIツールのテンプレート
# ============================================================
set -euo pipefail

readonly VERSION="1.0.0"
readonly SCRIPT_NAME="$(basename "$0")"

# デフォルト値
VERBOSE=false
DRY_RUN=false
CONFIG_FILE=""
OUTPUT_FORMAT="text"

# カラー出力
if [[ -t 1 ]]; then
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    YELLOW='\033[1;33m'
    BLUE='\033[0;34m'
    NC='\033[0m'
else
    RED='' GREEN='' YELLOW='' BLUE='' NC=''
fi

info()  { echo -e "${GREEN}[INFO]${NC} $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
debug() { $VERBOSE && echo -e "${BLUE}[DEBUG]${NC} $*" >&2 || true; }

# 使用方法
usage() {
    cat << EOF
Usage: ${SCRIPT_NAME} [OPTIONS] COMMAND [ARGS...]

多機能CLIツールのテンプレート

Commands:
  init          プロジェクトの初期化
  build         プロジェクトのビルド
  deploy        デプロイの実行
  status        ステータスの確認

Options:
  -v, --verbose     詳細出力を有効化
  -n, --dry-run     ドライラン(実際には実行しない)
  -c, --config FILE 設定ファイルの指定
  -f, --format FMT  出力形式 (text|json|yaml) [default: text]
  -h, --help        このヘルプを表示
  -V, --version     バージョンを表示

Examples:
  ${SCRIPT_NAME} init --config myproject.yml
  ${SCRIPT_NAME} build --verbose
  ${SCRIPT_NAME} deploy --dry-run
  ${SCRIPT_NAME} status --format json

EOF
}

# 引数のパース
parse_args() {
    local positional=()
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -v|--verbose)
                VERBOSE=true
                shift
                ;;
            -n|--dry-run)
                DRY_RUN=true
                shift
                ;;
            -c|--config)
                CONFIG_FILE="$2"
                shift 2
                ;;
            -f|--format)
                OUTPUT_FORMAT="$2"
                shift 2
                ;;
            -h|--help)
                usage
                exit 0
                ;;
            -V|--version)
                echo "${SCRIPT_NAME} version ${VERSION}"
                exit 0
                ;;
            --)
                shift
                positional+=("$@")
                break
                ;;
            -*)
                error "不明なオプション: $1"
                usage
                exit 1
                ;;
            *)
                positional+=("$1")
                shift
                ;;
        esac
    done
    
    set -- "${positional[@]}"
    COMMAND="${1:-}"
    shift || true
    COMMAND_ARGS=("$@")
}

# コマンドの実行
cmd_init() {
    info "プロジェクトを初期化しています..."
    debug "Config: ${CONFIG_FILE:-default}"
    
    if $DRY_RUN; then
        info "[DRY-RUN] 初期化をスキップ"
        return
    fi
    
    # 初期化ロジック
    info "初期化完了"
}

cmd_build() {
    info "ビルドを開始します..."
    # ビルドロジック
    info "ビルド完了"
}

cmd_deploy() {
    info "デプロイを開始します..."
    
    if $DRY_RUN; then
        info "[DRY-RUN] デプロイをスキップ"
        return
    fi
    
    # デプロイロジック
    info "デプロイ完了"
}

cmd_status() {
    case "$OUTPUT_FORMAT" in
        json)
            echo '{"status":"running","version":"'"${VERSION}"'"}'
            ;;
        yaml)
            echo "status: running"
            echo "version: ${VERSION}"
            ;;
        text|*)
            echo "Status: running"
            echo "Version: ${VERSION}"
            ;;
    esac
}

# メイン
main() {
    parse_args "$@"
    
    debug "Command: ${COMMAND}"
    debug "Verbose: ${VERBOSE}"
    debug "Dry-run: ${DRY_RUN}"
    
    case "${COMMAND}" in
        init)   cmd_init "${COMMAND_ARGS[@]}" ;;
        build)  cmd_build "${COMMAND_ARGS[@]}" ;;
        deploy) cmd_deploy "${COMMAND_ARGS[@]}" ;;
        status) cmd_status "${COMMAND_ARGS[@]}" ;;
        "")
            error "コマンドを指定してください"
            usage
            exit 1
            ;;
        *)
            error "不明なコマンド: ${COMMAND}"
            usage
            exit 1
            ;;
    esac
}

main "$@"

16.2 設定ファイルのパーサー

#!/usr/bin/env bash

# --- INIファイルパーサー ---
declare -A INI_CONFIG

parse_ini() {
    local file=$1
    local section=""
    
    while IFS= read -r line; do
        # 空行とコメント
        [[ -z "$line" || "$line" =~ ^[[:space:]]*[#\;] ]] && continue
        
        # セクション
        if [[ "$line" =~ ^\[([^\]]+)\] ]]; then
            section="${BASH_REMATCH[1]}"
            continue
        fi
        
        # キー=値
        if [[ "$line" =~ ^[[:space:]]*([^=]+)[[:space:]]*=[[:space:]]*(.*) ]]; then
            local key="${BASH_REMATCH[1]}"
            local value="${BASH_REMATCH[2]}"
            
            # 前後の空白を除去
            key="${key%"${key##*[![:space:]]}"}"
            value="${value%"${value##*[![:space:]]}"}"
            value="${value#\"}" ; value="${value%\"}"
            
            INI_CONFIG["${section}.${key}"]="$value"
        fi
    done < "$file"
}

ini_get() {
    local key=$1
    local default=${2:-}
    echo "${INI_CONFIG[$key]:-$default}"
}

# 使用例
# parse_ini config.ini
# db_host=$(ini_get "database.host" "localhost")
# db_port=$(ini_get "database.port" "5432")

# --- YAMLライクパーサー(簡易版) ---
declare -A YAML_CONFIG

parse_simple_yaml() {
    local file=$1
    local prefix=""
    
    while IFS= read -r line; do
        [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
        
        # インデントレベルの検出
        local indent="${line%%[! ]*}"
        local level=$(( ${#indent} / 2 ))
        
        line="${line#"${indent}"}"
        
        if [[ "$line" =~ ^([^:]+):[[:space:]]+(.+) ]]; then
            local key="${BASH_REMATCH[1]}"
            local value="${BASH_REMATCH[2]}"
            value="${value#\"}" ; value="${value%\"}"
            
            YAML_CONFIG["${key}"]="$value"
        fi
    done < "$file"
}

# --- JSON処理(jq使用) ---
# jq を使ったJSON操作の例
json_operations() {
    local json_file=$1
    
    # 値の取得
    jq -r '.database.host' "$json_file"
    
    # 配列の要素数
    jq '.servers | length' "$json_file"
    
    # 条件フィルタ
    jq '.servers[] | select(.status == "active")' "$json_file"
    
    # 値の変更
    jq '.database.port = 5433' "$json_file" > tmp.$$ && mv tmp.$$ "$json_file"
    
    # 新しいキーの追加
    jq '.metadata.updated = now' "$json_file"
    
    # CSV形式で出力
    jq -r '.users[] | [.name, .email, .role] | @csv' "$json_file"
    
    # テーブル形式
    jq -r '.users[] | "\(.name)\t\(.email)\t\(.role)"' "$json_file" | column -t
}

16.3 データ処理パイプライン

#!/usr/bin/env bash
set -euo pipefail

# ============================================================
# ログ分析パイプライン
# ============================================================

readonly LOG_DIR="/var/log/nginx"
readonly REPORT_DIR="/tmp/log_analysis"

analyze_access_logs() {
    mkdir -p "$REPORT_DIR"
    
    echo "=== アクセスログ分析 ==="
    
    # 1. トップIPアドレス
    echo "--- Top 10 IP Addresses ---"
    awk '{print $1}' "${LOG_DIR}/access.log" | \
        sort | uniq -c | sort -rn | head -10 | \
        awk '{printf "  %-15s %d requests\n", $2, $1}'
    
    # 2. HTTPステータスコードの分布
    echo "--- HTTP Status Code Distribution ---"
    awk '{print $9}' "${LOG_DIR}/access.log" | \
        sort | uniq -c | sort -rn | \
        awk '{printf "  HTTP %s: %d (%d%%)\n", $2, $1, $1*100/NR}'
    
    # 3. トップリクエストパス
    echo "--- Top 20 Request Paths ---"
    awk '{print $7}' "${LOG_DIR}/access.log" | \
        sort | uniq -c | sort -rn | head -20 | \
        awk '{printf "  %-50s %d\n", $2, $1}'
    
    # 4. 時間帯別リクエスト数
    echo "--- Requests by Hour ---"
    awk '{print $4}' "${LOG_DIR}/access.log" | \
        cut -d: -f2 | sort | uniq -c | \
        awk '{printf "  %02d:00 - %02d:59: %d requests\n", $2, $2, $1}'
    
    # 5. エラーレート(5xxレスポンス)
    echo "--- Error Rate ---"
    local total errors rate
    total=$(wc -l < "${LOG_DIR}/access.log")
    errors=$(awk '$9 ~ /^5/' "${LOG_DIR}/access.log" | wc -l)
    if (( total > 0 )); then
        rate=$(echo "scale=2; $errors * 100 / $total" | bc)
    else
        rate="0.00"
    fi
    echo "  Total: ${total}, 5xx Errors: ${errors}, Rate: ${rate}%"
    
    # 6. レスポンスサイズの統計
    echo "--- Response Size Stats ---"
    awk '{print $10}' "${LOG_DIR}/access.log" | \
        grep -E '^[0-9]+$' | \
        awk '
        BEGIN { min=99999999; max=0; sum=0; count=0 }
        {
            if ($1 < min) min = $1
            if ($1 > max) max = $1
            sum += $1
            count++
        }
        END {
            if (count > 0) {
                printf "  Min: %d bytes\n  Max: %d bytes\n  Avg: %d bytes\n  Total: %.2f MB\n",
                    min, max, sum/count, sum/1048576
            }
        }'
}

# レポート生成
generate_report() {
    local report_file="${REPORT_DIR}/report_$(date +%Y%m%d).txt"
    analyze_access_logs > "$report_file" 2>&1
    echo "レポート生成完了: ${report_file}"
}

generate_report

第17章: パフォーマンスとベストプラクティス

17.1 パフォーマンス最適化

#!/usr/bin/env bash

# --- 1. ビルトインコマンドを優先する ---

# 悪い例: 外部コマンド
result=$(echo "hello" | tr '[:lower:]' '[:upper:]')

# 良い例: Bash組込み
result="${variable^^}"  # 大文字変換 (Bash 4.0+)

# 悪い例: 外部コマンド
length=$(echo -n "$string" | wc -c)

# 良い例: パラメータ展開
length=${#string}

# --- 2. サブシェルの回避 ---

# 悪い例: サブシェル内でのループ(変数が伝播しない)
count=0
cat file.txt | while read -r line; do
    ((count++))
done
echo "$count"  # 0(サブシェル内の変更は反映されない)

# 良い例: リダイレクションを使用
count=0
while read -r line; do
    ((count++))
done < file.txt
echo "$count"  # 正しい値

# --- 3. コマンド置換の最小化 ---

# 悪い例: ループ内でdate呼び出し
for i in {1..1000}; do
    timestamp=$(date '+%Y-%m-%d')
    echo "${timestamp}: item ${i}"
done

# 良い例: ループ外で1回だけ
timestamp=$(date '+%Y-%m-%d')
for i in {1..1000}; do
    echo "${timestamp}: item ${i}"
done

# --- 4. 大量のファイル処理 ---

# 悪い例: for + ls
for file in $(ls *.txt); do  # ワード分割問題もある
    process "$file"
done

# 良い例: glob + nullglob
shopt -s nullglob
for file in *.txt; do
    process "$file"
done

# 大量ファイルの場合: find + -exec
find . -name "*.txt" -exec process {} +  # バッチ実行(高速)

# --- 5. 文字列結合の最適化 ---

# 悪い例: += による繰り返し結合(O(n^2))
result=""
for i in {1..10000}; do
    result+="line ${i}\n"
done

# 良い例: printf + 配列
lines=()
for i in {1..10000}; do
    lines+=("line ${i}")
done
printf '%s\n' "${lines[@]}"

# --- 6. mapfile/readarray の活用 ---

# 悪い例
declare -a lines
while IFS= read -r line; do
    lines+=("$line")
done < large_file.txt

# 良い例 (Bash 4.0+)
mapfile -t lines < large_file.txt

# コマンド出力から
mapfile -t processes < <(ps aux | awk '{print $11}')

# --- 7. ベンチマーク ---
benchmark() {
    local iterations=$1
    shift
    local cmd=("$@")
    
    local start end elapsed
    start=$(date +%s%N)
    
    for ((i = 0; i < iterations; i++)); do
        "${cmd[@]}" > /dev/null 2>&1
    done
    
    end=$(date +%s%N)
    elapsed=$(( (end - start) / 1000000 ))
    
    echo "${iterations}回実行: ${elapsed}ms (平均: $((elapsed / iterations))ms/回)"
}

# time コマンドでの計測
time {
    for i in {1..100}; do
        echo "$i" > /dev/null
    done
}

17.2 ベストプラクティスまとめ

#!/usr/bin/env bash
# ============================================================
# Shell Script ベストプラクティスチェックリスト
# ============================================================

# [1] 常に厳格モードを使用
set -euo pipefail

# [2] 意味のある変数名を使用
readonly MAX_RETRY_COUNT=3        # Good
readonly MRC=3                    # Bad

# [3] 変数は必ずダブルクォートで囲む
echo "$variable"                  # Good
echo $variable                    # Bad

# [4] ローカル変数を使用
my_function() {
    local result=""               # Good
    result="value"                # 関数スコープ内
}

# [5] コマンド置換は $() を使用
result=$(command)                 # Good (ネスト可能)
result=`command`                  # Bad (レガシー)

# [6] [[ ]] を使用([ ] ではなく)
if [[ -f "$file" ]]; then        # Good
    :
fi

# [7] 算術式には (( )) を使用
if (( count > 0 )); then         # Good
    :
fi

# [8] 配列は適切にクォート
for item in "${array[@]}"; do    # Good
    :
done

# [9] 関数名はスネークケースで
my_function_name() {             # Good
    :
}

# [10] 定数は readonly で宣言
readonly CONFIG_FILE="/etc/myapp.conf"  # Good

# [11] エラーメッセージは stderr へ
echo "Error: something failed" >&2      # Good

# [12] 終了コードを適切に使用
# 0: 成功
# 1: 一般的なエラー
# 2: 誤った使い方
# 126: コマンドは見つかったが実行不可
# 127: コマンドが見つからない
# 128+n: シグナルnで終了

# [13] tmpファイルは mktemp で作成
tmpfile=$(mktemp)                       # Good
trap 'rm -f "$tmpfile"' EXIT

# [14] スクリプトの自己文書化
: "${REQUIRED_VAR:?環境変数REQUIRED_VARが設定されていません}"

# [15] ShellCheckを使う
# shellcheck disable=SC2034  # 意図的な未使用変数の場合

17.3 テスティング

#!/usr/bin/env bash
# ============================================================
# シンプルなテストフレームワーク
# ============================================================

TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0

assert_equals() {
    local expected=$1
    local actual=$2
    local message=${3:-""}
    
    ((TESTS_RUN++))
    
    if [[ "$expected" == "$actual" ]]; then
        ((TESTS_PASSED++))
        echo "  PASS: ${message}"
    else
        ((TESTS_FAILED++))
        echo "  FAIL: ${message}"
        echo "    Expected: '${expected}'"
        echo "    Actual:   '${actual}'"
    fi
}

assert_true() {
    local condition=$1
    local message=${2:-""}
    
    ((TESTS_RUN++))
    
    if eval "$condition"; then
        ((TESTS_PASSED++))
        echo "  PASS: ${message}"
    else
        ((TESTS_FAILED++))
        echo "  FAIL: ${message}"
        echo "    Condition: ${condition}"
    fi
}

assert_exit_code() {
    local expected_code=$1
    shift
    local message="${*: -1}"
    set -- "${@:1:$#-1}"
    
    ((TESTS_RUN++))
    
    "$@" > /dev/null 2>&1
    local actual_code=$?
    
    if (( actual_code == expected_code )); then
        ((TESTS_PASSED++))
        echo "  PASS: ${message}"
    else
        ((TESTS_FAILED++))
        echo "  FAIL: ${message} (exit code: ${actual_code}, expected: ${expected_code})"
    fi
}

test_summary() {
    echo
    echo "============================================"
    echo " Test Results"
    echo "============================================"
    echo " Total:  ${TESTS_RUN}"
    echo " Passed: ${TESTS_PASSED}"
    echo " Failed: ${TESTS_FAILED}"
    echo "============================================"
    
    if (( TESTS_FAILED > 0 )); then
        return 1
    fi
}

# --- テストの実行例 ---
# source対象のスクリプト
# source ./my_functions.sh

echo "=== String Operations Tests ==="
str="Hello, World!"
assert_equals 13 "${#str}" "文字列長の計算"
assert_equals "Hello" "${str%%,*}" "カンマ前の部分文字列"
assert_equals "World!" "${str##*, }" "カンマ後の部分文字列"

echo
echo "=== File Operations Tests ==="
tmpfile=$(mktemp)
echo "test content" > "$tmpfile"
assert_true "[[ -f '$tmpfile' ]]" "一時ファイルの存在"
assert_true "[[ -s '$tmpfile' ]]" "一時ファイルが空でない"
rm -f "$tmpfile"
assert_true "[[ ! -f '$tmpfile' ]]" "一時ファイルの削除"

echo
echo "=== Exit Code Tests ==="
assert_exit_code 0 true "true は終了コード0"
assert_exit_code 1 false "false は終了コード1"

test_summary

第18章: 高度なトピック

18.1 サブシェルとプロセスグループ

#!/usr/bin/env bash

# --- サブシェル ---
# () でサブシェルを作成(現在のシェルのコピー)
(
    cd /tmp
    echo "サブシェル内のPWD: $(pwd)"  # /tmp
    export MY_VAR="subshell_value"
)
echo "親シェルのPWD: $(pwd)"          # 元のディレクトリ
echo "MY_VAR: ${MY_VAR:-未定義}"      # 未定義(サブシェルの変更は伝播しない)

# --- グループコマンド ---
# { } はカレントシェルで実行(リダイレクションをまとめるのに便利)
{
    echo "Line 1"
    echo "Line 2"
    echo "Line 3"
} > output.txt

# --- コマンドグループの使い分け ---
# サブシェル: 環境を隔離したい場合
( cd /tmp && dangerous_command )  # 親のカレントディレクトリは変わらない

# グループ: リダイレクションをまとめたい場合
{
    echo "=== Header ==="
    date
    echo "=== Data ==="
    cat data.txt
} | mail -s "Report" admin@example.com

18.2 evalとindirect expansion

#!/usr/bin/env bash

# --- 間接参照 ---
# ${!var} による間接参照
color_name="RED"
RED="#FF0000"
echo "Color: ${!color_name}"  # #FF0000

# 動的変数名
for env in production staging development; do
    var_name="${env^^}_DB_HOST"
    echo "${env}: ${!var_name:-not set}"
done

# --- nameref (Bash 4.3+) ---
# declare -n による参照変数
create_config() {
    local -n config_ref=$1
    config_ref[host]="localhost"
    config_ref[port]="8080"
    config_ref[env]="production"
}

declare -A my_config
create_config my_config
echo "Host: ${my_config[host]}"

# --- eval の安全な使用 ---
# evalは基本的に避けるべきだが、必要な場合は入力を厳密に検証
safe_eval_example() {
    local key=$1
    # 英数字とアンダースコアのみ許可
    if [[ ! "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
        echo "Invalid key: ${key}" >&2
        return 1
    fi
    eval "echo \"\${${key}:-undefined}\""
}

18.3 動的関数生成

#!/usr/bin/env bash

# --- 関数の動的生成 ---
# HTTPメソッド用の関数を自動生成
for method in GET POST PUT DELETE PATCH; do
    eval "
    http_${method,,}() {
        local url=\$1
        shift
        curl -sf -X ${method} \"\$@\" \"\$url\"
    }
    "
done

# 使用例
# http_get "https://api.example.com/users"
# http_post "https://api.example.com/users" -d '{"name":"Alice"}'

# --- デコレータパターン ---
measure_time() {
    local func_name=$1
    local original_func
    original_func=$(declare -f "$func_name")
    
    eval "
    _original_${func_name}() {
        ${original_func#*\{}
    "
    
    eval "
    ${func_name}() {
        local start=\$(date +%s%N)
        _original_${func_name} \"\$@\"
        local status=\$?
        local end=\$(date +%s%N)
        local elapsed=\$(( (end - start) / 1000000 ))
        echo \"[TIMER] ${func_name}: \${elapsed}ms\" >&2
        return \$status
    }
    "
}

# 使用例
slow_function() {
    sleep 1
    echo "Done"
}

measure_time slow_function
slow_function  # 実行時間が表示される

18.4 heredocの高度な使い方

#!/usr/bin/env bash

# --- テンプレートエンジン ---
render_template() {
    local template_file=$1
    
    # テンプレート内の ${VAR} を環境変数で置換
    envsubst < "$template_file"
}

# --- 複数行のSQLをMySQLに渡す ---
# mysql -u root << EOF
# SELECT 
#     u.name,
#     u.email,
#     COUNT(o.id) as order_count
# FROM users u
# LEFT JOIN orders o ON u.id = o.user_id
# WHERE u.created_at >= '$(date -d '30 days ago' +%Y-%m-%d)'
# GROUP BY u.id
# ORDER BY order_count DESC
# LIMIT 10;
# EOF

# --- Pythonスクリプトの埋め込み ---
generate_chart() {
    python3 << 'PYTHON'
import json
import sys

data = {"labels": ["Jan", "Feb", "Mar"], "values": [10, 20, 30]}
print(json.dumps(data, indent=2))
PYTHON
}

# --- AWKスクリプトの埋め込み ---
analyze_log() {
    awk '
    BEGIN {
        FS = " "
        print "=== Log Analysis ==="
    }
    /ERROR/ {
        errors++
        error_types[$5]++
    }
    /WARN/ {
        warnings++
    }
    END {
        printf "Errors: %d\n", errors
        printf "Warnings: %d\n", warnings
        print "\nError Types:"
        for (t in error_types) {
            printf "  %-30s %d\n", t, error_types[t]
        }
    }
    ' "$@"
}

18.5 移植性とPOSIX互換性

#!/bin/sh
# POSIX準拠スクリプト(#!/bin/sh)

# --- Bash固有機能の代替 ---

# [[ ]] の代わりに [ ] を使用
if [ -f "/etc/hosts" ]; then
    echo "File exists"
fi

# 文字列比較は = を使用(== はBash拡張)
if [ "$var" = "value" ]; then
    echo "Match"
fi

# 配列は使えないのでスペース区切り文字列で代用
items="item1 item2 item3"
for item in $items; do
    echo "$item"
done

# 算術演算は $(( )) または expr
result=$((5 + 3))

# プロセス置換 <() は使えないので一時ファイルを使用
cmd1 > /tmp/result1.$$
cmd2 > /tmp/result2.$$
diff /tmp/result1.$$ /tmp/result2.$$
rm -f /tmp/result1.$$ /tmp/result2.$$

# local は多くのshで使えるが、厳密にはPOSIX外
# 代替: 関数スコープの変数管理に注意

# --- OS間の差異への対処 ---
detect_os() {
    case "$(uname -s)" in
        Linux*)   OS="linux" ;;
        Darwin*)  OS="macos" ;;
        FreeBSD*) OS="freebsd" ;;
        CYGWIN*|MINGW*) OS="windows" ;;
        *)        OS="unknown" ;;
    esac
    echo "$OS"
}

# OS依存コマンドのラッパー
get_file_size() {
    local file=$1
    case "$(uname -s)" in
        Linux)  stat -c%s "$file" ;;
        Darwin) stat -f%z "$file" ;;
        *)      wc -c < "$file" ;;
    esac
}

# sedの互換性 (-i オプションが異なる)
portable_sed_inplace() {
    local expression=$1
    local file=$2
    if [[ "$(uname -s)" == "Darwin" ]]; then
        sed -i '' "$expression" "$file"
    else
        sed -i "$expression" "$file"
    fi
}

# dateの互換性
portable_date_calc() {
    local days_ago=$1
    if date --version >/dev/null 2>&1; then
        # GNU date
        date -d "${days_ago} days ago" '+%Y-%m-%d'
    else
        # BSD date (macOS)
        date -v-"${days_ago}d" '+%Y-%m-%d'
    fi
}

第19章: まとめとリファレンス

19.1 Shell Scriptの選択基準

用途Shell ScriptPython/Go等
ファイル操作・パイプライン最適冗長
テキスト処理(awk/sed)最適同等
システムコマンドの組合わせ最適やや冗長
CI/CDパイプライン最適
複雑なデータ構造不向き最適
エラーハンドリング限定的最適
並行処理基本的最適
ユニットテスト限定的最適
クロスプラットフォーム注意が必要最適

目安: 100行以内で完結し、テキスト処理やコマンド実行が主体ならShell Script。それ以上の複雑さならPython等の使用を検討。

19.2 よく使うワンライナー集

# プロセスのメモリ使用量トップ10
ps aux --sort=-%mem | head -11

# ディスク使用量トップ10
du -sh /* 2>/dev/null | sort -hr | head -10

# 特定ポートを使用しているプロセス
lsof -i :8080

# ファイル内の重複行を検出
sort file.txt | uniq -d

# 2つのファイルの共通行
comm -12 <(sort file1.txt) <(sort file2.txt)

# ランダム文字列生成
openssl rand -base64 32

# 全サブディレクトリでgit pull
find . -maxdepth 2 -name ".git" -exec dirname {} \; | \
    xargs -P4 -I{} git -C {} pull

# IPアドレスの抽出
grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' access.log | \
    sort | uniq -c | sort -rn

# JSONの整形
echo '{"key":"value"}' | python3 -m json.tool

# 変更されたファイルの監視
while inotifywait -r -e modify ./src/; do
    echo "ファイルが変更されました"
    make build
done

# 指定サイズのテストファイル生成
dd if=/dev/urandom of=testfile bs=1M count=100

# タイムスタンプ付きでコマンド出力をログ
command 2>&1 | while IFS= read -r line; do
    echo "$(date '+%Y-%m-%d %H:%M:%S') ${line}"
done

19.3 参考リソース


本記事は、Shell Script(主にBash)の包括的な概要として、基礎文法からシステム管理、CI/CD、セキュリティ、パフォーマンス最適化までを網羅的に解説しました。実運用では、ShellCheckによる静的解析を活用し、セキュリティと保守性を意識したスクリプティングを心がけてください。