Shell Scripting for System Administration
Linuxシステム管理のためのシェルスクリプティング完全ガイド
対象読者: Linuxシステム管理者、SREエンジニア、DevOpsエンジニア
難易度: 中級〜上級
最終更新: 2026年4月
目次
- はじめに
- Bash基礎
- デバッグ技法
- エラーハンドリング
- シグナルハンドリング(trap)
- プロセス管理
- 正規表現とテキスト処理
- ファイル処理
- システム管理スクリプト実例
- 9.1 バックアップスクリプト
- 9.2 監視スクリプト
- 9.3 ユーザー管理スクリプト
- 9.4 ログ解析スクリプト
- cron・systemd タイマー連携
- セキュリティベストプラクティス
- 便利なワンライナー集
- まとめと参考資料
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 -e | set -o errexit | コマンド失敗で即終了 | ✓ |
set -u | set -o nounset | 未定義変数をエラー扱い | ✓ |
set -x | set -o xtrace | 実行コマンドのトレース出力 | デバッグ時のみ |
set -o pipefail | — | パイプライン全体の失敗を検出 | ✓ |
set -n | set -o noexec | コマンドを実行せず構文チェックのみ | 開発時 |
set -f | set -o noglob | グロブ展開を無効化 | 用途に応じて |
set -v | set -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 |
-P | Perl互換正規表現 | 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 タイマーの比較
| 機能 | cron | systemd タイマー |
|---|---|---|
| 設定の簡単さ | シンプル(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 (グロブ・単語分割のリスク) |
| コマンド実行 | 配列による構築 | eval、bash -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 Manual | https://www.gnu.org/software/bash/manual/ | Bash公式リファレンス |
| POSIX Shell Specification | https://pubs.opengroup.org/onlinepubs/9699919799/ | POSIX標準 |
| Advanced Bash-Scripting Guide | https://tldp.org/LDP/abs/html/ | 詳細なBashガイド |
| shellcheck | https://www.shellcheck.net/ | シェルスクリプト静的解析 |
ツール・ユーティリティ
| ツール | 用途 | インストール |
|---|---|---|
| shellcheck | 静的解析 | apt install shellcheck |
| bash-completion | コマンド補完 | apt install bash-completion |
| bats | Bashテストフレームワーク | apt install bats |
| shfmt | コードフォーマッタ | Go binary |
| explainshell | コマンド説明 | https://explainshell.com |
学習リソース
| リソース | 種類 | 推奨レベル |
|---|---|---|
| Linux Command Line (William Shotts) | 書籍 | 入門〜中級 |
| The Linux Documentation Project | Webサイト | 全レベル |
| Greg's Wiki (BashGuide) | Webサイト | 中級〜上級 |
| Wooledge Bash FAQ | Webサイト | 中級〜上級 |
| Linux Journal Archive | 雑誌/Web | 上級 |
Bash バージョン別機能一覧
| 機能 | 最小Bashバージョン |
|---|---|
[[ ]] 条件式 | 2.02+ |
連想配列 (declare -A) | 4.0+ |
mapfile / readarray | 4.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つの原則を常に念頭に置いてください:
- 安全性第一:
set -euo pipefailと適切なエラーハンドリングで予期しない動作を防ぐ- 再利用性: 関数化・モジュール化して保守しやすいコードを書く
- 継続的改善: shellcheck などのツールを活用し、コードの品質を維持する
シェルスクリプトは書き始めると奥が深く、常に学びがあります。本ガイドを起点として、実際のシステム管理業務でのスクリプト作成に役立てていただければ幸いです。