Shell Scripting for System Administration

Linuxシステム管理のためのシェルスクリプティング完全ガイド

対象読者: Linuxシステム管理者、SREエンジニア、DevOpsエンジニア
難易度: 中級〜上級
最終更新: 2026年4月


目次

  1. はじめに
  2. Bash基礎
  3. デバッグ技法
  4. エラーハンドリング
  5. シグナルハンドリング(trap)
  6. プロセス管理
  7. 正規表現とテキスト処理
  8. ファイル処理
  9. システム管理スクリプト実例
  10. cron・systemd タイマー連携
  11. セキュリティベストプラクティス
  12. 便利なワンライナー集
  13. まとめと参考資料

1. はじめに

シェルスクリプティングがシステム管理に不可欠な理由

Linuxシステム管理において、シェルスクリプトは単なる便利ツールではなく、インフラ運用の根幹をなすものです。手動で繰り返し実行する作業をスクリプト化することで、以下のメリットが得られます。

メリット説明
再現性同一の手順を何度でも正確に実行できる
自動化cronやsystemdタイマーと組み合わせて定期実行が可能
監査性ログに残すことで何がいつ実行されたかを追跡できる
一貫性人的ミスを排除し、環境間の差異を吸収する
スピード数百台のサーバーに対して同時に操作を適用できる

シェルの種類と選択基準

# 現在利用可能なシェルの確認
cat /etc/shells
/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/bin/dash
/usr/bin/dash
/bin/zsh
/usr/bin/zsh
シェル特徴推奨用途
bash高機能、広く普及、POSIX互換システム管理スクリプト全般
sh (dash)軽量、POSIX準拠起動スクリプト、移植性重視
zshインタラクティブ性が高い開発者の日常使い
fishユーザーフレンドリー初心者向けインタラクティブ用途
kshスクリプト性能が高いエンタープライズ環境(AIX等)

本ガイドでは bash 5.x を基準に解説します。

シェバン(shebang)とスクリプトの実行方法

#!/usr/bin/env bash
# ↑ envを使うことでbashのパスに依存しない(移植性が高い)

# または絶対パスで指定
#!/bin/bash
# スクリプトに実行権限を付与して実行
chmod +x script.sh
./script.sh

# bashコマンドで直接実行(権限不要)
bash script.sh

# デバッグモードで実行
bash -x script.sh

2. Bash基礎

2.1 変数と型

Bashは動的型付けのシェルですが、declareコマンドで型を明示的に指定できます。

変数の基本

#!/usr/bin/env bash

# 変数の代入(=の前後にスペースを入れない)
name="Alice"
age=30
pi=3.14159

# 変数の参照(${} を使うと安全)
echo "Name: ${name}"
echo "Age: ${age}"

# コマンド置換
current_date=$(date +%Y-%m-%d)
echo "Today: ${current_date}"

# 算術演算
count=10
result=$((count * 2 + 5))
echo "Result: ${result}"   # 出力: Result: 25

# let コマンドによる算術演算
let "total = count + result"
echo "Total: ${total}"

# 読み取り専用変数
readonly MAX_RETRY=3
# MAX_RETRY=5  # これはエラーになる

# 変数の削除
temp_var="temporary"
unset temp_var
echo "${temp_var:-unset}"  # 出力: unset

declare による型指定

#!/usr/bin/env bash

# 整数型
declare -i num=42
num="hello"  # 整数型なので 0 になる
echo "${num}"  # 出力: 0

# 大文字変換
declare -u upper_var="hello world"
echo "${upper_var}"  # 出力: HELLO WORLD

# 小文字変換
declare -l lower_var="HELLO WORLD"
echo "${lower_var}"  # 出力: hello world

# エクスポート(サブプロセスに引き継ぐ)
declare -x EXPORTED_VAR="visible to children"
# export EXPORTED_VAR="visible to children" と同じ

# 変数の属性確認
declare -p num upper_var lower_var
declare -i num="0"
declare -u upper_var="HELLO WORLD"
declare -l lower_var="hello world"

特殊変数

#!/usr/bin/env bash
# スクリプト名: special_vars.sh

echo "スクリプト名: $0"           # ./special_vars.sh
echo "第1引数: $1"
echo "第2引数: $2"
echo "全引数: $@"
echo "引数の数: $#"
echo "前のコマンドの終了コード: $?"
echo "現在のPID: $$"
echo "バックグラウンドの最後のPID: $!"
echo "IFS: '$IFS'"

# 実行例: ./special_vars.sh foo bar baz
# スクリプト名: ./special_vars.sh
# 第1引数: foo
# 第2引数: bar
# 全引数: foo bar baz
# 引数の数: 3
# 前のコマンドの終了コード: 0
# 現在のPID: 12345

変数のデフォルト値とパラメータ展開

#!/usr/bin/env bash

# ${var:-default}: varが未設定または空なら default を返す
echo "${UNDEFINED_VAR:-'default value'}"   # 出力: default value

# ${var:=default}: varが未設定または空なら default をセットして返す
echo "${MYVAR:='assigned default'}"         # 出力: assigned default
echo "${MYVAR}"                             # 出力: assigned default

# ${var:?error_msg}: varが未設定または空ならエラーメッセージを出して終了
# echo "${REQUIRED_VAR:?'REQUIRED_VAR must be set'}"

# ${var:+alt}: varがセットされていれば alt を返す(varは変更しない)
EXISTING="hello"
echo "${EXISTING:+'found'}"     # 出力: found
echo "${NONEXISTENT:+'found'}"  # 出力: (空)

# ${#var}: 変数の文字列長
str="Hello, World!"
echo "${#str}"  # 出力: 13

2.2 条件分岐

if 文の基本構造

#!/usr/bin/env bash

# 基本構造
if [[ condition ]]; then
    # 処理
elif [[ other_condition ]]; then
    # 別の処理
else
    # それ以外
fi

テスト演算子の比較

#!/usr/bin/env bash

# === 文字列比較 ===
str1="hello"
str2="world"

if [[ "${str1}" == "${str2}" ]]; then
    echo "等しい"
elif [[ "${str1}" != "${str2}" ]]; then
    echo "等しくない"  # ← これが実行される
fi

# パターンマッチ([[]] のみ)
filename="backup_2026-04-10.tar.gz"
if [[ "${filename}" == backup_*.tar.gz ]]; then
    echo "バックアップファイルです"  # ← これが実行される
fi

# 正規表現マッチ([[]] のみ)
ip="192.168.1.100"
if [[ "${ip}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
    echo "有効なIPv4アドレス形式"  # ← これが実行される
fi

# === 数値比較 ===
num=42

if (( num > 40 )); then
    echo "${num} は 40 より大きい"  # ← これが実行される
fi

# [[ ]] での数値比較演算子
# -eq: 等しい  -ne: 等しくない  -lt: 未満  -le: 以下  -gt: より大きい  -ge: 以上
if [[ ${num} -ge 40 && ${num} -le 50 ]]; then
    echo "${num} は 40〜50 の範囲"  # ← これが実行される
fi

# === ファイル/ディレクトリのテスト ===
file="/etc/passwd"
dir="/etc"

if [[ -f "${file}" ]]; then echo "通常ファイル"; fi
if [[ -d "${dir}" ]]; then echo "ディレクトリ"; fi
if [[ -r "${file}" ]]; then echo "読み取り可能"; fi
if [[ -w "${file}" ]]; then echo "書き込み可能"; fi
if [[ -x "${file}" ]]; then echo "実行可能"; fi
if [[ -e "${file}" ]]; then echo "存在する"; fi
if [[ -s "${file}" ]]; then echo "サイズが0より大きい"; fi
if [[ -L "/etc/mtab" ]]; then echo "シンボリックリンク"; fi
テスト演算子意味
-f通常ファイルが存在する[[ -f /etc/passwd ]]
-dディレクトリが存在する[[ -d /tmp ]]
-eファイル/ディレクトリが存在する[[ -e /var/log ]]
-r読み取り可能[[ -r config.txt ]]
-w書き込み可能[[ -w /tmp ]]
-x実行可能[[ -x /usr/bin/bash ]]
-sサイズが0より大きい[[ -s logfile.log ]]
-z文字列が空[[ -z "${var}" ]]
-n文字列が空でない[[ -n "${var}" ]]
-Lシンボリックリンク[[ -L /etc/localtime ]]
-ntより新しい[[ file1 -nt file2 ]]
-otより古い[[ file1 -ot file2 ]]

case 文

#!/usr/bin/env bash

# case文はパターンマッチングに優れている
action="${1:-help}"

case "${action}" in
    start|begin)
        echo "サービスを開始します"
        ;;
    stop|end|halt)
        echo "サービスを停止します"
        ;;
    restart|reload)
        echo "サービスを再起動します"
        ;;
    status)
        echo "ステータスを確認します"
        ;;
    [0-9]*)
        echo "数字で始まる引数: ${action}"
        ;;
    *.log)
        echo "ログファイルが指定されました: ${action}"
        ;;
    *)
        echo "使用法: $0 {start|stop|restart|status}"
        exit 1
        ;;
esac

2.3 ループ

for ループ

#!/usr/bin/env bash

# リストの反復
for fruit in apple banana cherry; do
    echo "フルーツ: ${fruit}"
done
# 出力:
# フルーツ: apple
# フルーツ: banana
# フルーツ: cherry

# C言語スタイルの for ループ
for ((i=0; i<5; i++)); do
    echo "i = ${i}"
done

# seq コマンドとの組み合わせ
for i in $(seq 1 5); do
    echo "番号: ${i}"
done

# ファイルのグロブ展開
for log_file in /var/log/*.log; do
    if [[ -s "${log_file}" ]]; then
        echo "非空ログファイル: ${log_file} ($(wc -l < "${log_file}") 行)"
    fi
done

# 配列の反復
servers=("web01" "web02" "db01" "cache01")
for server in "${servers[@]}"; do
    echo "サーバー確認: ${server}"
    # ping -c 1 "${server}" &>/dev/null && echo "  → 到達可能" || echo "  → 到達不可"
done

while・until ループ

#!/usr/bin/env bash

# while ループ
count=0
while [[ ${count} -lt 5 ]]; do
    echo "カウント: ${count}"
    ((count++))
done

# ファイルを1行ずつ読み込む(最も重要なパターン)
while IFS= read -r line; do
    echo "行: ${line}"
done < /etc/hosts

# コマンドの出力を1行ずつ処理
while IFS= read -r process; do
    pid=$(echo "${process}" | awk '{print $1}')
    name=$(echo "${process}" | awk '{print $2}')
    echo "PID: ${pid}, 名前: ${name}"
done < <(ps -eo pid,comm --no-headers | head -5)

# until ループ(条件が偽の間ループ)
retry=0
max_retry=3
until ping -c 1 -W 1 8.8.8.8 &>/dev/null; do
    ((retry++))
    if [[ ${retry} -ge ${max_retry} ]]; then
        echo "ネットワーク到達不可。${max_retry}回試行後に失敗。"
        exit 1
    fi
    echo "試行 ${retry}/${max_retry}... 再試行します"
    sleep 2
done
echo "ネットワーク到達可能"

# ループ制御: break と continue
for i in $(seq 1 10); do
    if (( i % 2 == 0 )); then
        continue  # 偶数をスキップ
    fi
    if (( i > 7 )); then
        break     # 7を超えたら終了
    fi
    echo "奇数: ${i}"
done
# 出力: 1, 3, 5, 7

2.4 関数

#!/usr/bin/env bash

# === 関数の定義 ===

# スタイル1: function キーワードあり
function greet() {
    local name="${1:-World}"  # ローカル変数(推奨)
    echo "Hello, ${name}!"
}

# スタイル2: function キーワードなし(POSIX互換)
log_message() {
    local level="${1}"
    local message="${2}"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[${timestamp}] [${level}] ${message}"
}

# 関数の呼び出し
greet "Alice"            # 出力: Hello, Alice!
greet                    # 出力: Hello, World!
log_message "INFO" "スクリプト開始"
# 出力: [2026-04-10 12:00:00] [INFO] スクリプト開始

# === 戻り値 ===

# return は終了コード(0-255)のみ返せる
is_root() {
    if [[ $(id -u) -eq 0 ]]; then
        return 0  # 成功(root)
    else
        return 1  # 失敗(非root)
    fi
}

if is_root; then
    echo "rootユーザーで実行中"
else
    echo "一般ユーザーで実行中"
fi

# 文字列を返す場合はechoを使う
get_os_version() {
    if [[ -f /etc/os-release ]]; then
        source /etc/os-release
        echo "${NAME} ${VERSION}"
    else
        echo "不明なOS"
    fi
}

os_info=$(get_os_version)
echo "OS情報: ${os_info}"

# === 高度な関数パターン ===

# 可変長引数
sum_numbers() {
    local total=0
    for num in "$@"; do
        (( total += num ))
    done
    echo "${total}"
}

result=$(sum_numbers 1 2 3 4 5)
echo "合計: ${result}"  # 出力: 合計: 15

# 名前付き引数(Bash 4.3+)
create_user() {
    local -A opts=()
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --username) opts[username]="$2"; shift 2 ;;
            --group)    opts[group]="$2";    shift 2 ;;
            --shell)    opts[shell]="$2";    shift 2 ;;
            *) echo "不明なオプション: $1" >&2; return 1 ;;
        esac
    done

    echo "ユーザー作成:"
    echo "  ユーザー名: ${opts[username]}"
    echo "  グループ: ${opts[group]:-users}"
    echo "  シェル: ${opts[shell]:-/bin/bash}"
}

create_user --username alice --group sysadmin --shell /bin/zsh

2.5 配列と連想配列

#!/usr/bin/env bash

# === 通常の配列(インデックス配列)===

# 宣言と初期化
declare -a fruits=("apple" "banana" "cherry" "date")

# 要素アクセス
echo "${fruits[0]}"    # apple
echo "${fruits[2]}"    # cherry
echo "${fruits[-1]}"   # date(最後の要素、Bash 4.3+)

# 全要素
echo "${fruits[@]}"    # apple banana cherry date
echo "${fruits[*]}"    # apple banana cherry date(IFSで区切られる)

# 要素数
echo "${#fruits[@]}"   # 4

# スライス(offset, length)
echo "${fruits[@]:1:2}"  # banana cherry(インデックス1から2個)

# 要素の追加
fruits+=("elderberry")
echo "${fruits[@]}"  # apple banana cherry date elderberry

# 要素の削除
unset fruits[1]  # banana を削除
echo "${fruits[@]}"  # apple cherry date elderberry
echo "${#fruits[@]}"  # 4 (インデックスは詰められない)

# 配列のキー(インデックス)一覧
echo "${!fruits[@]}"  # 0 2 3 4

# 配列全体のコピー
copy_array=("${fruits[@]}")

# === 連想配列(Bash 4.0+)===

declare -A server_config
server_config[hostname]="web01.example.com"
server_config[ip]="192.168.1.10"
server_config[port]="8080"
server_config[env]="production"

# キー・バリューの参照
echo "ホスト名: ${server_config[hostname]}"
echo "IPアドレス: ${server_config[ip]}"

# 全キー
echo "キー一覧: ${!server_config[@]}"

# 全バリュー
echo "値一覧: ${server_config[@]}"

# キーの存在確認
if [[ -v server_config[port] ]]; then
    echo "portが設定されています: ${server_config[port]}"
fi

# 連想配列の反復
for key in "${!server_config[@]}"; do
    echo "  ${key} = ${server_config[${key}]}"
done

# 実用例: ホスト名とIPのマッピング
declare -A hosts=(
    [web01]="10.0.0.1"
    [web02]="10.0.0.2"
    [db01]="10.0.1.1"
    [cache01]="10.0.2.1"
)

for hostname in "${!hosts[@]}"; do
    ip="${hosts[${hostname}]}"
    echo "${hostname} -> ${ip}"
done

2.6 文字列操作

#!/usr/bin/env bash

str="Hello, World! This is Bash."

# === 部分文字列抽出 ===
echo "${str:0:5}"      # Hello(位置0から5文字)
echo "${str:7}"        # World! This is Bash.(位置7以降)
echo "${str: -5}"      # Bash.(末尾から5文字)

# === 文字列長 ===
echo "${#str}"         # 28

# === パターン削除 ===
path="/usr/local/bin/bash"

# 前方一致・最短削除(%の反対)
echo "${path#/}"       # usr/local/bin/bash
echo "${path#*/}"      # local/bin/bash(最短マッチ)
echo "${path##*/}"     # bash(最長マッチ=basename相当)

# 後方一致削除(%)
echo "${path%/*}"      # /usr/local/bin(最短マッチ=dirname相当)
echo "${path%%/*}"     # (空 - 先頭の / まで削除)

# === 文字列置換 ===
text="apple banana apple cherry"
echo "${text/apple/APPLE}"       # APPLE banana apple cherry(最初の1つ)
echo "${text//apple/APPLE}"      # APPLE banana APPLE cherry(全て)
echo "${text/#apple/FRUIT}"      # FRUIT banana apple cherry(先頭のみ)
echo "${text/%cherry/FRUIT}"     # apple banana apple FRUIT(末尾のみ)

# === 大文字/小文字変換(Bash 4.0+)===
lower="hello world"
echo "${lower^}"   # Hello world(先頭1文字を大文字)
echo "${lower^^}"  # HELLO WORLD(全て大文字)

upper="HELLO WORLD"
echo "${upper,}"   # hELLO WORLD(先頭1文字を小文字)
echo "${upper,,}"  # hello world(全て小文字)

# === 実用的な文字列処理 ===

# ファイル名から拡張子を取得・除去
filename="backup_2026-04-10.tar.gz"
extension="${filename##*.}"           # gz
base_no_ext="${filename%.tar.gz}"     # backup_2026-04-10
echo "拡張子: ${extension}"
echo "ベース名: ${base_no_ext}"

# 文字列の分割
csv_line="alice,30,engineer,tokyo"
IFS=',' read -ra fields <<< "${csv_line}"
echo "名前: ${fields[0]}"     # alice
echo "年齢: ${fields[1]}"     # 30
echo "職種: ${fields[2]}"     # engineer
echo "場所: ${fields[3]}"     # tokyo

# 文字列に特定のパターンが含まれるか確認
haystack="The quick brown fox jumps over the lazy dog"
needle="fox"
if [[ "${haystack}" == *"${needle}"* ]]; then
    echo "\"${needle}\" が見つかりました"
fi

# 文字列のトリム(前後の空白除去)
trim() {
    local var="$*"
    var="${var#"${var%%[![:space:]]*}"}"  # 先頭の空白を除去
    var="${var%"${var##*[![:space:]]}"}"  # 末尾の空白を除去
    echo "${var}"
}

padded="   hello world   "
trimmed=$(trim "${padded}")
echo "'${trimmed}'"  # 出力: 'hello world'

3. デバッグ技法

3.1 set オプション

Bashには、スクリプトの挙動を制御する強力なsetオプションがあります。本番スクリプトでは必ずこれらを活用してください。

#!/usr/bin/env bash

# === 推奨される安全設定(スクリプト冒頭に記述)===
set -euo pipefail

# 個別の説明:
# set -e (errexit): コマンドが失敗(終了コード≠0)したら即座に終了
# set -u (nounset): 未定義変数を参照したらエラーで終了
# set -o pipefail: パイプラインのどこかが失敗したら失敗とみなす
# set -x (xtrace): 実行するコマンドを表示(デバッグ用)

各オプションの詳細解説

set -e (errexit) の動作

#!/usr/bin/env bash
set -e

echo "開始"
ls /nonexistent_directory   # ← ここで失敗
echo "この行は実行されない"  # ← 実行されない
開始
ls: cannot access '/nonexistent_directory': No such file or directory

set -e の落とし穴と回避策

#!/usr/bin/env bash
set -e

# 問題: if文の条件式は set -e の対象外
if grep -q "pattern" file.txt; then
    echo "見つかりました"
fi

# 問題: コマンドの後に || を使うと set -e を無効化できる
grep -q "pattern" file.txt || echo "見つかりませんでした"

# 意図的なエラーを無視する場合
rm -f /tmp/lockfile || true  # || true で常に成功とみなす

# サブシェルで一時的に無効化
{
    set +e  # 一時的に無効化
    risky_command
    set -e  # 再有効化
}

set -u (nounset) の動作

#!/usr/bin/env bash
set -u

echo "スクリプト開始"
echo "${UNDEFINED_VARIABLE}"  # ← ここでエラー
# bash: UNDEFINED_VARIABLE: unbound variable
# set -u との共存パターン
# デフォルト値を使って未定義変数エラーを回避
LOG_LEVEL="${LOG_LEVEL:-INFO}"
MAX_RETRY="${MAX_RETRY:-3}"
CONFIG_FILE="${CONFIG_FILE:-/etc/myapp/config.conf}"

echo "ログレベル: ${LOG_LEVEL}"
echo "最大リトライ: ${MAX_RETRY}"

set -o pipefail の動作

#!/usr/bin/env bash

# pipefail なし(デフォルト)
cat /nonexistent_file | grep "pattern"
echo "終了コード: $?"  # 出力: 終了コード: 1 (grep の終了コード)

# pipefail あり
set -o pipefail
cat /nonexistent_file | grep "pattern"
echo "終了コード: $?"  # 出力: 終了コード: 1 (cat の失敗が反映される)

# 実用的な例: ログをフィルタリングしてファイルに書き込む
set -euo pipefail
grep "ERROR" /var/log/app.log | sort | uniq -c > /tmp/error_summary.txt
# パイプの途中で失敗してもスクリプト全体が停止する

set -x (xtrace) デバッグモード

#!/usr/bin/env bash
set -x  # デバッグモード開始

name="Alice"
age=30
echo "Name: ${name}, Age: ${age}"

set +x  # デバッグモード終了
echo "この行はトレースされない"
+ name=Alice
+ age=30
+ echo 'Name: Alice, Age: 30'
Name: Alice, Age: 30

PS4 変数でトレース出力をカスタマイズ

#!/usr/bin/env bash

# デフォルトは "+ "
# より詳細な情報を表示するカスタムPS4
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x

function do_something() {
    local val="test"
    echo "${val}"
}

do_something
+(debug_script.sh:8): do_something(): local val=test
+(debug_script.sh:9): do_something(): echo test
test

set オプションまとめ表

オプション長形式効果本番推奨
set -eset -o errexitコマンド失敗で即終了
set -uset -o nounset未定義変数をエラー扱い
set -xset -o xtrace実行コマンドのトレース出力デバッグ時のみ
set -o pipefailパイプライン全体の失敗を検出
set -nset -o noexecコマンドを実行せず構文チェックのみ開発時
set -fset -o noglobグロブ展開を無効化用途に応じて
set -vset -o verbose読み込んだ行を表示デバッグ時のみ

3.2 デバッグツール

shellcheck によるスタティック解析

# インストール
sudo apt-get install shellcheck  # Debian/Ubuntu
sudo yum install shellcheck      # RHEL/CentOS
brew install shellcheck          # macOS

# 使用方法
shellcheck script.sh

# 特定の警告を無効化
# shellcheck disable=SC2086
echo $unquoted_var
In script.sh line 5:
echo $unquoted_var
     ^-----------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean:
echo "$unquoted_var"

bash デバッグセッション

#!/usr/bin/env bash

# デバッグ関数
debug() {
    if [[ "${DEBUG:-0}" == "1" ]]; then
        echo "[DEBUG] $*" >&2
    fi
}

# 使用方法
debug "変数の値: ${my_var}"

# 実行時に有効化
# DEBUG=1 ./script.sh

trap を使ったデバッグ

#!/usr/bin/env bash

# ERR トラップでエラー情報を出力
trap 'echo "エラー: ${BASH_SOURCE[0]}:${LINENO}: コマンド \"${BASH_COMMAND}\" が失敗しました (終了コード: $?)"' ERR

# DEBUG トラップで全コマンドを追跡
# trap 'echo "実行: ${BASH_COMMAND}"' DEBUG

function_call_stack() {
    local i
    echo "コールスタック:"
    for ((i=${#FUNCNAME[@]}-1; i>=0; i--)); do
        echo "  ${FUNCNAME[i]}() @ ${BASH_SOURCE[i]}:${BASH_LINENO[i]}"
    done
}

my_function() {
    function_call_stack
}

my_function

4. エラーハンドリング

基本的なエラーハンドリングパターン

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

# === ログ関数の定義 ===
readonly LOG_FILE="/var/log/myscript.log"
readonly SCRIPT_NAME=$(basename "$0")

log() {
    local level="$1"
    shift
    local message="$*"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[${timestamp}] [${level}] ${SCRIPT_NAME}: ${message}" | tee -a "${LOG_FILE}"
}

log_info()  { log "INFO"  "$@"; }
log_warn()  { log "WARN"  "$@" >&2; }
log_error() { log "ERROR" "$@" >&2; }

# === エラーハンドリング関数 ===
die() {
    log_error "$@"
    exit 1
}

# === 終了ハンドラー ===
cleanup() {
    local exit_code=$?
    log_info "クリーンアップ処理を実行中..."
    
    # 一時ファイルの削除
    if [[ -n "${TEMP_FILE:-}" && -f "${TEMP_FILE}" ]]; then
        rm -f "${TEMP_FILE}"
        log_info "一時ファイルを削除: ${TEMP_FILE}"
    fi
    
    # ロックファイルの削除
    if [[ -n "${LOCK_FILE:-}" && -f "${LOCK_FILE}" ]]; then
        rm -f "${LOCK_FILE}"
        log_info "ロックファイルを削除: ${LOCK_FILE}"
    fi
    
    if [[ ${exit_code} -eq 0 ]]; then
        log_info "スクリプト正常終了"
    else
        log_error "スクリプト異常終了(終了コード: ${exit_code})"
    fi
}
trap cleanup EXIT

# === リトライ機能 ===
retry() {
    local max_attempts="${1}"
    local delay="${2}"
    shift 2
    local cmd="$@"
    local attempt=1

    while (( attempt <= max_attempts )); do
        log_info "試行 ${attempt}/${max_attempts}: ${cmd}"
        if eval "${cmd}"; then
            return 0
        fi
        if (( attempt < max_attempts )); then
            log_warn "失敗。${delay}秒後に再試行..."
            sleep "${delay}"
        fi
        (( attempt++ ))
    done

    log_error "${max_attempts}回の試行後も失敗: ${cmd}"
    return 1
}

# 使用例
# retry 3 5 curl -f https://api.example.com/health

# === 排他制御(ロックファイル)===
LOCK_FILE="/var/run/${SCRIPT_NAME}.lock"

acquire_lock() {
    if [[ -f "${LOCK_FILE}" ]]; then
        local old_pid
        old_pid=$(cat "${LOCK_FILE}" 2>/dev/null || echo "")
        if [[ -n "${old_pid}" ]] && kill -0 "${old_pid}" 2>/dev/null; then
            die "別のインスタンスが実行中です(PID: ${old_pid})"
        else
            log_warn "古いロックファイルを削除します"
            rm -f "${LOCK_FILE}"
        fi
    fi
    echo $$ > "${LOCK_FILE}"
    log_info "ロックを取得しました(PID: $$)"
}

# === コマンドの存在確認 ===
require_commands() {
    local missing=()
    for cmd in "$@"; do
        if ! command -v "${cmd}" &>/dev/null; then
            missing+=("${cmd}")
        fi
    done
    if [[ ${#missing[@]} -gt 0 ]]; then
        die "必要なコマンドが見つかりません: ${missing[*]}"
    fi
}

require_commands curl jq rsync

# === root権限の確認 ===
require_root() {
    if [[ $(id -u) -ne 0 ]]; then
        die "このスクリプトはrootとして実行する必要があります"
    fi
}

# === メイン処理 ===
main() {
    log_info "スクリプト開始"
    acquire_lock
    
    # 一時ファイルの作成(trapで自動クリーンアップ)
    TEMP_FILE=$(mktemp /tmp/myscript.XXXXXX)
    log_info "一時ファイル作成: ${TEMP_FILE}"
    
    # メインロジック
    log_info "処理を実行中..."
    
    log_info "スクリプト完了"
}

main "$@"

エラーコードの設計

#!/usr/bin/env bash

# 終了コードの定数定義(慣習的な値)
readonly E_SUCCESS=0       # 成功
readonly E_GENERAL=1       # 一般エラー
readonly E_MISUSE=2        # シェルコマンドの誤使用
readonly E_PERMISSION=77   # 権限エラー
readonly E_NOT_FOUND=127   # コマンドが見つからない
readonly E_TIMEOUT=124     # タイムアウト

# カスタムエラーコード(64-113 を推奨)
readonly E_CONFIG_MISSING=64    # 設定ファイルが見つからない
readonly E_INVALID_ARGS=65      # 引数が無効
readonly E_NETWORK_ERROR=66     # ネットワークエラー
readonly E_DISK_FULL=67         # ディスク容量不足

check_disk_space() {
    local required_mb="${1}"
    local path="${2:-/}"
    local available_mb
    available_mb=$(df -m "${path}" | awk 'NR==2 {print $4}')
    
    if (( available_mb < required_mb )); then
        log_error "ディスク容量不足: ${available_mb}MB 利用可能、${required_mb}MB 必要"
        return ${E_DISK_FULL}
    fi
    
    log_info "ディスク容量OK: ${available_mb}MB 利用可能"
    return ${E_SUCCESS}
}

5. シグナルハンドリング(trap)

シグナルとtrapの基礎

#!/usr/bin/env bash

# === 主要なシグナル一覧 ===
# SIGHUP  (1):  ターミナルが切断された(デーモンの設定再読み込みにも使われる)
# SIGINT  (2):  Ctrl+C(割り込み)
# SIGQUIT (3):  Ctrl+\(コアダンプ付き終了)
# SIGTERM (15): 通常の終了要求(killコマンドのデフォルト)
# SIGKILL (9):  強制終了(トラップ不可)
# SIGUSR1 (10): ユーザー定義シグナル1
# SIGUSR2 (12): ユーザー定義シグナル2
# SIGPIPE (13): パイプの読み取り側が終了
# EXIT    : シェル終了時(擬似シグナル)
# ERR     : コマンドが失敗時(擬似シグナル)
# DEBUG   : コマンド実行前(擬似シグナル)
# RETURN  : 関数/sourceからのリターン時(擬似シグナル)

# シグナルの送受信を確認
kill -l  # 全シグナル一覧

実用的なtrapパターン

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

# === グレースフルシャットダウン ===

# グローバルフラグ
SHUTDOWN_REQUESTED=0
TEMP_FILES=()
CHILD_PIDS=()

cleanup() {
    local exit_code=$?
    echo ""
    echo "クリーンアップを実行中..."
    
    # 子プロセスの終了
    for pid in "${CHILD_PIDS[@]}"; do
        if kill -0 "${pid}" 2>/dev/null; then
            echo "子プロセス ${pid} を終了中..."
            kill -TERM "${pid}" 2>/dev/null || true
            # TERMを無視する場合はKILLで強制終了
            sleep 1
            kill -KILL "${pid}" 2>/dev/null || true
        fi
    done
    
    # 一時ファイルの削除
    for file in "${TEMP_FILES[@]}"; do
        if [[ -f "${file}" ]]; then
            rm -f "${file}"
            echo "削除: ${file}"
        fi
    done
    
    echo "クリーンアップ完了(終了コード: ${exit_code})"
    exit "${exit_code}"
}

handle_sigterm() {
    echo "SIGTERMを受信しました。終了します..."
    SHUTDOWN_REQUESTED=1
}

handle_sighup() {
    echo "SIGHUPを受信しました。設定を再読み込みします..."
    reload_config
}

handle_sigusr1() {
    echo "SIGUSR1を受信しました。統計を出力します..."
    print_stats
}

# trapの登録
trap cleanup EXIT
trap handle_sigterm SIGTERM SIGINT
trap handle_sighup SIGHUP
trap handle_sigusr1 SIGUSR1

# === デーモンスタイルのメインループ ===
main_loop() {
    echo "サービス開始 (PID: $$)"
    
    local iteration=0
    while [[ ${SHUTDOWN_REQUESTED} -eq 0 ]]; do
        ((iteration++))
        echo "処理中... (反復: ${iteration})"
        
        # バックグラウンドで処理
        sleep 10 &
        CHILD_PIDS+=($!)
        wait $! || true
        
        # SHUTDOWNフラグのチェック
        if [[ ${SHUTDOWN_REQUESTED} -eq 1 ]]; then
            echo "シャットダウン要求を検出"
            break
        fi
    done
    
    echo "メインループ終了"
}

reload_config() {
    echo "設定ファイルを再読み込み中..."
    # source /etc/myapp/config.conf
}

print_stats() {
    echo "=== 現在の統計 ==="
    echo "PID: $$"
    echo "実行時間: $(ps -o etime= -p $$)"
    echo "メモリ使用量: $(ps -o rss= -p $$) KB"
}

# 一時ファイルの作成(trapで自動クリーンアップ)
TEMP_FILE=$(mktemp)
TEMP_FILES+=("${TEMP_FILE}")

main_loop

タイムアウト制御

#!/usr/bin/env bash

# timeout コマンドを使ったタイムアウト
timeout 30 long_running_command || {
    if [[ $? -eq 124 ]]; then
        echo "タイムアウト: 30秒で処理が完了しませんでした"
    fi
}

# bash組み込みのタイムアウト実装
run_with_timeout() {
    local timeout="${1}"
    shift
    local cmd="$@"

    # バックグラウンドで実行
    eval "${cmd}" &
    local pid=$!

    # タイムアウト監視
    (
        sleep "${timeout}"
        if kill -0 "${pid}" 2>/dev/null; then
            echo "タイムアウト: ${cmd}" >&2
            kill -TERM "${pid}" 2>/dev/null
        fi
    ) &
    local watcher=$!

    # コマンドの完了を待つ
    wait "${pid}"
    local exit_code=$?
    
    # 監視プロセスを終了
    kill "${watcher}" 2>/dev/null || true
    wait "${watcher}" 2>/dev/null || true

    return ${exit_code}
}

# 使用例
run_with_timeout 10 "sleep 5 && echo '処理完了'"

6. プロセス管理

基本的なプロセス管理

#!/usr/bin/env bash

# === プロセスの確認 ===

# プロセス一覧
ps aux | head -5
# USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
# root         1  0.0  0.3 225708  7620 ?        Ss   Apr09   0:02 /sbin/init

# 特定プロセスの検索
pgrep -l nginx          # プロセス名でPIDを検索
pidof nginx             # nginxのPIDを取得
pgrep -u apache nginx   # apacheユーザーのnginxプロセス

# プロセスツリー表示
pstree -p $$            # 現在のプロセスツリー
pstree -p 1             # init/systemdから全プロセスツリー

# === プロセスの制御 ===

# シグナル送信
kill -TERM 1234         # 通常終了
kill -HUP 1234          # 再起動/設定再読み込み
kill -KILL 1234         # 強制終了
pkill -f "python app.py"  # コマンドラインでマッチするプロセスを終了
killall nginx           # 名前でプロセスを終了

# === バックグラウンド処理 ===

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

# 完了を待つ
wait ${bg_pid}
echo "バックグラウンドタスク完了(終了コード: $?)"

# 並列処理パターン
parallel_process() {
    local pids=()
    local servers=("server1" "server2" "server3" "server4")
    
    for server in "${servers[@]}"; do
        (
            echo "サーバー ${server} を処理中..."
            sleep $((RANDOM % 3 + 1))  # 処理をシミュレート
            echo "サーバー ${server} 完了"
        ) &
        pids+=($!)
    done
    
    # 全プロセスの完了を待つ
    local failed=0
    for pid in "${pids[@]}"; do
        wait "${pid}" || ((failed++))
    done
    
    if [[ ${failed} -gt 0 ]]; then
        echo "${failed} 個のプロセスが失敗しました"
        return 1
    fi
    echo "全プロセス完了"
}

parallel_process

プロセスのリソース管理

#!/usr/bin/env bash

# === CPU・メモリ使用量の監視 ===

get_process_info() {
    local pid="${1}"
    if [[ -d "/proc/${pid}" ]]; then
        local cpu mem vsz rss
        read -r _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ cpu _ <<< "$(cat /proc/${pid}/stat 2>/dev/null)"
        mem=$(cat /proc/${pid}/status 2>/dev/null | grep VmRSS | awk '{print $2}')
        echo "PID ${pid}: CPU=${cpu}, メモリ=${mem}KB"
    fi
}

# 上位プロセスのモニタリング
monitor_top_processes() {
    echo "=== CPU使用量上位5プロセス ==="
    ps aux --sort=-%cpu | head -6

    echo ""
    echo "=== メモリ使用量上位5プロセス ==="
    ps aux --sort=-%mem | head -6
}

# nice/renice でプライオリティ制御
# 低優先度でバックアップを実行
nice -n 19 rsync -a /source /destination &

# 実行中のプロセスの優先度を変更
# renice +10 -p 1234

# ulimit でリソース制限
# 最大ファイルオープン数
ulimit -n 65536
# 最大プロセス数
ulimit -u 1024
# コアダンプのサイズ(0=無効)
ulimit -c 0

echo "現在のリソース制限:"
ulimit -a

ジョブ制御

#!/usr/bin/env bash

# ジョブ管理
echo "ジョブ管理デモ"

# バックグラウンドジョブの起動
sleep 100 &
job1_pid=$!
sleep 200 &
job2_pid=$!

# ジョブ一覧
jobs -l

# フォアグラウンドに戻す
# fg %1

# バックグラウンドに送る(Ctrl+Z後)
# bg %1

# ジョブの待機
wait ${job1_pid}
echo "ジョブ1完了"

# 全ジョブの待機
wait
echo "全ジョブ完了"

# nohup: ターミナル切断後も実行継続
nohup long_running_script.sh > /var/log/script.log 2>&1 &
echo "バックグラウンドで実行開始 (PID: $!)"

7. 正規表現とテキスト処理

7.1 grep

grepはテキスト検索の基本ツールです。システム管理では特にログ解析で多用します。

# === 基本的な使い方 ===

# 基本検索
grep "error" /var/log/syslog

# 大文字・小文字を区別しない
grep -i "error" /var/log/syslog

# 行番号を表示
grep -n "ERROR" /var/log/app.log

# マッチしない行を表示(反転)
grep -v "DEBUG" /var/log/app.log

# 再帰的に検索
grep -r "TODO" /etc/

# ファイル名のみ表示
grep -l "error" /var/log/*.log

# マッチした行の前後を表示
grep -A 3 "ERROR"  /var/log/app.log   # マッチ後3行
grep -B 2 "ERROR"  /var/log/app.log   # マッチ前2行
grep -C 5 "ERROR"  /var/log/app.log   # 前後5行

# === 正規表現 ===

# 基本正規表現 (BRE)
grep "^2026" /var/log/messages         # 2026で始まる行
grep "error$" /var/log/app.log         # errorで終わる行
grep "err[ao]r" /var/log/app.log       # error または errar

# 拡張正規表現 (ERE) - grep -E または egrep
grep -E "ERROR|WARN|CRIT" /var/log/app.log   # OR条件
grep -E "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}" /var/log/access.log  # IPアドレス
grep -E "^(Jan|Feb|Mar)" /var/log/messages   # 1〜3月

# Perl互換正規表現 (PCRE) - grep -P
grep -P "\b\d{4}-\d{2}-\d{2}\b" logfile.txt   # 日付パターン
grep -P "(?<=\[ERROR\]).*" /var/log/app.log     # ルックアヘッド

# === 実用的な使用例 ===

# SSHログイン失敗の検出
grep "Failed password" /var/log/auth.log | \
    grep -oP '(?<=from )[0-9.]+' | \
    sort | uniq -c | sort -rn | head -10

# HTTPエラーコードの統計
grep -E '" [45][0-9]{2} ' /var/log/nginx/access.log | \
    grep -oP '" \K[45][0-9]{2}' | \
    sort | uniq -c | sort -rn

# 特定のIPアドレスのアクセスログ
grep "^192\.168\.1\." /var/log/nginx/access.log

# 特定の時間範囲のログ抽出
grep "2026-04-10 1[0-2]:" /var/log/app.log  # 10:00〜12:59

# バイナリファイルでのテキスト検索
grep -a "error" /proc/1/maps 2>/dev/null || true
オプション説明使用例
-i大文字小文字を区別しないgrep -i error log.txt
-vマッチしない行を表示grep -v debug log.txt
-rディレクトリを再帰的に検索grep -r TODO /src/
-n行番号を表示grep -n error log.txt
-lマッチしたファイル名のみgrep -l error *.log
-cマッチした行数を表示grep -c error log.txt
-E拡張正規表現を使用grep -E "err|warn" log.txt
-PPerl互換正規表現grep -P "\d{4}" log.txt
-oマッチ部分のみを表示grep -oP '\d+\.\d+\.\d+\.\d+' log.txt
-A nマッチ後n行を表示grep -A 3 ERROR log.txt
-B nマッチ前n行を表示grep -B 2 ERROR log.txt
-C n前後n行を表示grep -C 5 ERROR log.txt
--colorマッチ部分を着色grep --color=always error log.txt

7.2 sed

sedはストリームエディタで、テキストの変換・置換・削除に使います。

# === 基本的な置換 ===

# 最初のマッチを置換
sed 's/old/new/' file.txt

# 全マッチを置換
sed 's/old/new/g' file.txt

# 大文字小文字を区別しない置換
sed 's/old/new/gi' file.txt

# ファイルを直接編集(in-place)
sed -i 's/old/new/g' file.txt

# バックアップを作成してから直接編集
sed -i.bak 's/old/new/g' file.txt
# → file.txt.bak にバックアップ

# === 行の操作 ===

# 特定の行を削除
sed '5d' file.txt              # 5行目を削除
sed '5,10d' file.txt           # 5〜10行目を削除
sed '/pattern/d' file.txt      # パターンにマッチした行を削除
sed '/^$/d' file.txt           # 空行を削除
sed '/^#/d' file.txt           # コメント行(#始まり)を削除

# 特定の行だけを表示(-n と p を組み合わせ)
sed -n '10,20p' file.txt       # 10〜20行目を表示
sed -n '/start/,/end/p' file.txt  # start〜endの間を表示

# 行の追加・挿入
sed '5a\追加する行' file.txt    # 5行目の後に追加
sed '5i\挿入する行' file.txt    # 5行目の前に挿入
sed '$ a\最終行の後' file.txt   # 最終行の後に追加

# === 実用的な設定ファイル編集 ===

# NginxのHTTPをHTTPSにリダイレクト
sed -i 's|listen 80;|listen 443 ssl;|g' /etc/nginx/nginx.conf

# SSHdの設定変更(PasswordAuthenticationを無効化)
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config

# PHP-FPMのmax_children設定変更
sed -i 's/^pm\.max_children.*/pm.max_children = 50/' /etc/php/7.4/fpm/pool.d/www.conf

# コメントアウトされた設定を有効化
sed -i 's/^#\s*\(MaxConnections.*\)/\1/' /etc/some.conf

# 設定値の動的変更(変数を使う場合はダブルクォート)
DB_HOST="db.example.com"
sed -i "s/^DB_HOST=.*/DB_HOST=${DB_HOST}/" /etc/app/.env

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

# sedスクリプトファイルを使用
cat > /tmp/transform.sed << 'EOF'
s/^[[:space:]]*//      # 先頭の空白を削除
s/[[:space:]]*$//      # 末尾の空白を削除
/^#/d                  # コメント行を削除
/^$/d                  # 空行を削除
EOF
sed -f /tmp/transform.sed config.txt

# === ログの前処理 ===

# タイムスタンプ部分を削除
sed 's/^[0-9-]* [0-9:.]* //' app.log

# IPアドレスをマスキング
sed -E 's/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[MASKED]/g' access.log

# CSVの特定の列を削除
sed 's/,[^,]*,/,/' data.csv   # 2列目を削除(最初にマッチ)

7.3 awk

awkは強力なテキスト処理言語で、フィールド処理・集計・レポート生成に最適です。

# === awk の基本構造 ===
# awk 'BEGIN{...} /pattern/{action} END{...}' file

# 特定のフィールドを抽出
awk '{print $1}' file.txt          # 1列目
awk '{print $NF}' file.txt         # 最後の列
awk '{print $1, $3}' file.txt      # 1列目と3列目
awk -F: '{print $1}' /etc/passwd   # デリミタをコロンに指定

# === フィールドの活用 ===

# ディスク使用量の確認(df コマンドの出力を整形)
df -h | awk 'NR==1 || $5+0 > 80 {printf "%-20s %5s %5s %5s %5s\n", $6, $2, $3, $4, $5}'
# 出力例:
# Mounted on           Size  Used Avail Use%
# /                    100G   85G   15G  85%

# /etc/passwd からユーザー情報を取得
awk -F: '$3 >= 1000 {printf "%-20s UID=%-6s Shell=%s\n", $1, $3, $7}' /etc/passwd
# 出力例:
# alice                UID=1000   Shell=/bin/bash
# bob                  UID=1001   Shell=/bin/zsh

# ネットワーク接続の統計
ss -tn | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# 出力例:
#      15 ESTAB
#       3 TIME-WAIT

# === 条件とパターンマッチング ===

# 特定のフィールドが条件を満たす行だけを処理
awk '$3 > 1000' /etc/passwd           # UID > 1000
awk '$5 ~ /bash$/' /etc/passwd        # シェルがbashで終わる
awk 'NR>=10 && NR<=20' file.txt       # 10〜20行目
awk '/ERROR/' /var/log/app.log        # ERRORを含む行
awk '!/DEBUG/' /var/log/app.log       # DEBUGを含まない行

# 範囲パターン
awk '/START/,/END/' file.txt          # START〜ENDの間

# === 集計・統計 ===

# アクセスログのURL別カウント
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

# レスポンスサイズの合計と平均
awk '{sum += $10; count++} END {
    printf "合計: %.2f MB\n平均: %.2f KB\n", sum/1024/1024, sum/count/1024
}' /var/log/nginx/access.log

# エラー率の計算
awk '
{
    total++
    if ($9 ~ /^[45]/) errors++
}
END {
    if (total > 0)
        printf "エラー率: %.2f%% (%d/%d)\n", errors/total*100, errors, total
}' /var/log/nginx/access.log

# 時間帯別アクセス数
awk '{
    match($4, /\[(..)\/(..)\/(....):(..):/, arr)
    hour = arr[4]
    hourly[hour]++
}
END {
    for (h in hourly)
        printf "%s時: %d回\n", h, hourly[h]
}' /var/log/nginx/access.log | sort

# === 複数ファイルの処理 ===

# 複数のCSVファイルを結合して集計
awk -F',' '
FNR==1 && NR!=1 {next}   # 最初のファイル以外のヘッダーをスキップ
{print}
' data_*.csv

# === AWKプログラムのファイル化 ===

cat > /tmp/analyze_log.awk << 'EOF'
BEGIN {
    print "=== ログ分析レポート ==="
    print "日時\t\t\t情報\t件数"
    total = 0
    errors = 0
    warnings = 0
}

/ERROR/ {
    errors++
    total++
}

/WARN/ {
    warnings++
    total++
}

!/ERROR|WARN/ {
    total++
}

END {
    printf "\n合計ログ行数: %d\n", total
    printf "エラー数: %d (%.1f%%)\n", errors, (total>0 ? errors/total*100 : 0)
    printf "警告数: %d (%.1f%%)\n", warnings, (total>0 ? warnings/total*100 : 0)
}
EOF

awk -f /tmp/analyze_log.awk /var/log/app.log

grep・sed・awk の使い分け

ツール得意な用途典型的な使用シーン
grepパターン検索・フィルタリングログからエラー行を抽出、ファイル検索
sed行単位の変換・置換・削除設定ファイルの変更、テキスト変換
awkフィールド処理・集計・レポートログ分析、CSVデータ処理、統計出力

8. ファイル処理

ファイルの検索・操作

#!/usr/bin/env bash

# === find コマンドの高度な使用 ===

# 名前パターンで検索
find /var/log -name "*.log" -type f

# 変更時刻で検索
find /tmp -mtime +7 -type f              # 7日以上前に変更
find /var/log -mmin -60 -type f          # 60分以内に変更
find /home -newer /etc/passwd -type f    # /etc/passwd より新しい

# サイズで検索
find /var -size +100M -type f            # 100MB以上
find /tmp -size 0 -type f               # 空ファイル

# 権限で検索
find / -perm -4000 -type f 2>/dev/null  # SUIDビット付きファイル
find / -perm -2000 -type f 2>/dev/null  # SGIDビット付きファイル
find /home -perm 777 -type f            # 全員書き込み可能

# オーナーで検索
find / -user nobody -type f 2>/dev/null
find /home -group wheel -type f

# 複合条件
find /var/log -name "*.log" -type f -size +10M -mtime +30

# 検索結果にアクションを適用
find /tmp -name "*.tmp" -type f -delete           # 削除
find /var/log -name "*.log" -type f -exec ls -lh {} \;  # ls -lh を適用
find /var/log -name "*.gz" -type f -exec rm {} +  # 一括削除(効率的)

# xargs と組み合わせ
find /home -name ".bash_history" -print0 | xargs -0 wc -l

# === 大容量ファイルの処理 ===

# ファイルを分割処理(メモリ節約)
process_large_file() {
    local file="${1}"
    local chunk_size=10000  # 行数

    # 全行数を取得
    local total_lines
    total_lines=$(wc -l < "${file}")
    echo "合計 ${total_lines} 行を処理します"

    local processed=0
    while IFS= read -r line; do
        # 各行を処理
        process_line "${line}"
        ((processed++))
        if (( processed % chunk_size == 0 )); then
            echo "進捗: ${processed}/${total_lines} 行 ($(( processed * 100 / total_lines ))%)"
        fi
    done < "${file}"
    echo "処理完了: ${processed} 行"
}

process_line() {
    local line="${1}"
    # 実際の処理をここに記述
    echo "${line}" >> /tmp/processed_output.txt
}

# === ファイルのロック ===

# flock を使った排他的ファイルアクセス
(
    flock -n 200 || { echo "別のプロセスが実行中"; exit 1; }
    
    # クリティカルセクション
    echo "排他的アクセス中..."
    sleep 5
    echo "処理完了"
    
) 200>/var/lock/myapp.lock

# === テンポラリファイルの安全な扱い ===

# mktemp で一意なテンポラリファイル作成
TEMP_FILE=$(mktemp /tmp/script.XXXXXX)
TEMP_DIR=$(mktemp -d /tmp/workdir.XXXXXX)

# trapで確実にクリーンアップ
trap "rm -rf '${TEMP_FILE}' '${TEMP_DIR}'" EXIT

echo "一時ファイル: ${TEMP_FILE}"
echo "一時ディレクトリ: ${TEMP_DIR}"

# 処理
echo "some data" > "${TEMP_FILE}"
cp "${TEMP_FILE}" "${TEMP_DIR}/"

テキストファイルの処理パターン

#!/usr/bin/env bash

# === CSVファイルの処理 ===

# ヘッダー行を取得
header=$(head -1 data.csv)
echo "列名: ${header}"

# ヘッダーをスキップして処理
tail -n +2 data.csv | while IFS=',' read -r name age city; do
    echo "名前: ${name}, 年齢: ${age}, 都市: ${city}"
done

# awk でCSVを確実に処理
awk -F',' 'NR>1 {
    gsub(/^"|"$/, "", $1)  # ダブルクォートを除去
    printf "名前: %-20s 年齢: %3s 都市: %s\n", $1, $2, $3
}' data.csv

# === 設定ファイルのパース ===

# key=value 形式の設定ファイルを読み込む
parse_config() {
    local config_file="${1}"
    declare -gA CONFIG

    while IFS='=' read -r key value; do
        # コメントと空行をスキップ
        [[ "${key}" =~ ^[[:space:]]*# ]] && continue
        [[ -z "${key}" ]] && continue

        # 前後の空白を除去
        key="${key// /}"
        value="${value#"${value%%[![:space:]]*}"}"
        value="${value%"${value##*[![:space:]]}"}"

        CONFIG["${key}"]="${value}"
    done < "${config_file}"
}

# 使用例
cat > /tmp/test.conf << 'EOF'
# データベース設定
DB_HOST = localhost
DB_PORT = 5432
DB_NAME = myapp
DB_USER = appuser
EOF

parse_config /tmp/test.conf
echo "ホスト: ${CONFIG[DB_HOST]}"
echo "ポート: ${CONFIG[DB_PORT]}"

# === JSONの処理(jq が利用可能な場合)===

# jqを使ったJSON処理
if command -v jq &>/dev/null; then
    # JSONからフィールドを抽出
    echo '{"name": "Alice", "age": 30, "city": "Tokyo"}' | jq '.name'
    # 出力: "Alice"
    
    # 配列の処理
    echo '[{"name":"Alice","score":95},{"name":"Bob","score":87}]' | \
        jq '.[] | select(.score > 90) | .name'
    # 出力: "Alice"
    
    # APIレスポンスの解析
    curl -s https://api.example.com/users | \
        jq -r '.users[] | "\(.name)\t\(.email)"'
fi

# === ログローテーション処理 ===

rotate_log() {
    local log_file="${1}"
    local max_size_mb="${2:-100}"
    local max_files="${3:-5}"

    if [[ ! -f "${log_file}" ]]; then
        return 0
    fi

    local size_mb
    size_mb=$(du -m "${log_file}" | cut -f1)

    if (( size_mb < max_size_mb )); then
        return 0
    fi

    echo "ログローテーション: ${log_file} (${size_mb}MB)"

    # 古いローテーションファイルを整理
    for ((i=max_files-1; i>=1; i--)); do
        if [[ -f "${log_file}.${i}" ]]; then
            mv "${log_file}.${i}" "${log_file}.$((i+1))"
        fi
    done

    # 古すぎるファイルを削除
    if [[ -f "${log_file}.${max_files}" ]]; then
        rm -f "${log_file}.${max_files}"
    fi

    # 現在のログをローテーション
    mv "${log_file}" "${log_file}.1"
    
    # 新しいログファイルを作成
    touch "${log_file}"
    
    # アプリにシグナルを送ってログファイルを再オープンさせる(必要に応じて)
    # kill -USR1 $(cat /var/run/app.pid)
}

9. システム管理スクリプト実例

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

#!/usr/bin/env bash
# backup.sh - 包括的なバックアップスクリプト
# 使用法: backup.sh [--full|--incremental] [--destination DIR]

set -euo pipefail

# === 設定 ===
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
readonly LOG_FILE="/var/log/backup/backup_$(date +%Y%m%d).log"
readonly LOCK_FILE="/var/run/backup.lock"

# デフォルト設定
BACKUP_TYPE="${BACKUP_TYPE:-incremental}"
BACKUP_DEST="${BACKUP_DEST:-/backup}"
BACKUP_SRC="${BACKUP_SRC:-/var/www /etc /home}"
RETENTION_DAYS="${RETENTION_DAYS:-30}"
COMPRESS="${COMPRESS:-yes}"
NOTIFY_EMAIL="${NOTIFY_EMAIL:-}"

# === ログ関数 ===
setup_logging() {
    mkdir -p "$(dirname "${LOG_FILE}")"
    exec 1> >(tee -a "${LOG_FILE}")
    exec 2> >(tee -a "${LOG_FILE}" >&2)
}

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
log_info()  { log "[INFO]  $*"; }
log_warn()  { log "[WARN]  $*" >&2; }
log_error() { log "[ERROR] $*" >&2; }

# === 引数解析 ===
parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --full)          BACKUP_TYPE="full";         shift ;;
            --incremental)   BACKUP_TYPE="incremental";  shift ;;
            --destination)   BACKUP_DEST="$2";           shift 2 ;;
            --source)        BACKUP_SRC="$2";            shift 2 ;;
            --retention)     RETENTION_DAYS="$2";        shift 2 ;;
            --no-compress)   COMPRESS="no";              shift ;;
            --email)         NOTIFY_EMAIL="$2";          shift 2 ;;
            -h|--help)       usage; exit 0 ;;
            *) log_error "不明なオプション: $1"; usage; exit 1 ;;
        esac
    done
}

usage() {
    cat << EOF
使用法: ${SCRIPT_NAME} [オプション]

オプション:
  --full              フルバックアップ
  --incremental       増分バックアップ(デフォルト)
  --destination DIR   バックアップ先ディレクトリ(デフォルト: /backup)
  --source DIR        バックアップ元(スペース区切りで複数指定可)
  --retention DAYS    保持期間(日数、デフォルト: 30)
  --no-compress       圧縮を無効化
  --email ADDRESS     完了通知先メールアドレス
  -h, --help          このヘルプを表示
EOF
}

# === バックアップ前チェック ===
pre_backup_checks() {
    log_info "バックアップ前チェックを実行中..."

    # ディスク容量チェック
    local available_gb
    available_gb=$(df -BG "${BACKUP_DEST}" | awk 'NR==2 {gsub(/G/,"",$4); print $4}')
    if (( available_gb < 10 )); then
        log_error "バックアップ先のディスク容量が不足しています: ${available_gb}GB"
        return 1
    fi
    log_info "利用可能ディスク容量: ${available_gb}GB"

    # ソースディレクトリの確認
    for src in ${BACKUP_SRC}; do
        if [[ ! -d "${src}" ]]; then
            log_warn "ソースディレクトリが存在しません: ${src}"
        fi
    done

    # バックアップ先ディレクトリの作成
    mkdir -p "${BACKUP_DEST}"/{full,incremental,logs}
    log_info "バックアップ前チェック完了"
}

# === フルバックアップ ===
do_full_backup() {
    local timestamp
    timestamp=$(date +%Y%m%d_%H%M%S)
    local backup_name="full_${timestamp}"
    local backup_path="${BACKUP_DEST}/full/${backup_name}"

    log_info "フルバックアップ開始: ${backup_path}"

    mkdir -p "${backup_path}"

    local total_size=0
    local backup_start_time
    backup_start_time=$(date +%s)

    for src in ${BACKUP_SRC}; do
        if [[ ! -d "${src}" ]]; then
            log_warn "スキップ(存在しない): ${src}"
            continue
        fi

        local dir_name
        dir_name=$(echo "${src}" | tr '/' '_' | sed 's/^_//')
        
        log_info "バックアップ中: ${src}${backup_path}/${dir_name}"

        rsync -av \
            --delete \
            --exclude='*.tmp' \
            --exclude='*.swap' \
            --exclude='.git' \
            --log-file="${LOG_FILE}" \
            "${src}/" \
            "${backup_path}/${dir_name}/"

        local src_size
        src_size=$(du -sm "${src}" | cut -f1)
        total_size=$((total_size + src_size))
    done

    # メタデータファイルの作成
    cat > "${backup_path}/BACKUP_INFO" << EOF
バックアップタイプ: フル
作成日時: $(date '+%Y-%m-%d %H:%M:%S')
ホスト名: $(hostname)
ソース: ${BACKUP_SRC}
合計サイズ: ${total_size}MB
EOF

    # 圧縮(オプション)
    if [[ "${COMPRESS}" == "yes" ]]; then
        log_info "アーカイブを圧縮中..."
        tar -czf "${backup_path}.tar.gz" -C "${BACKUP_DEST}/full" "${backup_name}"
        rm -rf "${backup_path}"
        local compressed_size
        compressed_size=$(du -sh "${backup_path}.tar.gz" | cut -f1)
        log_info "圧縮完了: ${backup_path}.tar.gz (${compressed_size})"
    fi

    local elapsed=$(( $(date +%s) - backup_start_time ))
    log_info "フルバックアップ完了: ${elapsed}秒, ${total_size}MB"
    echo "${backup_name}"
}

# === 増分バックアップ(rsync + ハードリンク)===
do_incremental_backup() {
    local timestamp
    timestamp=$(date +%Y%m%d_%H%M%S)
    local backup_name="inc_${timestamp}"
    local backup_path="${BACKUP_DEST}/incremental/${backup_name}"

    # 最新のバックアップを参照リンクとして使用
    local link_dest=""
    local latest_backup
    latest_backup=$(ls -1d "${BACKUP_DEST}/incremental"/inc_* 2>/dev/null | tail -1)
    if [[ -n "${latest_backup}" ]]; then
        link_dest="--link-dest=${latest_backup}"
        log_info "参照先: ${latest_backup}"
    else
        log_info "初回増分バックアップ(フルコピー)"
    fi

    mkdir -p "${backup_path}"
    log_info "増分バックアップ開始: ${backup_path}"

    for src in ${BACKUP_SRC}; do
        if [[ ! -d "${src}" ]]; then
            continue
        fi

        local dir_name
        dir_name=$(echo "${src}" | tr '/' '_' | sed 's/^_//')
        mkdir -p "${backup_path}/${dir_name}"

        rsync -a \
            --delete \
            ${link_dest:+${link_dest}/${dir_name}} \
            --exclude='*.tmp' \
            "${src}/" \
            "${backup_path}/${dir_name}/"
    done

    log_info "増分バックアップ完了"
    echo "${backup_name}"
}

# === 古いバックアップの削除 ===
cleanup_old_backups() {
    log_info "古いバックアップを削除中(保持期間: ${RETENTION_DAYS}日)..."

    local deleted=0
    while IFS= read -r old_backup; do
        log_info "削除: ${old_backup}"
        rm -rf "${old_backup}"
        ((deleted++))
    done < <(find "${BACKUP_DEST}" -maxdepth 2 \
        \( -name "full_*" -o -name "inc_*" \) \
        -mtime +${RETENTION_DAYS} \
        2>/dev/null)

    log_info "${deleted}件の古いバックアップを削除しました"
}

# === 完了通知 ===
send_notification() {
    local status="${1}"
    local message="${2}"

    if [[ -z "${NOTIFY_EMAIL}" ]]; then
        return 0
    fi

    if command -v mail &>/dev/null; then
        echo "${message}" | mail -s "[${status}] バックアップ完了通知 - $(hostname)" "${NOTIFY_EMAIL}"
        log_info "通知メールを送信: ${NOTIFY_EMAIL}"
    fi
}

# === メイン処理 ===
main() {
    setup_logging
    parse_args "$@"

    log_info "=== バックアップ開始 ==="
    log_info "タイプ: ${BACKUP_TYPE}"
    log_info "ソース: ${BACKUP_SRC}"
    log_info "宛先: ${BACKUP_DEST}"

    # 排他制御
    if [[ -f "${LOCK_FILE}" ]]; then
        local old_pid
        old_pid=$(cat "${LOCK_FILE}")
        if kill -0 "${old_pid}" 2>/dev/null; then
            log_error "別のバックアッププロセスが実行中 (PID: ${old_pid})"
            exit 1
        fi
    fi
    echo $$ > "${LOCK_FILE}"
    trap "rm -f '${LOCK_FILE}'" EXIT

    pre_backup_checks

    local backup_name
    case "${BACKUP_TYPE}" in
        full)        backup_name=$(do_full_backup) ;;
        incremental) backup_name=$(do_incremental_backup) ;;
        *) log_error "不明なバックアップタイプ: ${BACKUP_TYPE}"; exit 1 ;;
    esac

    cleanup_old_backups

    local summary="バックアップ成功: ${backup_name} ($(date '+%Y-%m-%d %H:%M:%S'))"
    log_info "=== ${summary} ==="
    send_notification "SUCCESS" "${summary}"
}

main "$@"

9.2 監視スクリプト

#!/usr/bin/env bash
# monitor.sh - システムリソース監視スクリプト

set -euo pipefail

readonly SCRIPT_NAME=$(basename "$0")
readonly LOG_FILE="/var/log/monitor.log"
readonly ALERT_LOG="/var/log/monitor_alerts.log"

# アラートしきい値
CPU_THRESHOLD="${CPU_THRESHOLD:-80}"
MEM_THRESHOLD="${MEM_THRESHOLD:-85}"
DISK_THRESHOLD="${DISK_THRESHOLD:-90}"
LOAD_THRESHOLD="${LOAD_THRESHOLD:-4.0}"
ALERT_EMAIL="${ALERT_EMAIL:-}"
SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"; }
alert() {
    local message="$*"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ALERT] ${message}" | tee -a "${ALERT_LOG}"
    send_alert "${message}"
}

# === アラート送信 ===
send_alert() {
    local message="$*"
    local hostname
    hostname=$(hostname -f)

    # メール通知
    if [[ -n "${ALERT_EMAIL}" ]] && command -v mail &>/dev/null; then
        echo "${message}" | mail -s "[ALERT] ${hostname}: システム監視アラート" "${ALERT_EMAIL}"
    fi

    # Slack通知
    if [[ -n "${SLACK_WEBHOOK}" ]] && command -v curl &>/dev/null; then
        curl -s -X POST -H 'Content-type: application/json' \
            --data "{\"text\":\":warning: *${hostname}*: ${message}\"}" \
            "${SLACK_WEBHOOK}" &>/dev/null || true
    fi
}

# === CPU使用率チェック ===
check_cpu() {
    local cpu_usage
    cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d. -f1)
    # 代替方法: vmstatを使用
    # cpu_usage=$(vmstat 1 1 | awk 'NR==3{print 100-$15}')

    log "CPU使用率: ${cpu_usage}%"

    if (( cpu_usage > CPU_THRESHOLD )); then
        alert "CPU使用率が高い: ${cpu_usage}% (閾値: ${CPU_THRESHOLD}%)"

        # 上位プロセスを記録
        log "=== CPU使用量上位プロセス ==="
        ps aux --sort=-%cpu | head -6 | tee -a "${LOG_FILE}"
        return 1
    fi
    return 0
}

# === メモリ使用率チェック ===
check_memory() {
    local mem_info
    mem_info=$(free -m | awk 'NR==2{
        used=$3; total=$2; free=$4; cached=$6
        printf "%d %d %d %d", used, total, free, cached
    }')
    read -r used total free cached <<< "${mem_info}"
    local usage_pct=$(( (used * 100) / total ))

    log "メモリ使用率: ${usage_pct}% (使用: ${used}MB / 合計: ${total}MB, キャッシュ: ${cached}MB)"

    if (( usage_pct > MEM_THRESHOLD )); then
        alert "メモリ使用率が高い: ${usage_pct}% (閾値: ${MEM_THRESHOLD}%)"

        log "=== メモリ使用量上位プロセス ==="
        ps aux --sort=-%mem | head -6 | tee -a "${LOG_FILE}"
        return 1
    fi
    return 0
}

# === ディスク使用率チェック ===
check_disk() {
    local alert_triggered=0

    while IFS= read -r line; do
        local usage_pct mount_point
        usage_pct=$(echo "${line}" | awk '{print $5}' | tr -d '%')
        mount_point=$(echo "${line}" | awk '{print $6}')

        # 特定のマウントポイントをスキップ
        case "${mount_point}" in
            /proc|/sys|/dev|/run|/snap/*) continue ;;
        esac

        log "ディスク使用率 ${mount_point}: ${usage_pct}%"

        if (( usage_pct > DISK_THRESHOLD )); then
            alert "ディスク容量不足 ${mount_point}: ${usage_pct}% (閾値: ${DISK_THRESHOLD}%)"
            alert_triggered=1
        fi
    done < <(df -h | awk 'NR>1 && $6!="Mounted" {print}')

    return ${alert_triggered}
}

# === ロードアベレージチェック ===
check_load() {
    local load_avg
    load_avg=$(uptime | awk -F'load average:' '{print $2}' | awk '{print $1}' | tr -d ',')
    local cpu_count
    cpu_count=$(nproc)
    local load_per_cpu
    load_per_cpu=$(echo "${load_avg} ${cpu_count}" | awk '{printf "%.2f", $1/$2}')

    log "ロードアベレージ: ${load_avg} (CPU数: ${cpu_count}, CPU毎: ${load_per_cpu})"

    if (( $(echo "${load_avg} > ${LOAD_THRESHOLD}" | bc -l) )); then
        alert "ロードアベレージが高い: ${load_avg} (閾値: ${LOAD_THRESHOLD})"
        return 1
    fi
    return 0
}

# === サービス稼働確認 ===
check_services() {
    local services=("nginx" "sshd" "cron" "rsyslog")
    local failed=()

    for service in "${services[@]}"; do
        if systemctl is-active --quiet "${service}" 2>/dev/null; then
            log "サービス稼働中: ${service}"
        else
            log "サービス停止中: ${service}"
            failed+=("${service}")
        fi
    done

    if [[ ${#failed[@]} -gt 0 ]]; then
        alert "停止中のサービス: ${failed[*]}"
        return 1
    fi
    return 0
}

# === ポートの開放確認 ===
check_ports() {
    declare -A ports=(
        [22]="SSH"
        [80]="HTTP"
        [443]="HTTPS"
    )

    for port in "${!ports[@]}"; do
        local service="${ports[${port}]}"
        if ss -tlnp | grep -q ":${port} "; then
            log "ポート開放中: ${port}/${service}"
        else
            alert "ポートが閉じています: ${port}/${service}"
        fi
    done
}

# === メイン処理 ===
main() {
    log "=== システム監視開始 ==="
    log "ホスト: $(hostname -f)"
    log "稼働時間: $(uptime -p)"

    local errors=0

    check_cpu     || ((errors++))
    check_memory  || ((errors++))
    check_disk    || ((errors++))
    check_load    || ((errors++))
    check_services || ((errors++))
    check_ports

    if [[ ${errors} -gt 0 ]]; then
        log "=== 監視完了: ${errors}件の問題を検出 ==="
        exit 1
    else
        log "=== 監視完了: 問題なし ==="
        exit 0
    fi
}

main "$@"

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

#!/usr/bin/env bash
# user_management.sh - ユーザー一括管理スクリプト

set -euo pipefail

readonly SCRIPT_NAME=$(basename "$0")
readonly LOG_FILE="/var/log/user_management.log"
readonly DEFAULT_SHELL="/bin/bash"
readonly DEFAULT_GROUP="users"
readonly MIN_UID=1000
readonly MAX_UID=60000

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"; }
log_info()  { log "[INFO]  $*"; }
log_error() { log "[ERROR] $*" >&2; }

require_root() {
    [[ $(id -u) -eq 0 ]] || { log_error "root権限が必要です"; exit 1; }
}

# === ユーザー作成 ===
create_user() {
    local username="${1}"
    local fullname="${2:-}"
    local groups="${3:-${DEFAULT_GROUP}}"
    local shell="${4:-${DEFAULT_SHELL}}"
    local expire_date="${5:-}"

    # バリデーション
    if [[ ! "${username}" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]]; then
        log_error "無効なユーザー名: ${username}"
        return 1
    fi

    if id "${username}" &>/dev/null; then
        log_info "ユーザーが既に存在します: ${username}"
        return 0
    fi

    # ユーザー作成
    local useradd_opts="-m -s ${shell}"
    [[ -n "${fullname}" ]] && useradd_opts+=" -c '${fullname}'"
    [[ -n "${expire_date}" ]] && useradd_opts+=" -e ${expire_date}"
    
    eval useradd ${useradd_opts} "${username}"
    log_info "ユーザーを作成しました: ${username}"

    # グループへの追加
    IFS=',' read -ra group_list <<< "${groups}"
    for group in "${group_list[@]}"; do
        group="${group// /}"
        if getent group "${group}" &>/dev/null; then
            usermod -aG "${group}" "${username}"
            log_info "グループに追加: ${username}${group}"
        else
            log_error "グループが存在しません: ${group}"
        fi
    done

    # ランダムパスワードの生成と設定
    local temp_password
    temp_password=$(openssl rand -base64 12)
    echo "${username}:${temp_password}" | chpasswd
    passwd --expire "${username}"  # 初回ログイン時のパスワード変更を強制
    log_info "一時パスワードを設定(初回ログイン時に変更必須)"

    # SSH公開鍵ディレクトリの準備
    local ssh_dir="/home/${username}/.ssh"
    mkdir -p "${ssh_dir}"
    chmod 700 "${ssh_dir}"
    touch "${ssh_dir}/authorized_keys"
    chmod 600 "${ssh_dir}/authorized_keys"
    chown -R "${username}:${username}" "${ssh_dir}"

    log_info "ユーザー作成完了: ${username} (一時パスワード: ${temp_password})"
    echo "${temp_password}"
}

# === CSVファイルから一括ユーザー作成 ===
bulk_create_users() {
    local csv_file="${1}"

    if [[ ! -f "${csv_file}" ]]; then
        log_error "CSVファイルが見つかりません: ${csv_file}"
        return 1
    fi

    log_info "一括ユーザー作成開始: ${csv_file}"

    local created=0
    local failed=0

    # ヘッダーをスキップして処理
    # CSV形式: username,fullname,groups,shell,expire_date
    while IFS=',' read -r username fullname groups shell expire_date; do
        # コメント行と空行をスキップ
        [[ "${username}" =~ ^# ]] && continue
        [[ -z "${username}" ]] && continue

        if create_user "${username}" "${fullname}" "${groups}" "${shell}" "${expire_date}"; then
            ((created++))
        else
            ((failed++))
        fi
    done < <(tail -n +2 "${csv_file}")

    log_info "一括作成完了: 成功=${created}, 失敗=${failed}"
}

# === ユーザー削除(安全版)===
delete_user() {
    local username="${1}"
    local archive="${2:-yes}"  # デフォルトでホームディレクトリをアーカイブ

    if ! id "${username}" &>/dev/null; then
        log_error "ユーザーが存在しません: ${username}"
        return 1
    fi

    # root や システムユーザーの削除を防ぐ
    local uid
    uid=$(id -u "${username}")
    if (( uid < MIN_UID )); then
        log_error "システムユーザーの削除は許可されていません: ${username} (UID: ${uid})"
        return 1
    fi

    # 実行中のプロセスを終了
    log_info "ユーザーのプロセスを終了中: ${username}"
    pkill -u "${username}" || true
    sleep 1
    pkill -9 -u "${username}" || true

    # ホームディレクトリのアーカイブ
    local home_dir="/home/${username}"
    if [[ "${archive}" == "yes" && -d "${home_dir}" ]]; then
        local archive_path="/var/backup/users/${username}_$(date +%Y%m%d%H%M%S).tar.gz"
        mkdir -p "$(dirname "${archive_path}")"
        tar -czf "${archive_path}" -C /home "${username}" 2>/dev/null || true
        log_info "ホームディレクトリをアーカイブ: ${archive_path}"
    fi

    # ユーザーの削除
    userdel -r "${username}" 2>/dev/null || userdel "${username}"
    log_info "ユーザーを削除しました: ${username} (UID: ${uid})"
}

# === パスワードポリシーチェック ===
audit_password_policy() {
    log_info "=== パスワードポリシー監査 ==="

    # パスワードなしのユーザー
    log_info "パスワード未設定のユーザー:"
    awk -F: '($2 == "" || $2 == "!") && $3 >= '"${MIN_UID}"' {print "  警告: " $1}' /etc/shadow

    # パスワード有効期限切れのユーザー
    log_info "パスワード期限切れのユーザー:"
    while IFS=: read -r username _ uid _ _ _ _ _ _; do
        (( uid < MIN_UID )) && continue
        local expire
        expire=$(chage -l "${username}" 2>/dev/null | grep "Password expires" | awk '{print $3, $4, $5}')
        echo "  ${username}: ${expire}"
    done < /etc/passwd

    # 長期間ログインなしのユーザー
    log_info "90日以上ログインなしのユーザー:"
    lastlog | awk 'NR>1 && $0 !~ /Never/ {
        if ($3 != "in") {
            cmd = "date -d \""$4" "$5" "$7"\" +%s 2>/dev/null"
            cmd | getline last_login
            close(cmd)
            now = systime()
            days = (now - last_login) / 86400
            if (days > 90) printf "  %s: %d日前\n", $1, days
        }
    }'
}

# === メイン処理 ===
main() {
    require_root

    case "${1:-help}" in
        create)    shift; create_user "$@" ;;
        delete)    shift; delete_user "$@" ;;
        bulk)      shift; bulk_create_users "$@" ;;
        audit)     audit_password_policy ;;
        *)
            echo "使用法: ${SCRIPT_NAME} {create|delete|bulk|audit} [オプション]"
            echo "  create USERNAME [FULLNAME] [GROUPS] [SHELL] [EXPIRE_DATE]"
            echo "  delete USERNAME [yes|no]  # yes=ホームディレクトリをアーカイブ"
            echo "  bulk CSV_FILE"
            echo "  audit"
            ;;
    esac
}

main "$@"

9.4 ログ解析スクリプト

#!/usr/bin/env bash
# log_analyzer.sh - Nginxアクセスログ解析スクリプト

set -euo pipefail

readonly DEFAULT_LOG="/var/log/nginx/access.log"
readonly REPORT_DIR="/var/reports/nginx"

# Nginxのデフォルトログフォーマット:
# $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"

analyze_access_log() {
    local log_file="${1:-${DEFAULT_LOG}}"
    local lines="${2:-all}"
    local report_file="${REPORT_DIR}/report_$(date +%Y%m%d_%H%M%S).txt"

    if [[ ! -f "${log_file}" ]]; then
        echo "ログファイルが見つかりません: ${log_file}" >&2
        return 1
    fi

    mkdir -p "${REPORT_DIR}"

    # 解析対象のログを取得
    local log_data
    if [[ "${lines}" == "all" ]]; then
        log_data=$(cat "${log_file}")
    else
        log_data=$(tail -n "${lines}" "${log_file}")
    fi

    local total_requests
    total_requests=$(echo "${log_data}" | wc -l)

    {
        echo "======================================"
        echo "  Nginxアクセスログ解析レポート"
        echo "  生成日時: $(date '+%Y-%m-%d %H:%M:%S')"
        echo "  対象ファイル: ${log_file}"
        echo "  総リクエスト数: ${total_requests}"
        echo "======================================"
        echo ""

        echo "【1. HTTPステータスコード分布】"
        echo "${log_data}" | awk '{print $9}' | sort | uniq -c | sort -rn | \
            awk '{printf "  %5d  %s\n", $1, $2}'
        echo ""

        echo "【2. アクセス数上位10 IPアドレス】"
        echo "${log_data}" | awk '{print $1}' | sort | uniq -c | sort -rn | head -10 | \
            awk '{printf "  %5d  %s\n", $1, $2}'
        echo ""

        echo "【3. 上位10 リクエストURL】"
        echo "${log_data}" | awk '{print $7}' | sort | uniq -c | sort -rn | head -10 | \
            awk '{printf "  %5d  %s\n", $1, $2}'
        echo ""

        echo "【4. 4xx/5xxエラーの詳細(上位20)】"
        echo "${log_data}" | awk '$9 ~ /^[45]/' | \
            awk '{printf "%s %s %s\n", $1, $9, $7}' | \
            sort | uniq -c | sort -rn | head -20 | \
            awk '{printf "  %4d  IP:%-16s Status:%s URL:%s\n", $1, $2, $3, $4}'
        echo ""

        echo "【5. 時間帯別アクセス数】"
        echo "${log_data}" | awk '{
            match($4, /\[([0-9]+\/[A-Za-z]+\/[0-9]+):([0-9]+):/, arr)
            hour = arr[2]
            count[hour]++
        }
        END {
            for (h in count)
                printf "  %02d時: %d\n", h+0, count[h]
        }' | sort
        echo ""

        echo "【6. 転送量上位10リクエスト】"
        echo "${log_data}" | awk '$10 ~ /^[0-9]/ {print $10, $7}' | \
            sort -rn | head -10 | \
            awk '{printf "  %8.1f KB  %s\n", $1/1024, $2}'
        echo ""

        echo "【7. ユーザーエージェント上位5】"
        echo "${log_data}" | awk -F'"' '{print $6}' | sort | uniq -c | sort -rn | head -5 | \
            awk '{
                count = $1
                $1 = ""
                sub(/^ /, "")
                printf "  %5d  %.80s\n", count, $0
            }'
        echo ""

        echo "【8. エラー率サマリー】"
        echo "${log_data}" | awk -v total="${total_requests}" '{
            if ($9 ~ /^2/) ok++
            if ($9 ~ /^3/) redirect++
            if ($9 ~ /^4/) client_err++
            if ($9 ~ /^5/) server_err++
        }
        END {
            printf "  2xx (成功):      %5d  (%5.1f%%)\n", ok+0, (ok+0)/total*100
            printf "  3xx (リダイレクト): %5d  (%5.1f%%)\n", redirect+0, (redirect+0)/total*100
            printf "  4xx (クライアントエラー): %5d  (%5.1f%%)\n", client_err+0, (client_err+0)/total*100
            printf "  5xx (サーバーエラー):    %5d  (%5.1f%%)\n", server_err+0, (server_err+0)/total*100
        }'

    } | tee "${report_file}"

    echo ""
    echo "レポートを保存しました: ${report_file}"
}

analyze_access_log "$@"

10. cron・systemd タイマー連携

cron の基礎

# === cron の基本構文 ===
# 分(0-59) 時(0-23) 日(1-31) 月(1-12) 曜日(0-7, 0と7は日曜日)

# crontabの編集
crontab -e       # 現在のユーザーのcrontabを編集
crontab -l       # 現在のcrontabを表示
crontab -r       # crontabを削除
sudo crontab -e  # rootのcrontabを編集

# === crontab の例 ===
# 毎分実行
* * * * * /usr/local/bin/monitor.sh

# 毎時0分に実行
0 * * * * /usr/local/bin/hourly_task.sh

# 毎日午前2時に実行
0 2 * * * /usr/local/bin/daily_backup.sh

# 毎週日曜日の午前3時に実行
0 3 * * 0 /usr/local/bin/weekly_cleanup.sh

# 毎月1日の午前0時に実行
0 0 1 * * /usr/local/bin/monthly_report.sh

# 平日(月〜金)の午前9時〜午後6時まで毎時実行
0 9-18 * * 1-5 /usr/local/bin/business_hours_task.sh

# 5分おきに実行
*/5 * * * * /usr/local/bin/check_service.sh

# 1月, 4月, 7月, 10月の1日に実行(四半期)
0 0 1 1,4,7,10 * /usr/local/bin/quarterly_audit.sh

# === cron の特別な文字列 ===
@reboot   /usr/local/bin/on_startup.sh     # 起動時
@hourly   /usr/local/bin/hourly.sh         # = "0 * * * *"
@daily    /usr/local/bin/daily.sh          # = "0 0 * * *"
@weekly   /usr/local/bin/weekly.sh         # = "0 0 * * 0"
@monthly  /usr/local/bin/monthly.sh        # = "0 0 1 * *"
@yearly   /usr/local/bin/yearly.sh         # = "0 0 1 1 *"

cron 対応スクリプトのベストプラクティス

#!/usr/bin/env bash
# cron_safe_script.sh - cron対応の安全なスクリプトテンプレート

set -euo pipefail

# === cron環境での注意点 ===
# 1. cronはPATHが最小限しか設定されていない
# 2. 環境変数が通常の対話セッションとは異なる
# 3. 標準入力がない

# === パスの明示的な設定 ===
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# === 出力先の設定 ===
readonly LOG_FILE="/var/log/cron_script.log"
readonly LOCK_FILE="/var/run/cron_script.lock"

exec >> "${LOG_FILE}" 2>&1
echo "=== 実行開始: $(date '+%Y-%m-%d %H:%M:%S') ==="

# === 重複実行防止 ===
if [[ -f "${LOCK_FILE}" ]]; then
    pid=$(cat "${LOCK_FILE}")
    if kill -0 "${pid}" 2>/dev/null; then
        echo "別のインスタンスが実行中 (PID: ${pid})"
        exit 0
    fi
    rm -f "${LOCK_FILE}"
fi
echo $$ > "${LOCK_FILE}"
trap "rm -f '${LOCK_FILE}'" EXIT

# === メイン処理 ===
echo "処理を実行中..."
# ... 実際の処理 ...

echo "=== 実行完了: $(date '+%Y-%m-%d %H:%M:%S') ==="

systemd タイマー

systemdタイマーはcronの代替として、より高度な機能を提供します。

# === systemd タイマーの構造 ===
# タイマーユニット (.timer) + サービスユニット (.service) のペア

# 例: /etc/systemd/system/backup.service
cat > /etc/systemd/system/backup.service << 'EOF'
[Unit]
Description=Daily Backup Service
After=network.target
# 依存サービスが起動後に実行

[Service]
Type=oneshot
User=backup
Group=backup
# スクリプトを安全な環境で実行
ExecStart=/usr/local/bin/backup.sh --incremental
# 失敗時の出力をsyslogに記録
StandardOutput=journal
StandardError=journal
# セキュリティ設定
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/backup /var/log/backup
# タイムアウト設定
TimeoutStartSec=2h

[Install]
WantedBy=multi-user.target
EOF

# 例: /etc/systemd/system/backup.timer
cat > /etc/systemd/system/backup.timer << 'EOF'
[Unit]
Description=Daily Backup Timer
Requires=backup.service

[Timer]
# 毎日午前2時に実行
OnCalendar=*-*-* 02:00:00
# システム起動時にもし実行時刻を過ぎていたら即座に実行
Persistent=true
# ランダムな遅延(サーバー負荷分散)
RandomizedDelaySec=5min
# 前の実行からの最小間隔
AccuracySec=1min

[Install]
WantedBy=timers.target
EOF

# タイマーの有効化と開始
systemctl daemon-reload
systemctl enable --now backup.timer

# タイマーの確認
systemctl list-timers
systemctl status backup.timer
systemctl status backup.service
NEXT                         LEFT          LAST                         PASSED       UNIT           ACTIVATES
Thu 2026-04-11 02:00:00 JST  13h left      Wed 2026-04-10 02:03:17 JST  10h ago      backup.timer   backup.service

systemd タイマーの時間指定形式

# === OnCalendar の時間指定 ===

# 基本形式: 曜日 年-月-日 時:分:秒

# 毎分
OnCalendar=minutely
# または
OnCalendar=*-*-* *:*:00

# 毎時
OnCalendar=hourly
# または
OnCalendar=*-*-* *:00:00

# 毎日午前3時
OnCalendar=daily
OnCalendar=*-*-* 03:00:00

# 毎週月曜日の午前2時
OnCalendar=Mon *-*-* 02:00:00

# 毎月1日の深夜
OnCalendar=*-*-01 00:00:00

# 平日の午前9時から午後6時まで毎時間
OnCalendar=Mon..Fri *-*-* 09..18:00:00

# 特定の日時
OnCalendar=2026-04-15 14:30:00

# 15分おき
OnCalendar=*:0/15

# 時間指定の検証
systemd-analyze calendar "Mon..Fri *-*-* 09..18:00:00"

cron と systemd タイマーの比較

機能cronsystemd タイマー
設定の簡単さシンプル(1行)やや複雑(2ファイル)
ログ管理手動設定が必要journalに自動記録
依存関係指定できないAfter=, Requires= で指定可
失敗時の対応手動設定が必要OnFailure= で自動対応
起動時実行@reboot のみPersistent=true で柔軟対応
実行コンテキスト環境変数が少ないユニット内で完全制御
リソース制限外部ツールが必要cgroups で制御可能
一時停止crontab編集が必要systemctl disable/stop
ランダム遅延手動実装が必要RandomizedDelaySec
監視・ステータス外部ツールが必要systemctl status で確認
移植性POSIX標準systemd依存

systemd タイマーの管理コマンド

# タイマー一覧の確認
systemctl list-timers --all

# 特定タイマーのステータス
systemctl status backup.timer

# タイマーの一時停止(次回実行のみキャンセル)
systemctl stop backup.timer

# タイマーの無効化(恒久的に無効)
systemctl disable backup.timer

# 即座に手動実行
systemctl start backup.service

# サービスのジャーナルログ確認
journalctl -u backup.service -f           # リアルタイム
journalctl -u backup.service --since today # 今日のログ
journalctl -u backup.service -n 50        # 最新50行

# タイマーの時間指定検証
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
# 出力:
#   Original form: Mon..Fri *-*-* 09:00:00
# Normalized form: Mon..Fri *-*-* 09:00:00
#     Next elapse: Mon 2026-04-13 09:00:00 JST
#        (in UTC): Mon 2026-04-13 00:00:00 UTC
#        From now: 2 days 20h left

11. セキュリティベストプラクティス

入力値のバリデーション

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

# === 入力検証の基本原則 ===
# 1. 外部からの入力は常に「悪意ある値」として扱う
# 2. ホワイトリスト方式(許可リスト)を使う
# 3. eval は絶対に避ける

# === ユーザー入力のサニタイズ ===

validate_username() {
    local username="${1}"
    # ホワイトリスト: 英小文字、数字、アンダースコア、ハイフンのみ
    if [[ ! "${username}" =~ ^[a-z][a-z0-9_-]{0,30}$ ]]; then
        echo "エラー: 無効なユーザー名: ${username}" >&2
        return 1
    fi
    echo "${username}"
}

validate_ip_address() {
    local ip="${1}"
    local regex='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
    if [[ ! "${ip}" =~ ${regex} ]]; then
        echo "エラー: 無効なIPアドレス: ${ip}" >&2
        return 1
    fi
    # 各オクテットが0-255の範囲か確認
    IFS='.' read -ra octets <<< "${ip}"
    for octet in "${octets[@]}"; do
        if (( octet < 0 || octet > 255 )); then
            echo "エラー: 無効なIPアドレス(範囲外): ${ip}" >&2
            return 1
        fi
    done
    echo "${ip}"
}

validate_port() {
    local port="${1}"
    if [[ ! "${port}" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
        echo "エラー: 無効なポート番号: ${port}" >&2
        return 1
    fi
    echo "${port}"
}

validate_date() {
    local date_str="${1}"
    if [[ ! "${date_str}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
        echo "エラー: 無効な日付形式: ${date_str} (YYYY-MM-DD 形式が必要)" >&2
        return 1
    fi
    if ! date -d "${date_str}" &>/dev/null; then
        echo "エラー: 存在しない日付: ${date_str}" >&2
        return 1
    fi
    echo "${date_str}"
}

validate_file_path() {
    local path="${1}"
    local allowed_prefix="${2:-/tmp}"

    # パストラバーサル攻撃を防ぐ
    # realpath で正規化してチェック
    local real_path
    real_path=$(realpath -m "${path}" 2>/dev/null) || {
        echo "エラー: パスの正規化に失敗: ${path}" >&2
        return 1
    }

    if [[ "${real_path}" != "${allowed_prefix}"* ]]; then
        echo "エラー: 許可されていないパス: ${path}" >&2
        return 1
    fi
    echo "${real_path}"
}

# === 使用例 ===
user_input="${1:-}"

# 入力値を検証してから使用
username=$(validate_username "${user_input}") || exit 1
echo "有効なユーザー名: ${username}"

機密情報の安全な取り扱い

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

# === パスワード・シークレットの安全な扱い ===

# NG: コマンドラインにパスワードを渡す(ps で見える)
# mysql -u user -pPASSWORD database  ← 危険!

# OK: 環境変数から読み込む
# export DB_PASSWORD="secret"
# mysql -u user -p"${DB_PASSWORD}" database

# より安全: ファイルからパスワードを読み込む
read_secret_from_file() {
    local secret_file="${1}"
    
    if [[ ! -f "${secret_file}" ]]; then
        echo "エラー: シークレットファイルが見つかりません: ${secret_file}" >&2
        return 1
    fi
    
    # ファイルの権限を確認(所有者のみ読み取り可能であるべき)
    local perms
    perms=$(stat -c "%a" "${secret_file}")
    if [[ "${perms}" != "600" && "${perms}" != "400" ]]; then
        echo "警告: シークレットファイルの権限が緩すぎます: ${perms} (推奨: 600)" >&2
    fi
    
    cat "${secret_file}"
}

# パスワードをプロンプトで読み込む(エコーなし)
read_password() {
    local prompt="${1:-パスワードを入力してください: }"
    local password
    read -rs -p "${prompt}" password
    echo ""  # 改行
    echo "${password}"
}

# === 一時的なシークレットの管理 ===

# メモリ上でのみ使用(ファイルに書き込まない)
DB_PASSWORD=$(read_secret_from_file /etc/myapp/db_password)
export DB_PASSWORD

# スクリプト終了時にシークレットをクリア
trap 'unset DB_PASSWORD' EXIT

# === 設定ファイルの権限設定 ===

secure_config_file() {
    local config_file="${1}"
    local owner="${2:-root}"
    
    if [[ ! -f "${config_file}" ]]; then
        touch "${config_file}"
    fi
    
    chown "${owner}:${owner}" "${config_file}"
    chmod 600 "${config_file}"
    echo "セキュアな設定: ${config_file} (所有者: ${owner}, 権限: 600)"
}

# === eval を避ける ===

# NG: eval を使った変数展開(コマンドインジェクションの危険)
# user_input="foo; rm -rf /"
# eval "command ${user_input}"  ← 絶対NG!

# OK: 配列を使った安全なコマンド構築
build_command() {
    local -a cmd=("mysql")
    cmd+=("-u" "dbuser")
    cmd+=("-h" "localhost")
    cmd+=("mydb")
    
    # コマンドを安全に実行
    "${cmd[@]}"
}

# === スクリプトの権限チェック ===

check_script_security() {
    local script="${1}"
    
    echo "=== スクリプトセキュリティチェック: ${script} ==="
    
    # 所有者チェック
    local owner
    owner=$(stat -c "%U" "${script}")
    if [[ "${owner}" != "root" ]]; then
        echo "警告: スクリプトがrootの所有でない: ${owner}"
    fi
    
    # 書き込み権限チェック
    local perms
    perms=$(stat -c "%a" "${script}")
    if [[ "${perms}" =~ [2367] ]]; then
        echo "警告: グループまたは他者に書き込み権限あり: ${perms}"
    fi
    
    # shellcheck での静的解析
    if command -v shellcheck &>/dev/null; then
        shellcheck "${script}" && echo "shellcheck: 問題なし" || echo "警告: shellcheck が問題を検出"
    fi
}

ログとセキュリティ監査

#!/usr/bin/env bash

# === セキュリティイベントの監視 ===

monitor_security_events() {
    local log_file="${1:-/var/log/auth.log}"
    local threshold="${2:-10}"

    echo "=== セキュリティ監視レポート ($(date '+%Y-%m-%d')) ==="
    echo ""

    # SSH認証失敗の多いIPを特定
    echo "【SSH認証失敗 上位IPアドレス】"
    grep "Failed password" "${log_file}" | \
        grep -oP 'from \K[0-9.]+' | \
        sort | uniq -c | sort -rn | \
        awk -v threshold="${threshold}" '$1 >= threshold {printf "  %5d回  %s\n", $1, $2}' | \
        head -20

    echo ""
    echo "【sudo 使用履歴】"
    grep "sudo:" "${log_file}" | \
        grep -v "pam_unix" | \
        awk '{print $1, $2, $3, $6, $8, $NF}' | \
        tail -20

    echo ""
    echo "【新規ユーザー作成・削除】"
    grep -E "useradd|userdel|usermod" "${log_file}" | tail -10

    echo ""
    echo "【su コマンドの使用】"
    grep "su\[" "${log_file}" | tail -10

    echo ""
    echo "【ログイン失敗の合計】"
    grep "Failed" "${log_file}" | wc -l
}

# === セキュリティベストプラクティスチェックリスト ===
security_audit() {
    echo "=== システムセキュリティ監査 ==="
    local issues=0

    # SSH設定チェック
    check_ssh_config() {
        local sshd_config="/etc/ssh/sshd_config"
        echo ""
        echo "【SSH設定】"

        local checks=(
            "PermitRootLogin:no:rootのSSHログインを禁止すべき"
            "PasswordAuthentication:no:パスワード認証を無効にすべき"
            "X11Forwarding:no:X11転送を無効にすべき"
            "MaxAuthTries:3:認証試行回数を制限すべき"
        )

        for check in "${checks[@]}"; do
            IFS=':' read -r param recommended message <<< "${check}"
            local current
            current=$(grep -iP "^${param}\s" "${sshd_config}" 2>/dev/null | awk '{print $2}' || echo "未設定")
            if [[ "${current,,}" == "${recommended,,}" ]]; then
                echo "  ✓ ${param}: ${current}"
            else
                echo "  ✗ ${param}: ${current} (推奨: ${recommended}) - ${message}"
                ((issues++))
            fi
        done
    }

    # ファイアウォールチェック
    check_firewall() {
        echo ""
        echo "【ファイアウォール】"
        if systemctl is-active --quiet ufw 2>/dev/null; then
            echo "  ✓ UFW: 有効"
        elif systemctl is-active --quiet firewalld 2>/dev/null; then
            echo "  ✓ firewalld: 有効"
        else
            echo "  ✗ ファイアウォールが無効"
            ((issues++))
        fi
    }

    # SUID/SGIDファイルチェック
    check_suid_files() {
        echo ""
        echo "【SUID/SGIDファイル(要確認)】"
        find / -perm -4000 -o -perm -2000 2>/dev/null | \
            grep -v proc | grep -v sys | head -20 | \
            while read -r f; do
                echo "  $(ls -l "${f}")"
            done
    }

    check_ssh_config
    check_firewall
    check_suid_files

    echo ""
    if [[ ${issues} -gt 0 ]]; then
        echo "  ${issues}件の問題が見つかりました"
    else
        echo "  問題は見つかりませんでした"
    fi
}

スクリプトセキュリティのベストプラクティス一覧

カテゴリ推奨非推奨
変数の引用"${var}"$var (グロブ・単語分割のリスク)
コマンド実行配列による構築evalbash -c "$input"
一時ファイルmktemp予測可能な名前 (/tmp/script.tmp)
シークレットファイル・環境変数コマンドライン引数
権限最小権限の原則chmod 777
入力値ホワイトリスト検証サニタイズせずにそのまま使用
パス絶対パス$PATH 依存の相対パス
ファイル権限600 (秘密情報)644, 666 (秘密情報)

12. 便利なワンライナー集

システム情報収集

# === ハードウェア情報 ===

# CPU情報
lscpu | grep -E "Architecture|CPU\(s\)|Model name|CPU MHz"

# メモリ情報(GB単位)
free -h

# ディスク使用量(サイズ降順)
df -h | sort -k5 -rn

# ブロックデバイス一覧
lsblk -d -o NAME,SIZE,TYPE,ROTA,VENDOR,MODEL

# PCI デバイス一覧(グラフィックカード等)
lspci | grep -iE "VGA|Display|3D"

# カーネル情報
uname -r

# OS情報
cat /etc/os-release | grep -E "^(NAME|VERSION)="

# システム稼働時間と平均負荷
uptime -p; uptime | awk -F': ' '{print "負荷: "$2}'

# ネットワークインターフェース情報
ip -br addr show

# 全IPアドレスをシンプルに表示
hostname -I | tr ' ' '\n' | grep -v '^$'

プロセス管理

# === プロセス操作ワンライナー ===

# CPU使用量上位10プロセス
ps aux --sort=-%cpu | awk 'NR==1 || NR<=11' | column -t

# メモリ使用量上位10プロセス
ps aux --sort=-%mem | awk 'NR==1 || NR<=11' | column -t

# ゾンビプロセスの確認
ps aux | awk '$8=="Z" {print}'

# 特定プロセスの全スレッド確認
ps -T -p $(pgrep nginx | head -1)

# プロセスツリーを圧縮表示
pstree -c -p -u | head -20

# 特定ポートを使用しているプロセスを確認
ss -tlnp | grep :80
# または
fuser 80/tcp

# 開いているファイルディスクリプタ数が多いプロセス
lsof 2>/dev/null | awk '{print $2}' | sort | uniq -c | sort -rn | head -10

# 実行時間が長いプロセスを探す
ps aux --sort=-etime | head -10

# メモリを多く使っているプロセスの詳細
ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%mem | head -10

# バックグラウンドプロセスを一括終了
kill $(pgrep -f "old_process_name")

ネットワーク管理

# === ネットワーク診断ワンライナー ===

# 現在の接続状況サマリー
ss -s

# TCP接続をステータス別に集計
ss -tn | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn

# 特定ホストへの接続をカウント
ss -tn | grep "ESTAB" | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -10

# ネットワーク帯域使用状況(1秒間隔)
# sar -n DEV 1 5 | grep eth0

# ルーティングテーブルの確認
ip route show

# ARP テーブルの確認
arp -n

# DNS 解決の確認
dig +short @8.8.8.8 example.com

# HTTPレスポンスタイムの計測
curl -o /dev/null -s -w "dns: %{time_namelookup}\nconnect: %{time_connect}\ntotal: %{time_total}\n" https://example.com

# 開いているポートのスキャン(自ホスト)
ss -tlnp | awk 'NR>1 {print $4}' | grep -oP ':\K[0-9]+$' | sort -n

# NTP同期確認
timedatectl status | grep -E "NTP|synchronized"

# 帯域幅テスト(iperf3 が必要)
# iperf3 -c server_ip -t 10 --bidir

ファイルシステム

# === ファイル検索・操作ワンライナー ===

# 最近変更されたファイルTOP10
find / -type f -newer /proc/1/cmdline -ls 2>/dev/null | sort -k7 -rn | head -10

# 大きなファイルを探す(100MB以上)
find / -type f -size +100M -exec ls -lh {} \; 2>/dev/null | sort -k5 -rh

# ディスク消費上位ディレクトリ
du -hx / --max-depth=3 2>/dev/null | sort -rh | head -20

# 特定拡張子のファイルを一括処理
find /var/log -name "*.log.gz" -mtime +30 -delete

# 重複ファイルの検出(md5hashを使用)
find /home -type f -exec md5sum {} + 2>/dev/null | sort | awk 'BEGIN{last=""} $1==last{print; if(!printed){print last_line; printed=1}} {last=$1; last_line=$0; printed=0}'

# iノード使用量が高いディレクトリを確認
df -i | awk 'NR>1 && $5+0 > 80 {print}'

# シンボリックリンクが壊れているファイルを探す
find / -type l ! -exists 2>/dev/null | head -20

# パーミッションが777のファイルを探す(セキュリティチェック)
find /home /tmp /var/tmp -perm 777 -type f 2>/dev/null

# ファイルのMIMEタイプ確認
file -b --mime-type suspicious_file.bin

# 特定ユーザーのファイルをすべて検索
find / -user alice -type f 2>/dev/null | head -20

ログ解析

# === ログ解析ワンライナー ===

# Nginxアクセスログの上位IPアドレス
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

# 1時間あたりのリクエスト数(今日分)
grep "$(date '+%d/%b/%Y')" /var/log/nginx/access.log | \
    awk '{print substr($4,14,2)}' | sort | uniq -c

# 4xx/5xxエラーのみ抽出
awk '$9 ~ /^[45]/' /var/log/nginx/access.log | tail -20

# SSH ブルートフォース攻撃を確認
grep "Failed password" /var/log/auth.log | \
    grep -oP 'from \K[0-9.]+' | \
    sort | uniq -c | sort -rn | head -10

# システムログからエラー/警告を抽出
journalctl -p err -n 50 --no-pager

# 最近1時間のカーネルメッセージ
journalctl -k --since "1 hour ago" --no-pager

# ログファイルのサイズと最終更新日時
ls -lh /var/log/*.log | awk '{printf "%-50s %8s %s %s %s\n", $9, $5, $6, $7, $8}'

# アプリケーションログからスタックトレースを抽出
grep -A 20 "Exception\|Traceback" /var/log/app/error.log | head -100

# 特定の時間帯だけを抽出
awk '/2026-04-10 10:00/,/2026-04-10 11:00/' /var/log/app.log

# ログの行数と最大行長
wc -l /var/log/syslog; awk '{if(length>max)max=length} END{print "最大行長: "max}' /var/log/syslog

システム管理

# === システム管理ワンライナー ===

# パッケージ更新が必要な一覧(Ubuntu/Debian)
apt list --upgradable 2>/dev/null | grep -v "Listing"

# インストール済みパッケージ数
dpkg -l | grep ^ii | wc -l            # Debian/Ubuntu
rpm -qa | wc -l                        # RHEL/CentOS

# 孤立したパッケージを確認(Ubuntu)
apt-get autoremove --dry-run 2>/dev/null | grep "^Remv"

# サービスの稼働状況を一覧表示
systemctl list-units --type=service --state=running | head -20

# 失敗したサービスを確認
systemctl list-units --type=service --state=failed

# cron ジョブ一覧(全ユーザー)
for user in $(cut -d: -f1 /etc/passwd); do
    crontab -l -u "${user}" 2>/dev/null | grep -v "^#\|^$" | \
        awk -v u="${user}" '{print u": "$0}'
done

# 現在ログインしているユーザー
who -H

# 最近ログインしたユーザー
last | head -10

# ファイアウォールルール確認
iptables -L -n -v --line-numbers 2>/dev/null || ufw status verbose 2>/dev/null

# SELinuxの状態
getenforce 2>/dev/null || echo "SELinux未インストール"

# カーネルパラメータの確認
sysctl -a 2>/dev/null | grep -E "vm.swappiness|net.ipv4.tcp_fin_timeout|fs.file-max"

# 利用可能なエントロピー(低いと暗号処理が遅くなる)
cat /proc/sys/kernel/random/entropy_avail

データ処理

# === データ処理ワンライナー ===

# CSVの特定列を抽出
cut -d',' -f1,3 data.csv

# CSVの行をソート(2列目数値順)
sort -t',' -k2 -n data.csv

# テキストの行数・単語数・文字数
wc -lwc file.txt

# 重複行を削除(順序を保持)
awk '!seen[$0]++' file.txt

# 重複行の数を確認
sort file.txt | uniq -d | wc -l

# 列の合計を計算(awk)
awk '{sum += $1} END {print "合計:", sum}' numbers.txt

# 数値列の平均・最大・最小
awk '{sum+=$1; if($1>max)max=$1; if(NR==1||$1<min)min=$1; count++} END {printf "平均:%.2f 最大:%d 最小:%d\n", sum/count, max, min}' numbers.txt

# 文字列をBase64エンコード/デコード
echo -n "Hello, World!" | base64
echo "SGVsbG8sIFdvcmxkIQ==" | base64 -d

# ランダムパスワード生成(16文字)
openssl rand -base64 16 | tr -d '/+=' | cut -c1-16

# UUIDの生成
uuidgen
# または
cat /proc/sys/kernel/random/uuid

# JSONの整形(jq が必要)
echo '{"name":"Alice","age":30}' | jq .

# YAMLをJSONに変換(python が必要)
python3 -c "import sys, yaml, json; json.dump(yaml.safe_load(sys.stdin), sys.stdout)" < config.yaml

パフォーマンス分析

# === パフォーマンス分析ワンライナー ===

# I/Oウェイトの確認
iostat -x 1 3

# ディスクI/O上位プロセス
iotop -bo --iter=3 2>/dev/null | head -20

# ネットワーク接続統計
netstat -s 2>/dev/null | grep -E "segments|packets|errors" | head -10

# TCP再送信率の確認
ss -s | grep "TCP:"

# メモリ詳細情報
cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|Cached|SwapUsed"

# スワップ使用量の多いプロセス
for pid in /proc/[0-9]*/; do
    pid=${pid%*/}; pid=${pid##*/}
    swap=$(awk '/VmSwap/{print $2}' /proc/${pid}/status 2>/dev/null)
    if [[ -n "${swap}" && "${swap}" -gt 0 ]]; then
        name=$(cat /proc/${pid}/comm 2>/dev/null)
        echo "${swap}KB ${pid} ${name}"
    fi
done | sort -rn | head -10

# ファイルシステムのバッファキャッシュをクリア(本番環境では注意)
# sync && echo 3 > /proc/sys/vm/drop_caches

# コンテキストスイッチ率の確認
vmstat 1 5 | awk 'NR>2{print "cs:"$12, "in:"$11}'

# アプリケーションの起動時間を計測
time bash -c "sleep 1"

# コマンドのシステムコール追跡(strace)
strace -c -p $(pgrep nginx | head -1) 2>&1 &
sleep 5
kill %1

13. まとめと参考資料

重要なベストプラクティスまとめ

#!/usr/bin/env bash
# === 本番品質スクリプトの必須テンプレート ===

set -euo pipefail  # 必須: エラー安全設定

readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
readonly LOG_FILE="/var/log/${SCRIPT_NAME%.sh}.log"

# ロギング
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$1] ${SCRIPT_NAME}: $2" | tee -a "${LOG_FILE}"; }
log_info()  { log "INFO"  "$*"; }
log_warn()  { log "WARN"  "$*" >&2; }
log_error() { log "ERROR" "$*" >&2; }
die()       { log_error "$*"; exit 1; }

# クリーンアップ
cleanup() { 
    log_info "クリーンアップ完了"
    # 一時ファイル等の削除
}
trap cleanup EXIT
trap 'die "SIGINTを受信"' INT
trap 'die "SIGTERMを受信"' TERM

# 事前チェック
[[ $(id -u) -eq 0 ]] || die "root権限が必要です"
command -v rsync &>/dev/null || die "rsyncが必要です"

# メイン処理
main() {
    log_info "=== ${SCRIPT_NAME} 開始 ==="
    # ... 処理 ...
    log_info "=== ${SCRIPT_NAME} 完了 ==="
}

main "$@"

チェックリスト:本番スクリプトの品質確認

安全性

  • set -euo pipefail を設定している
  • 外部入力をバリデーションしている
  • eval を使用していない
  • 一時ファイルは mktemp で作成している
  • trap でクリーンアップを登録している
  • シークレット情報はコマンドラインに含めていない

堅牢性

  • 全ての変数を引用符で囲んでいる
  • ローカル変数に local を使用している
  • コマンドの存在確認をしている
  • ロックファイルで重複実行を防止している
  • エラーハンドリングが適切に実装されている

保守性

  • shellcheck で静的解析を通過している
  • ログ出力が適切に設定されている
  • コメントが適切に記述されている
  • 設定値が定数として分離されている
  • 使用法(usage)が実装されている

テスト

  • 正常系・異常系のテストを実施している
  • 境界値(空文字、特殊文字等)をテストしている
  • 実際の環境で動作確認済みである

よくある落とし穴と対処法

落とし穴問題解決策
未引用変数$var でワード分割・グロブ展開が発生常に "${var}" を使用
パイプの終了コードcmd1 | cmd2 で cmd1 の失敗が見えないset -o pipefail を設定
for + コマンド置換for i in $(cmd) でスペース・改行に問題while IFS= read -r を使用
[[ と [ の混同[ は POSIX sh、[[ は bash専用bash では [[ を一貫して使用
算術演算の落とし穴let "x = 0" で終了コードが非0になる`(( x = 0 ))
ls のパースfor f in $(ls) はスペースを含むファイルで失敗for f in * または find を使用
cd の失敗cd /nonexistent && rm files で危険cd /path || exit 1 を使用
シェバン/bin/bash が存在しない環境#!/usr/bin/env bash を使用

参考資料

公式ドキュメント・仕様

リソースURL説明
GNU Bash Manualhttps://www.gnu.org/software/bash/manual/Bash公式リファレンス
POSIX Shell Specificationhttps://pubs.opengroup.org/onlinepubs/9699919799/POSIX標準
Advanced Bash-Scripting Guidehttps://tldp.org/LDP/abs/html/詳細なBashガイド
shellcheckhttps://www.shellcheck.net/シェルスクリプト静的解析

ツール・ユーティリティ

ツール用途インストール
shellcheck静的解析apt install shellcheck
bash-completionコマンド補完apt install bash-completion
batsBashテストフレームワークapt install bats
shfmtコードフォーマッタGo binary
explainshellコマンド説明https://explainshell.com

学習リソース

リソース種類推奨レベル
Linux Command Line (William Shotts)書籍入門〜中級
The Linux Documentation ProjectWebサイト全レベル
Greg's Wiki (BashGuide)Webサイト中級〜上級
Wooledge Bash FAQWebサイト中級〜上級
Linux Journal Archive雑誌/Web上級

Bash バージョン別機能一覧

機能最小Bashバージョン
[[ ]] 条件式2.02+
連想配列 (declare -A)4.0+
mapfile / readarray4.0+
${var^^} 大文字変換4.0+
負のインデックス (${arr[-1]})4.3+
declare -n 参照変数4.3+
local - オプションのローカルスコープ4.4+
${parameter@operator} 変換4.4+
local -A 連想配列のローカル宣言4.2+

まとめ

シェルスクリプティングはLinuxシステム管理の基盤技術です。本ガイドで解説した内容を実践に適用する際には、以下の3つの原則を常に念頭に置いてください:

  1. 安全性第一: set -euo pipefail と適切なエラーハンドリングで予期しない動作を防ぐ
  2. 再利用性: 関数化・モジュール化して保守しやすいコードを書く
  3. 継続的改善: shellcheck などのツールを活用し、コードの品質を維持する

シェルスクリプトは書き始めると奥が深く、常に学びがあります。本ガイドを起点として、実際のシステム管理業務でのスクリプト作成に役立てていただければ幸いです。