はじめに:シェルスクリプトで自動化の世界へ

繰り返し作業を手動で処理することは時間の無駄であり、ミスの原因です。シェルスクリプトを活用すれば、複雑な作業を自動化し、一貫性のあるサーバー管理が可能です。今回はBashシェルスクリプトの基礎を解説します。

1. シェルスクリプトを始める

1.1 最初のスクリプト

#!/bin/bash
# 最初のシェルスクリプト
# ファイル名: hello.sh

echo "Hello, World!"
echo "現在時刻: $(date)"
echo "現在のユーザー: $USER"
echo "現在のディレクトリ: $PWD"

1.2 スクリプトの実行

# スクリプトファイル作成
vim hello.sh

# 実行権限を付与
chmod +x hello.sh

# 実行方法
./hello.sh            # カレントディレクトリから実行
bash hello.sh         # bashで直接実行
/path/to/hello.sh     # 絶対パスで実行

# 実行権限なしで実行
bash hello.sh
sh hello.sh

1.3 Shebangの理解

#!/bin/bash         # Bashシェルを使用
#!/bin/sh           # POSIX互換シェルを使用
#!/usr/bin/env bash # 環境からbashを検索(移植性が高い)
#!/usr/bin/python3  # Pythonスクリプト
#!/usr/bin/perl     # Perlスクリプト

2. 変数

2.1 変数の宣言と使用

#!/bin/bash

# 変数宣言(=の両側にスペースなし)
name="田中"
age=25
readonly PI=3.14159  # 読み取り専用変数

# 変数の使用
echo "名前: $name"
echo "年齢: ${age}歳"  # 中括弧の使用を推奨

# 変数の削除
unset name

# デフォルト値の設定
echo "${undefined_var:-デフォルト}"    # 値がなければデフォルトを出力
echo "${undefined_var:=新値}"          # 値がなければ新値を代入
echo "${name:?変数がありません}"       # 値がなければエラー出力

2.2 特殊変数

#!/bin/bash
# special_vars.sh arg1 arg2 arg3

echo "スクリプト名: $0"
echo "第1引数: $1"
echo "第2引数: $2"
echo "全引数: $@"
echo "全引数(文字列): $*"
echo "引数の数: $#"
echo "現在のプロセスID: $$"
echo "最後のバックグラウンドプロセスID: $!"
echo "最後のコマンド終了コード: $?"

2.3 環境変数

#!/bin/bash

# 主要な環境変数
echo "ホームディレクトリ: $HOME"
echo "パス: $PATH"
echo "現在のシェル: $SHELL"
echo "ユーザー名: $USER"
echo "ホスト名: $HOSTNAME"
echo "ターミナル種類: $TERM"

# 環境変数の設定
export MY_VAR="my value"

# すべての環境変数を確認
env
printenv

2.4 配列

#!/bin/bash

# 配列宣言
fruits=("りんご" "バナナ" "オレンジ" "ぶどう")

# 配列アクセス
echo "最初: ${fruits[0]}"
echo "3番目: ${fruits[2]}"
echo "全要素: ${fruits[@]}"
echo "配列長: ${#fruits[@]}"

# 配列修正
fruits[1]="マンゴー"
fruits+=("キウイ")  # 要素追加

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

# 連想配列(Bash 4以降)
declare -A person
person[name]="田中"
person[age]=25
person[city]="東京"

echo "名前: ${person[name]}"
echo "全キー: ${!person[@]}"

3. 演算子

3.1 算術演算

#!/bin/bash

a=10
b=3

# 算術演算の方法
echo "加算: $((a + b))"
echo "減算: $((a - b))"
echo "乗算: $((a * b))"
echo "除算: $((a / b))"
echo "剰余: $((a % b))"
echo "累乗: $((a ** 2))"

# letコマンド
let "c = a + b"
echo "let結果: $c"

# exprコマンド(スペース必須)
result=$(expr $a + $b)
echo "expr結果: $result"

# インクリメント演算子
((a++))
echo "増加後: $a"

# 小数点計算(bc使用)
result=$(echo "scale=2; 10 / 3" | bc)
echo "小数点計算: $result"

3.2 文字列操作

#!/bin/bash

str="Hello, World!"

# 文字列長
echo "長さ: ${#str}"

# 部分文字列
echo "部分文字列: ${str:0:5}"    # Hello
echo "後ろ5文字: ${str: -5}"     # orld!

# 文字列置換
echo "置換: ${str/World/Japan}"  # 最初のみ
echo "全置換: ${str//o/O}"       # 全てのoをOに

# 文字列削除
filename="document.txt.bak"
echo "拡張子削除: ${filename%.*}"       # document.txt
echo "全拡張子削除: ${filename%%.*}"    # document
echo "パス削除: ${filename#*/}"
echo "プレフィックス削除: ${filename##*.}"  # bak

# 大文字小文字変換(Bash 4以降)
text="Hello World"
echo "大文字: ${text^^}"
echo "小文字: ${text,,}"

4. 条件分岐

4.1 if文

#!/bin/bash

age=20

# 基本if文
if [ $age -ge 18 ]; then
    echo "成人です"
fi

# if-else
if [ $age -ge 18 ]; then
    echo "成人です"
else
    echo "未成年です"
fi

# if-elif-else
if [ $age -lt 13 ]; then
    echo "子供です"
elif [ $age -lt 20 ]; then
    echo "ティーンエイジャーです"
else
    echo "成人です"
fi

# [[ ]]使用(より多くの機能、Bash専用)
name="田中"
if [[ $name == "田中" && $age -ge 18 ]]; then
    echo "成人の田中さんです"
fi

4.2 比較演算子

#!/bin/bash

# 数値比較
a=10
b=20

[ $a -eq $b ]  # 等しい(equal)
[ $a -ne $b ]  # 等しくない(not equal)
[ $a -lt $b ]  # より小さい(less than)
[ $a -le $b ]  # 以下(less or equal)
[ $a -gt $b ]  # より大きい(greater than)
[ $a -ge $b ]  # 以上(greater or equal)

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

[ "$str1" = "$str2" ]   # 等しい
[ "$str1" != "$str2" ]  # 等しくない
[ -z "$str1" ]          # 空文字列
[ -n "$str1" ]          # 空でない

# [[ ]]での文字列比較
[[ $str1 == $str2 ]]    # 等しい
[[ $str1 < $str2 ]]     # 辞書順比較
[[ $str1 =~ ^h ]]       # 正規表現マッチ

4.3 ファイルテスト

#!/bin/bash

file="/etc/passwd"
dir="/home"

# ファイル存在と種類
[ -e $file ]  # 存在する
[ -f $file ]  # 通常ファイル
[ -d $dir ]   # ディレクトリ
[ -L $file ]  # シンボリックリンク
[ -b $file ]  # ブロックデバイス
[ -c $file ]  # キャラクタデバイス
[ -S $file ]  # ソケット

# ファイル権限
[ -r $file ]  # 読み取り可能
[ -w $file ]  # 書き込み可能
[ -x $file ]  # 実行可能

# ファイル属性
[ -s $file ]  # サイズが0でない
[ -O $file ]  # 所有者が現在のユーザー
[ -G $file ]  # グループが現在のグループ

# ファイル比較
[ $file1 -nt $file2 ]  # より新しい(newer than)
[ $file1 -ot $file2 ]  # より古い(older than)
[ $file1 -ef $file2 ]  # 同じファイル(同じinode)

# 例
if [ -f "/etc/passwd" ] && [ -r "/etc/passwd" ]; then
    echo "passwdファイルが存在し、読み取り可能です"
fi

4.4 case文

#!/bin/bash

echo "果物を選択してください(1-4):"
echo "1) りんご"
echo "2) バナナ"
echo "3) オレンジ"
echo "4) 終了"
read choice

case $choice in
    1)
        echo "りんごを選択しました"
        ;;
    2)
        echo "バナナを選択しました"
        ;;
    3)
        echo "オレンジを選択しました"
        ;;
    4)
        echo "終了します"
        exit 0
        ;;
    *)
        echo "無効な選択です"
        ;;
esac

# パターンマッチング例
read -p "ファイル名入力: " filename
case $filename in
    *.txt)
        echo "テキストファイルです"
        ;;
    *.jpg|*.png|*.gif)
        echo "画像ファイルです"
        ;;
    *.sh)
        echo "シェルスクリプトです"
        ;;
    *)
        echo "不明なファイル形式です"
        ;;
esac

5. ループ

5.1 forループ

#!/bin/bash

# リスト反復
for fruit in りんご バナナ オレンジ ぶどう; do
    echo "果物: $fruit"
done

# 範囲反復
for i in {1..5}; do
    echo "数字: $i"
done

# ステップ指定
for i in {0..10..2}; do
    echo "偶数: $i"
done

# Cスタイルforループ
for ((i=0; i<5; i++)); do
    echo "インデックス: $i"
done

# ファイル反復
for file in /var/log/*.log; do
    echo "ログファイル: $file"
done

# コマンド結果反復
for user in $(cat /etc/passwd | cut -d: -f1); do
    echo "ユーザー: $user"
done

5.2 whileループ

#!/bin/bash

# 基本while
count=1
while [ $count -le 5 ]; do
    echo "カウント: $count"
    ((count++))
done

# 無限ループ
while true; do
    read -p "終了するには'q'を入力: " input
    if [ "$input" = "q" ]; then
        break
    fi
done

# ファイル読み込み
while IFS= read -r line; do
    echo "行: $line"
done < /etc/passwd

# パイプと併用
cat /etc/passwd | while read line; do
    echo "$line"
done

# 複数フィールド読み込み
while IFS=: read user pass uid gid desc home shell; do
    echo "ユーザー: $user, UID: $uid, ホーム: $home"
done < /etc/passwd

5.3 untilループ

#!/bin/bash

# until:条件が真になるまで繰り返し
count=1
until [ $count -gt 5 ]; do
    echo "カウント: $count"
    ((count++))
done

# サービス待機例
until nc -z localhost 80 2>/dev/null; do
    echo "Webサーバー起動待機中..."
    sleep 1
done
echo "Webサーバーが起動しました!"

5.4 breakとcontinue

#!/bin/bash

# break:ループ終了
for i in {1..10}; do
    if [ $i -eq 5 ]; then
        echo "5で終了"
        break
    fi
    echo "数字: $i"
done

# continue:次の反復へ
for i in {1..10}; do
    if [ $((i % 2)) -eq 0 ]; then
        continue  # 偶数はスキップ
    fi
    echo "奇数: $i"
done

# ネストループでのbreak
for i in {1..3}; do
    for j in {1..3}; do
        if [ $j -eq 2 ]; then
            break 2  # 外側のループまで終了
        fi
        echo "i=$i, j=$j"
    done
done

6. 関数

6.1 関数の定義と呼び出し

#!/bin/bash

# 関数定義方法1
function greet {
    echo "こんにちは!"
}

# 関数定義方法2(推奨)
greet2() {
    echo "ようこそ!"
}

# 関数呼び出し
greet
greet2

6.2 関数引数

#!/bin/bash

# 引数を受け取る関数
print_info() {
    echo "名前: $1"
    echo "年齢: $2"
    echo "全引数: $@"
    echo "引数の数: $#"
}

print_info "田中" 25

# デフォルト値設定
greet() {
    local name=${1:-"ゲスト"}
    echo "こんにちは、${name}さん!"
}

greet "田中"
greet

6.3 戻り値

#!/bin/bash

# returnで終了コードを返す(0-255)
is_even() {
    if [ $(($1 % 2)) -eq 0 ]; then
        return 0  # 成功/真
    else
        return 1  # 失敗/偽
    fi
}

if is_even 4; then
    echo "4は偶数です"
fi

# 値を返す(echo使用)
get_double() {
    echo $(($1 * 2))
}

result=$(get_double 5)
echo "5の2倍: $result"

# 複数の値を返す
get_stats() {
    local sum=$(($1 + $2))
    local diff=$(($1 - $2))
    echo "$sum $diff"
}

read sum diff <<< $(get_stats 10 3)
echo "合計: $sum, 差: $diff"

6.4 ローカル変数

#!/bin/bash

global_var="グローバル"

test_scope() {
    local local_var="ローカル"
    global_var="関数で変更"

    echo "関数内ローカル変数: $local_var"
    echo "関数内グローバル変数: $global_var"
}

test_scope

echo "関数外グローバル変数: $global_var"
echo "関数外ローカル変数: $local_var"  # 空

7. 入出力

7.1 ユーザー入力

#!/bin/bash

# 基本入力
echo "名前を入力してください:"
read name
echo "こんにちは、$nameさん!"

# プロンプト付き
read -p "年齢を入力してください: " age

# パスワード入力(非表示)
read -sp "パスワード: " password
echo ""

# タイムアウト設定
read -t 5 -p "5秒以内に入力してください: " input

# 文字数制限
read -n 1 -p "続行しますか? (y/n): " answer
echo ""

# 配列として読み込み
read -a colors -p "好きな色(スペース区切り): "
echo "最初の色: ${colors[0]}"

7.2 出力

#!/bin/bash

# echo
echo "通常出力"
echo -n "改行なし"
echo -e "タブ\t改行\n特殊文字"

# printf(フォーマット指定)
printf "名前: %s, 年齢: %d\n" "田中" 25
printf "小数点: %.2f\n" 3.14159
printf "%-10s | %5d\n" "りんご" 100
printf "%-10s | %5d\n" "バナナ" 50

# 色付き出力
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'  # No Color

echo -e "${RED}エラーメッセージ${NC}"
echo -e "${GREEN}成功メッセージ${NC}"

7.3 リダイレクション

#!/bin/bash

# 標準出力リダイレクション
echo "ログ内容" > logfile.txt     # 上書き
echo "追加内容" >> logfile.txt    # 追記

# 標準エラーリダイレクション
command 2> error.log              # エラーのみ
command > output.log 2>&1         # 出力とエラー両方
command &> all.log                # 出力とエラー両方(Bash)

# 標準入力リダイレクション
while read line; do
    echo "$line"
done < input.txt

# Here Document
cat << EOF
複数行の
テキストを
一度に出力します。
変数置換: $USER
EOF

# Here String
grep "pattern" <<< "検索する文字列"

# パイプ
cat /etc/passwd | grep "root" | cut -d: -f1

8. コマンド実行と置換

8.1 コマンド置換

#!/bin/bash

# $()使用(推奨)
current_date=$(date)
file_count=$(ls -1 | wc -l)

# バッククォート使用(旧式)
current_date=`date`

# ネスト可能
inner=$(echo "Hello $(whoami)")

echo "現在の日付: $current_date"
echo "ファイル数: $file_count"

8.2 コマンド実行結果の確認

#!/bin/bash

# 終了コード確認
ls /nonexistent 2>/dev/null
if [ $? -eq 0 ]; then
    echo "成功"
else
    echo "失敗"
fi

# 条件文で直接使用
if grep -q "root" /etc/passwd; then
    echo "rootユーザーが存在します"
fi

# && と ||
mkdir test_dir && echo "ディレクトリ作成成功"
rm nonexistent 2>/dev/null || echo "ファイルがありません"

# コマンドグループ
{ echo "1行目"; echo "2行目"; } > output.txt

9. 実用的なスクリプト例

9.1 システム情報スクリプト

#!/bin/bash
# system_info.sh - システム情報出力

echo "========================================"
echo "        システム情報レポート"
echo "========================================"
echo ""

echo "ホスト名: $(hostname)"
echo "OS: $(uname -o)"
echo "カーネル: $(uname -r)"
echo "アーキテクチャ: $(uname -m)"
echo ""

echo "--- CPU情報 ---"
echo "CPU: $(grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2)"
echo "コア数: $(nproc)"
echo ""

echo "--- メモリ情報 ---"
free -h | grep -E "Mem|Swap"
echo ""

echo "--- ディスク使用量 ---"
df -h | grep -E "^/dev"
echo ""

echo "--- ネットワーク情報 ---"
ip -4 addr show | grep inet | awk '{print $2}'
echo ""

echo "--- システム稼働時間 ---"
uptime
echo ""

echo "========================================"

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

#!/bin/bash
# backup.sh - シンプルなバックアップスクリプト

# 設定
SOURCE_DIR="/home/$USER/documents"
BACKUP_DIR="/home/$USER/backups"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="backup_${DATE}.tar.gz"

# バックアップディレクトリ作成
mkdir -p "$BACKUP_DIR"

# バックアップ実行
echo "バックアップ開始: $SOURCE_DIR"
if tar -czf "${BACKUP_DIR}/${BACKUP_FILE}" -C "$(dirname $SOURCE_DIR)" "$(basename $SOURCE_DIR)" 2>/dev/null; then
    echo "バックアップ完了: ${BACKUP_DIR}/${BACKUP_FILE}"
    echo "ファイルサイズ: $(du -h ${BACKUP_DIR}/${BACKUP_FILE} | cut -f1)"
else
    echo "バックアップ失敗!"
    exit 1
fi

# 古いバックアップ削除(7日以上)
echo ""
echo "古いバックアップを整理中..."
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +7 -delete
echo "完了!"

9.3 ログ監視スクリプト

#!/bin/bash
# log_monitor.sh - ログファイル監視

LOG_FILE="${1:-/var/log/syslog}"
KEYWORD="${2:-error}"

if [ ! -f "$LOG_FILE" ]; then
    echo "ファイルが存在しません: $LOG_FILE"
    exit 1
fi

echo "ログ監視開始: $LOG_FILE"
echo "キーワード: $KEYWORD"
echo "終了: Ctrl+C"
echo "----------------------------"

tail -f "$LOG_FILE" | while read line; do
    if echo "$line" | grep -qi "$KEYWORD"; then
        echo "[$(date '+%H:%M:%S')] $line"
    fi
done

10. デバッグ

10.1 デバッグモード

#!/bin/bash

# スクリプト全体をデバッグ
bash -x script.sh

# スクリプト内でデバッグ有効化
set -x  # デバッグ開始
# コマンド群...
set +x  # デバッグ終了

# 特定部分のみデバッグ
debug() {
    if [ "$DEBUG" = "1" ]; then
        echo "[DEBUG] $*" >&2
    fi
}

DEBUG=1
debug "これはデバッグメッセージです"

10.2 エラー処理

#!/bin/bash

# エラー発生時に即座に終了
set -e

# 未定義変数使用時にエラー
set -u

# パイプエラー検出
set -o pipefail

# 全て設定
set -euo pipefail

# エラーハンドラー
trap 'echo "エラー発生: 行 $LINENO"; exit 1' ERR

# 終了時の整理
cleanup() {
    echo "クリーンアップ実行..."
    rm -f /tmp/tempfile
}
trap cleanup EXIT

まとめ

シェルスクリプトの基礎を学びました。変数、条件分岐、ループ、関数を組み合わせて、様々な自動化スクリプトを作成できます。次回は、正規表現、テキスト処理、高度なパターンなどシェルスクリプト上級テクニックを解説します。