Shell Script
Shell Script 包括的概要ガイド
本記事では、Shell Script(主にBash)の機能・アーキテクチャ・設定例を網羅的に解説します。Linux/macOS環境でのシステム管理・自動化・DevOpsに不可欠なスキルとして、基礎から応用まで体系的にまとめています。
第1章: Shell Scriptとは
1.1 Shell Scriptの定義と歴史
Shell Scriptとは、UNIXシェル(コマンドラインインタプリタ)上で実行される一連のコマンドをファイルにまとめたプログラムのことです。対話的に入力するコマンドをスクリプトファイルとして自動化できるため、システム管理・運用自動化の要となる技術です。
歴史的経緯:
| 年代 | シェル | 開発者 | 特徴 |
|---|---|---|---|
| 1971 | Thompson Shell (sh) | Ken Thompson | 最初のUNIXシェル |
| 1977 | Bourne Shell (sh) | Stephen Bourne | プログラミング機能の導入 |
| 1978 | C Shell (csh) | Bill Joy | C言語ライクな構文 |
| 1983 | Korn Shell (ksh) | David Korn | Bourne Shell + C Shell の長所を統合 |
| 1989 | Bash (bash) | Brian Fox | GNU Project、最も普及したシェル |
| 1990 | Z Shell (zsh) | Paul Falstad | Bashの上位互換、高機能補完 |
| 2005 | Fish (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
各シェルの特徴比較:
| 機能 | sh | bash | zsh | fish | ksh |
|---|---|---|---|---|---|
| POSIX準拠 | Yes | Yes | Yes | No | Yes |
| 配列 | No | Yes | Yes | Yes | Yes |
| 連想配列 | No | Yes (4.0+) | Yes | Yes | Yes (93+) |
| プロセス置換 | No | Yes | Yes | Yes | Yes |
| 拡張glob | No | Yes | Yes | Yes | Yes |
| 自動補完 | 基本 | 基本 | 高度 | 高度 | 基本 |
| テーマ/プラグイン | No | 限定的 | Oh My Zsh | 組込み | No |
| スクリプト互換性 | 基準 | 高 | 高 | 低 | 高 |
1.4 なぜShell Scriptを学ぶのか
- システム管理の自動化: サーバー設定、ユーザー管理、パッケージ管理
- CI/CDパイプライン: Jenkins、GitHub Actions、GitLab CIのベース
- コンテナ技術: Dockerfileのエントリポイント、初期化スクリプト
- クラウド運用: AWS CLI、gcloud、az コマンドのオーケストレーション
- デバッグとトラブルシューティング: ログ分析、プロセス調査
- ポータビリティ: ほぼすべての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 Script | Python/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 参考リソース
- Bash Reference Manual: https://www.gnu.org/software/bash/manual/
- Advanced Bash-Scripting Guide: https://tldp.org/LDP/abs/html/
- ShellCheck: https://www.shellcheck.net/ (静的解析ツール)
- Bash Pitfalls: https://mywiki.wooledge.org/BashPitfalls
- Google Shell Style Guide: https://google.github.io/styleguide/shellguide.html
- explainshell.com: コマンドの解説サイト
本記事は、Shell Script(主にBash)の包括的な概要として、基礎文法からシステム管理、CI/CD、セキュリティ、パフォーマンス最適化までを網羅的に解説しました。実運用では、ShellCheckによる静的解析を活用し、セキュリティと保守性を意識したスクリプティングを心がけてください。