第5章: シェルスクリプト入門
🎯 この章の目標
- シェルスクリプトの基本構造を理解する
- 変数と条件分岐を使える
- 簡単な自動化スクリプトを作れる
🚀 できるようになること
- 繰り返し作業を自動化できる
- バックアップスクリプトを作れる
- システム管理が効率化できる
はじめに:シェルスクリプトで作業を自動化する
毎日同じコマンドを打つのは面倒ですよね? シェルスクリプトを使えば、複数のコマンドを「レシピ」として保存し、1回の実行で全部終わらせることができます。
🤖 シェルスクリプトでできること
- 定期バックアップ: 毎日自動でファイルを保存
- ログ監視: エラーを見つけたらメール通知
- 環境構築: 新しいPCでも1コマンドで環境完成
- データ処理: 大量のファイルを一括処理
5.1 シェルスクリプトとは
プログラミング未経験でも大丈夫!
シェルスクリプトは「コマンドの順番を書いたメモ」です。 料理のレシピと同じように、上から順番に実行されます。
最初のスクリプト(Hello World)
# 1. スクリプトファイルを作る(.shは慣例)
vi hello.sh
# 2. 以下を書く(コピペOK)
#!/bin/bash
# ↑これは「おまじない」(bashで実行する宣言)
echo "Hello, WSL2!"
echo "今日は $(date) です"
echo "あなたは $USER さんですね"
# 3. 実行権限を付ける(重要!)
chmod +x hello.sh
# 4. 実行する
./hello.sh
💡 なぜ ./
が必要?
- セキュリティのため、現在のフォルダは実行対象外
./
で「このフォルダの」という意味になる
シェバン(Shebang)って何?
#!/bin/bash # bashで実行(一番よく使う)
#!/bin/sh # 古いシェルでも動く(互換性重視)
#!/usr/bin/env python3 # Pythonスクリプトの場合
最初の #!
は「このファイルを何で実行するか」を指定する記号です。
5.2 変数と基本構文
変数の定義と使用
#!/bin/bash
# 変数定義(=の前後にスペース不可)
NAME="Linux"
VERSION=24.04
COUNT=0
# 変数参照
echo "OS: $NAME"
echo "Version: ${VERSION}" # 推奨形式
# 変数の連結
FULL_NAME="${NAME}_${VERSION}"
echo "Full: $FULL_NAME"
# 算術演算
COUNT=$((COUNT + 1))
RESULT=$((10 * 5))
echo "Result: $RESULT"
# コマンド実行結果を変数に格納
CURRENT_DATE=$(date +%Y%m%d)
USER_COUNT=$(who | wc -l)
特殊変数
#!/bin/bash
# special_vars.sh - 特殊変数の確認
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"
echo "Process ID: $$"
echo "Last command exit status: $?"
# 実行例:
# ./special_vars.sh arg1 arg2 arg3
配列
#!/bin/bash
# 配列定義
SERVERS=("web01" "web02" "db01" "db02")
# 要素アクセス
echo "First server: ${SERVERS[0]}"
echo "All servers: ${SERVERS[@]}"
echo "Number of servers: ${#SERVERS[@]}"
# 要素追加
SERVERS+=("cache01")
# ループ処理
for server in "${SERVERS[@]}"; do
echo "Checking: $server"
done
5.3 条件分岐
if文
#!/bin/bash
# 基本形
if [ condition ]; then
commands
elif [ another_condition ]; then
other_commands
else
default_commands
fi
# 数値比較
NUMBER=10
if [ $NUMBER -eq 10 ]; then
echo "Number is 10"
elif [ $NUMBER -gt 10 ]; then
echo "Number is greater than 10"
else
echo "Number is less than 10"
fi
# 文字列比較
NAME="admin"
if [ "$NAME" = "admin" ]; then
echo "Admin user"
fi
if [ -z "$NAME" ]; then
echo "Name is empty"
fi
if [ -n "$NAME" ]; then
echo "Name is not empty"
fi
ファイル・ディレクトリ判定
#!/bin/bash
FILE="/etc/passwd"
DIR="/home"
# ファイル存在確認
if [ -f "$FILE" ]; then
echo "$FILE exists"
fi
# ディレクトリ存在確認
if [ -d "$DIR" ]; then
echo "$DIR is a directory"
fi
# 読み取り可能確認
if [ -r "$FILE" ]; then
echo "$FILE is readable"
fi
# 書き込み可能確認
if [ -w "$FILE" ]; then
echo "$FILE is writable"
fi
# 実行可能確認
if [ -x "$FILE" ]; then
echo "$FILE is executable"
fi
# ファイルサイズ確認
if [ -s "$FILE" ]; then
echo "$FILE is not empty"
fi
条件演算子一覧
演算子 | 意味 | 使用例 |
---|---|---|
-eq | 等しい(数値) | [ $a -eq $b ] |
-ne | 等しくない(数値) | [ $a -ne $b ] |
-gt | より大きい | [ $a -gt $b ] |
-ge | 以上 | [ $a -ge $b ] |
-lt | より小さい | [ $a -lt $b ] |
-le | 以下 | [ $a -le $b ] |
= | 等しい(文字列) | [ “$a” = “$b” ] |
!= | 等しくない(文字列) | [ “$a” != “$b” ] |
-z | 空文字列 | [ -z “$a” ] |
-n | 空でない | [ -n “$a” ] |
case文
#!/bin/bash
# メニュー選択
echo "Select operation:"
echo "1) Start"
echo "2) Stop"
echo "3) Restart"
echo "4) Status"
read -p "Choice: " CHOICE
case $CHOICE in
1|start)
echo "Starting service..."
;;
2|stop)
echo "Stopping service..."
;;
3|restart)
echo "Restarting service..."
;;
4|status)
echo "Checking status..."
;;
*)
echo "Invalid choice"
exit 1
;;
esac
5.4 ループ処理
forループ
#!/bin/bash
# 基本形
for i in 1 2 3 4 5; do
echo "Number: $i"
done
# 範囲指定
for i in {1..10}; do
echo "Count: $i"
done
# ステップ指定(Bash 4.0以降)
for i in {0..20..5}; do
echo "Step: $i" # 0, 5, 10, 15, 20
done
# ファイル処理
for file in *.txt; do
echo "Processing: $file"
# ファイルが存在しない場合の対処
[ -f "$file" ] || continue
wc -l "$file"
done
# コマンド出力を処理
for user in $(cut -d: -f1 /etc/passwd); do
echo "User: $user"
done
whileループ
#!/bin/bash
# カウンタループ
COUNT=0
while [ $COUNT -lt 5 ]; do
echo "Count: $COUNT"
COUNT=$((COUNT + 1))
done
# ファイル読み込み
while IFS= read -r line; do
echo "Line: $line"
done < input.txt
# 無限ループ
while true; do
echo "Running... (Press Ctrl+C to stop)"
sleep 1
done
# 条件待機
while ! ping -c 1 google.com > /dev/null 2>&1; do
echo "Waiting for network..."
sleep 5
done
echo "Network is up!"
untilループ
#!/bin/bash
# 条件が真になるまで継続
COUNT=0
until [ $COUNT -ge 5 ]; do
echo "Count: $COUNT"
COUNT=$((COUNT + 1))
done
# サービス起動待機
until systemctl is-active nginx > /dev/null 2>&1; do
echo "Waiting for nginx to start..."
sleep 2
done
echo "Nginx is running!"
5.5 関数
関数定義と呼び出し
#!/bin/bash
# 関数定義(方法1)
function greet() {
echo "Hello, $1!"
}
# 関数定義(方法2)
say_goodbye() {
local name=$1 # ローカル変数
echo "Goodbye, $name!"
return 0 # 戻り値(0-255)
}
# 関数呼び出し
greet "World"
say_goodbye "Linux"
# 戻り値確認
check_service() {
local service=$1
if systemctl is-active "$service" > /dev/null 2>&1; then
return 0
else
return 1
fi
}
if check_service nginx; then
echo "Nginx is running"
else
echo "Nginx is not running"
fi
実用的な関数例
#!/bin/bash
# ログ出力関数
log_message() {
local level=$1
shift
local message="$@"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" | tee -a /var/log/script.log
}
# エラーハンドリング関数
error_exit() {
log_message "ERROR" "$1"
exit 1
}
# 使用例
log_message "INFO" "Script started"
[ -f config.txt ] || error_exit "Config file not found"
log_message "INFO" "Script completed"
# バックアップ関数
backup_file() {
local source=$1
local backup="${source}.$(date +%Y%m%d_%H%M%S).bak"
if [ -f "$source" ]; then
cp "$source" "$backup"
log_message "INFO" "Backed up $source to $backup"
return 0
else
log_message "ERROR" "Source file $source not found"
return 1
fi
}
5.6 入出力とリダイレクト
標準入出力
#!/bin/bash
# ユーザー入力
read -p "Enter your name: " NAME
echo "Hello, $NAME"
# パスワード入力(非表示)
read -s -p "Enter password: " PASSWORD
echo # 改行
echo "Password length: ${#PASSWORD}"
# タイムアウト付き入力
if read -t 5 -p "Continue? (5 sec timeout) [y/n]: " ANSWER; then
echo "Answer: $ANSWER"
else
echo "Timeout!"
fi
# デフォルト値
read -p "Port number [8080]: " PORT
PORT=${PORT:-8080}
echo "Using port: $PORT"
リダイレクトとパイプ
#!/bin/bash
# 出力リダイレクト
echo "Log message" > output.txt # 上書き
echo "Another log" >> output.txt # 追記
# エラー出力リダイレクト
command 2> error.log # エラーのみ
command > output.log 2>&1 # 両方を同じファイルへ
command &> all.log # 簡略記法
# 入力リダイレクト
while read line; do
echo "Processing: $line"
done < input.txt
# ヒアドキュメント
cat << EOF > config.txt
server_name=localhost
port=8080
debug=true
EOF
# コマンド置換
FILES=$(ls *.txt)
LINE_COUNT=$(wc -l < file.txt)
# パイプライン処理
cat access.log | grep ERROR | sort | uniq -c | sort -rn > error_summary.txt
5.7 エラーハンドリング
終了ステータス
#!/bin/bash
# コマンド実行と結果確認
cp source.txt dest.txt
if [ $? -eq 0 ]; then
echo "Copy successful"
else
echo "Copy failed"
exit 1
fi
# より簡潔な書き方
if cp source.txt dest.txt; then
echo "Copy successful"
else
echo "Copy failed"
exit 1
fi
# &&と||の使用
mkdir /tmp/test && echo "Directory created" || echo "Failed to create directory"
set オプション
#!/bin/bash
# エラー時に即座に終了
set -e
# 未定義変数の使用でエラー
set -u
# パイプライン内のエラーも検出
set -o pipefail
# デバッグモード
set -x # 実行コマンドを表示
# 組み合わせ
set -euo pipefail
trapによるクリーンアップ
#!/bin/bash
# 一時ファイル
TEMP_FILE=$(mktemp)
# 終了時のクリーンアップ
cleanup() {
echo "Cleaning up..."
rm -f "$TEMP_FILE"
}
trap cleanup EXIT
# エラー時の処理
error_handler() {
echo "Error occurred on line $1"
exit 1
}
trap 'error_handler $LINENO' ERR
# メイン処理
echo "Processing..." > "$TEMP_FILE"
# 処理実行
5.8 実践スクリプト例
システム監視スクリプト
#!/bin/bash
# system_monitor.sh - システムリソース監視
set -euo pipefail
# 設定
THRESHOLD_CPU=80
THRESHOLD_MEM=90
THRESHOLD_DISK=85
LOG_FILE="/var/log/system_monitor.log"
# ログ関数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# CPU使用率チェック
check_cpu() {
local cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print int($2)}')
if [ $cpu_usage -gt $THRESHOLD_CPU ]; then
log "WARNING: High CPU usage: ${cpu_usage}%"
return 1
fi
return 0
}
# メモリ使用率チェック
check_memory() {
local mem_usage=$(free | grep Mem | awk '{print int($3/$2 * 100)}')
if [ $mem_usage -gt $THRESHOLD_MEM ]; then
log "WARNING: High memory usage: ${mem_usage}%"
return 1
fi
return 0
}
# ディスク使用率チェック
check_disk() {
local disk_usage=$(df -h / | tail -1 | awk '{print int($5)}')
if [ $disk_usage -gt $THRESHOLD_DISK ]; then
log "WARNING: High disk usage: ${disk_usage}%"
return 1
fi
return 0
}
# メイン処理
main() {
log "Starting system monitoring"
local alert=0
check_cpu || alert=1
check_memory || alert=1
check_disk || alert=1
if [ $alert -eq 1 ]; then
log "ALERT: System resources need attention"
# アラート送信(メール、Slack等)
else
log "INFO: All systems normal"
fi
}
# 実行
main
バックアップスクリプト
#!/bin/bash
# backup.sh - 増分バックアップスクリプト
set -euo pipefail
# 設定
SOURCE_DIR="/home/user/data"
BACKUP_DIR="/backup"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_${TIMESTAMP}"
RETENTION_DAYS=7
# バックアップ実行
perform_backup() {
local full_backup_dir="${BACKUP_DIR}/${BACKUP_NAME}"
# ディレクトリ作成
mkdir -p "$full_backup_dir"
# rsyncで増分バックアップ
if [ -L "${BACKUP_DIR}/latest" ]; then
# 前回のバックアップがある場合は増分
rsync -av --link-dest="${BACKUP_DIR}/latest" \
"$SOURCE_DIR/" "$full_backup_dir/"
else
# 初回は完全バックアップ
rsync -av "$SOURCE_DIR/" "$full_backup_dir/"
fi
# 最新バックアップへのシンボリックリンク更新
rm -f "${BACKUP_DIR}/latest"
ln -s "$full_backup_dir" "${BACKUP_DIR}/latest"
echo "Backup completed: $full_backup_dir"
}
# 古いバックアップ削除
cleanup_old_backups() {
find "$BACKUP_DIR" -maxdepth 1 -type d -name "backup_*" \
-mtime +$RETENTION_DAYS -exec rm -rf {} \;
echo "Old backups cleaned up"
}
# メイン処理
main() {
echo "Starting backup at $(date)"
# ソースディレクトリ確認
[ -d "$SOURCE_DIR" ] || { echo "Source directory not found"; exit 1; }
# バックアップディレクトリ作成
mkdir -p "$BACKUP_DIR"
# バックアップ実行
perform_backup
# クリーンアップ
cleanup_old_backups
echo "Backup process completed at $(date)"
}
# 実行
main
5.9 cron による定期実行
crontab 基本
# crontab編集
crontab -e
# crontab確認
crontab -l
# crontab削除
crontab -r
cron記法
# 分 時 日 月 曜日 コマンド
# (0-59) (0-23) (1-31) (1-12) (0-7, 0と7は日曜)
# 毎日午前2時に実行
0 2 * * * /home/user/backup.sh
# 平日の午前9時に実行
0 9 * * 1-5 /home/user/weekday_task.sh
# 5分ごとに実行
*/5 * * * * /home/user/monitor.sh
# 毎月1日の午前3時に実行
0 3 1 * * /home/user/monthly_report.sh
WSL2でのcron設定
# cron起動(systemd有効時)
sudo systemctl start cron
sudo systemctl enable cron
# cron起動(systemd無効時)
sudo service cron start
# 自動起動設定(.bashrcに追加)
echo 'sudo service cron start 2>/dev/null' >> ~/.bashrc
5.10 演習問題
演習1: ログローテーションスクリプト
#!/bin/bash
# log_rotate.sh - ログファイルローテーション
LOG_DIR="/var/log/myapp"
MAX_SIZE=10485760 # 10MB
MAX_FILES=5
rotate_log() {
local log_file=$1
local base_name=$(basename "$log_file")
# サイズチェック
if [ -f "$log_file" ] && [ $(stat -c%s "$log_file") -gt $MAX_SIZE ]; then
# 既存のローテーションファイルを移動
for i in $(seq $((MAX_FILES-1)) -1 1); do
[ -f "${log_file}.${i}" ] && mv "${log_file}.${i}" "${log_file}.$((i+1))"
done
# 現在のログをローテート
mv "$log_file" "${log_file}.1"
# 最古のファイルを削除
[ -f "${log_file}.${MAX_FILES}" ] && rm "${log_file}.${MAX_FILES}"
echo "Rotated: $log_file"
fi
}
# メイン処理
for log_file in "$LOG_DIR"/*.log; do
[ -f "$log_file" ] && rotate_log "$log_file"
done
演習2: ユーザー管理スクリプト
#!/bin/bash
# user_manager.sh - ユーザー一括管理
USER_LIST="users.txt" # username,fullname,group
create_users() {
while IFS=',' read -r username fullname group; do
# ユーザー存在チェック
if id "$username" &>/dev/null; then
echo "User $username already exists"
else
# グループ作成
groupadd "$group" 2>/dev/null
# ユーザー作成
useradd -m -g "$group" -c "$fullname" "$username"
# 初期パスワード設定
echo "${username}:password123" | chpasswd
# パスワード変更強制
passwd -e "$username"
echo "Created user: $username"
fi
done < "$USER_LIST"
}
# 実行(要root権限)
[ $EUID -eq 0 ] || { echo "Run as root"; exit 1; }
create_users
演習3: デプロイスクリプト
#!/bin/bash
# deploy.sh - アプリケーションデプロイ
set -euo pipefail
# 設定
APP_DIR="/var/www/myapp"
GIT_REPO="https://github.com/user/myapp.git"
BRANCH="main"
BACKUP_DIR="/var/backups/myapp"
# デプロイ前フック
pre_deploy() {
echo "Running pre-deploy tasks..."
# バックアップ作成
if [ -d "$APP_DIR" ]; then
backup_name="backup_$(date +%Y%m%d_%H%M%S).tar.gz"
tar -czf "${BACKUP_DIR}/${backup_name}" -C "$APP_DIR" .
echo "Backup created: ${backup_name}"
fi
# サービス停止
sudo systemctl stop myapp || true
}
# デプロイ実行
deploy() {
echo "Deploying application..."
# 最新コード取得
if [ -d "$APP_DIR/.git" ]; then
cd "$APP_DIR"
git fetch origin
git reset --hard "origin/${BRANCH}"
else
git clone -b "$BRANCH" "$GIT_REPO" "$APP_DIR"
cd "$APP_DIR"
fi
# 依存関係インストール
npm install --production
# アセットビルド
npm run build
# 権限設定
chown -R www-data:www-data "$APP_DIR"
}
# デプロイ後フック
post_deploy() {
echo "Running post-deploy tasks..."
# データベースマイグレーション
npm run migrate
# キャッシュクリア
npm run cache:clear
# サービス起動
sudo systemctl start myapp
# ヘルスチェック
sleep 5
if curl -f http://localhost:3000/health > /dev/null 2>&1; then
echo "Deployment successful!"
else
echo "Health check failed!"
exit 1
fi
}
# メイン処理
main() {
echo "Starting deployment at $(date)"
pre_deploy
deploy
post_deploy
echo "Deployment completed at $(date)"
}
# 実行
main
まとめ
シェルスクリプトの習得ポイント:
- 基本構文: 変数、条件分岐、ループを確実に理解
- エラーハンドリング: set -euo pipefail とtrapの活用
- 実践的活用: 日常業務の自動化から始める
これらの基礎を基に、インフラ運用の自動化を段階的に進めていきます。