Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bash 解説書

Linux や macOS、WSL をはじめとする Unix 系環境の標準シェルである Bash(Bourne Again Shell)の基礎文法からパイプライン、シェルスクリプト開発、テキスト処理、安全な運用ベストプラクティスまでを体系的に解説する手引きです。

章構成

入門編:Bashの概要とシェル環境

Bash の基本概念、シェルの役割と実行モード、初期化ファイルの読み込み順序、および実行環境の確認方法を解説します。

Bashとは何か

Bash(Bourne Again Shell)は、GNU プロジェクトによって開発されたオープンソースのコマンドラインシェルおよびスクリプト言語です。1979 年に開発された Unix の標準シェル「Bourne Shell(sh)」の後継として誕生し、POSIX 標準との互換性を保ちながら強力な拡張機能を備えています。

Linux ディストリビューション(Ubuntu、Debian、RHEL 等)の多くで既定のログインシェルとして採用されているほか、WSL(Windows Subsystem for Linux)や macOS、コンテナ環境(Docker)など、現代のインフラ管理や開発運用において世界中で最も広く利用されています。

シェルは、OS の中核である「カーネル」とユーザーの間に立ち、入力されたコマンドを解釈してプログラムを実行するコマンドインタープリタとしての役割を果たします。同時に、複数のコマンドを組み合わせて処理を自動化するプログラミング言語でもあります。

シェルの実行モード

Bash は動作する状況に応じて複数の実行モードが存在します。

» ログインシェルと非ログインシェル

ユーザーが SSH 接続やコンソール画面から ID とパスワードを入力して認証した際に起動するシェルを「ログインシェル」と呼びます。

一方、グラフィカル環境のターミナルウィンドウを新しく開いたときや、シェル内から別のシェルを起動したときは「非ログインシェル」として動作します。

» 対話的シェルと非対話的シェル

ユーザーがキーボードから直接コマンドを入力し、プロンプトとやり取りするモードを「対話的(インタラクティブ)シェル」と呼びます。

シェルスクリプトファイルを実行する際のように、事前に記述されたコマンド群を自動で順次実行するモードを「非対話的シェル」と呼びます。

設定ファイル(初期化スクリプト)の読み込み順序

Bash の起動時には、モードに応じて特定の設定ファイルが順番に読み込まれます。

» ログインシェルの場合

  1. /etc/profile(システム全体の共通設定)
  2. ~/.bash_profile、~/.bash_login、~/.profile の中から 最初に見つかった1つだけ を読み込む
  3. ログアウト時には ~/.bash_logout を実行

一般的には ~/.bash_profile の中で ~/.bashrc を呼び出すように記述し、設定の重複を防ぎます。

» 対話的非ログインシェルの場合

  1. /etc/bash.bashrc(システム全体の設定)
  2. ~/.bashrc(ユーザー個別の環境設定)

エイリアスやプロンプト設定(PS1)、日常的に使う関数は ~/.bashrc に記述するのが標準的な運用です。

バージョンの確認と環境整備

システムにインストールされている Bash のバージョンを確認します。

# バージョン情報の表示
bash --version

# 組み込み変数によるバージョン確認
echo "$BASH_VERSION"

macOS ではライセンス上の理由から、OS 標準の Bash が古いバージョン(3.2 系)に留められています。最新の連想配列機能やパフォーマンス改善(Bash 4.0 / 5.0 以降の機能)を利用したい場合は、Homebrew などのパッケージマネージャー経由で最新版をインストールして使用します。

# macOS での Homebrew による導入例
brew install bash

入門編:基本コマンドとコマンドライン操作

Unix 環境で必須となるファイル操作・権限管理の基本コマンド、ターミナル作業を加速する Readline キーバインド、およびコマンド履歴の活用法を解説します。

ファイルとディレクトリの基本コマンド

日常的に利用する代表的なコマンド群とその役割です。

» 移動と現在位置の確認

  • pwd: 現在作業しているディレクトリの絶対パスを表示(Print Working Directory)
  • cd <path>: 指定したディレクトリへ移動(引数なしで実行するとホームディレクトリ ~ へ移動)
  • cd -: 直前にいたディレクトリへ戻る

» ファイル・ディレクトリの一覧と操作

# 詳細情報と隠しファイル(.で始まるファイル)を表示
ls -la

# ディレクトリの再帰的作成(親ディレクトリも同時に作成)
mkdir -p project/src/utils

# 空ファイルの作成またはタイムスタンプの更新
touch README.md

# ディレクトリごと再帰的にコピー
cp -r project/ backup/

# ファイルの移動または名前変更
mv old_name.txt new_name.txt

# ファイルやディレクトリの強制削除
rm -rf temp_dir/

パーミッション(権限)の管理

Unix 系 OS では、すべてのファイルとディレクトリに対して「誰がどのような操作を行えるか」というアクセス権(パーミッション)が設定されています。

ls -l を実行した際、先頭に表示される10文字の記号(例: -rwxr-xr--)は以下の構成になっています。

  • 1文字目: 種別(- は通常ファイル、d はディレクトリ)
  • 2〜4文字目: 所有者(Owner)の権限
  • 5〜7文字目: 所有グループ(Group)の権限
  • 8〜10文字目: その他のユーザー(Others)の権限

権限は r(読み取り: 4)、w(書き込み: 2)、x(実行: 1) の3種類の合計値(8進数)で表されます。

# スクリプトに実行権限を付与する
chmod +x script.sh

# 権限を 755(所有者:全権限、他:読み取りと実行のみ)に設定
chmod 755 script.sh

# 所有者を変更(管理者権限が必要)
sudo chown user:group script.sh

Readline キーバインド(コマンドライン編集)

Bash の入力行編集ライブラリ(GNU Readline)には、作業効率を高めるキーボードショートカットが多数備わっています。

キー操作機能
Ctrl + Aカーソルを行の先頭へ移動
Ctrl + Eカーソルを行の末尾へ移動
Ctrl + Uカーソル位置から行頭までを削除(切り取り)
Ctrl + Kカーソル位置から行末までを削除(切り取り)
Ctrl + Wカーソル手前の1単語を削除
Ctrl + Y削除した文字列を貼り付け(ヤンク)
Ctrl + L画面をクリア(clear コマンドと同等)
Ctrl + C実行中のコマンドを中断

コマンド履歴(ヒストリ)の活用

過去に入力したコマンドを呼び出して再利用する機能です。

» インクリメンタル逆方向検索(Ctrl + R)

ターミナルで Ctrl + R を押してキーワードを入力すると、過去に入力したコマンド履歴の中から前方一致・部分一致で即座に検索して呼び出すことができます。続けて Ctrl + R を押すとさらに過去の履歴へと遡ります。

» ヒストリ展開の便利構文

# 直前に実行したコマンドを再度実行
!!

# 直前に実行したコマンドを sudo 付きで再実行
sudo !!

# 直前のコマンドの最後の引数を再利用(例: mkdir dir ➔ cd !$)
!$

入門編:パイプライン・リダイレクト・プロセス間通信

標準入出力の概念、ファイルや別ストリームへ出力を切り替えるリダイレクト、コマンド同士を接続するパイプライン、およびコマンド置換の仕組みを解説します。

標準入出力とファイルディスクリプタ

Unix 系 OS のプロセスは、実行時に3つの標準的な入出力ストリーム(ファイルディスクリプタ: FD)を自動的に開きます。

略称名称FD 番号既定の接続先
stdin標準入力0キーボード
stdout標準出力1ターミナル画面
stderr標準エラー出力2ターミナル画面

通常の実行結果(正常系)は stdout に、警告やエラーメッセージ(異常系)は stderr に出力されるよう設計されています。

リダイレクト構文(入出力の切り替え)

出力先をファイルに変更したり、ファイルからデータを読み込んだりする構文です。

» 出力のリダイレクト

# 標準出力をファイルに書き込む(上書き)
echo "Hello, World" > output.txt

# 標準出力をファイルの末尾に追記する
echo "New line" >> output.txt

# エラー出力のみをログファイルに保存する
ls /non_existent_path 2> error.log

# 標準出力と標準エラー出力の両方を同じファイルに保存する
make build > build.log 2>&1

# 不要なメッセージ(正常・エラーとも)を完全に破棄する
command_to_run > /dev/null 2>&1

» 入力のリダイレクトとヒアドキュメント

# ファイルの内容を標準入力として渡す
sort < unsorted.txt

# 複数行のテキストを直接渡す(ヒアドキュメント)
cat <<EOF > config.yaml
server:
  host: 127.0.0.1
  port: 8080
EOF

# 単一の文字列を標準入力として渡す(ヒアストリング)
grep "error" <<< "system info: unexpected error occurred"

パイプライン(|)と tee コマンド

パイプライン演算子(|)を使用すると、左側のコマンドの標準出力(stdout)を、右側のコマンドの標準入力(stdin)に直接渡すことができます。

# 実行中プロセス一覧から node を含む行を検索し、行数をカウントする
ps aux | grep "[n]ode" | wc -l

» tee コマンド(分岐)

パイプラインの途中で出力を画面に表示しつつ、同時にファイルへも保存したい場合は tee コマンドを挟みます。

# ビルドログを画面で見ながらファイルにも記録
./build.sh | tee build.log

コマンド置換とプロセス置換

コマンドの実行結果を変数に代入したり、ファイルのように扱ったりする強力な構文です。

» コマンド置換($(...))

実行結果の文字列をその場に展開します。古いバッククォート構文(`...`)に比べ、ネストが容易で可読性に優れています。

# 現在日付を変数に格納
CURRENT_DATE=$(date "+%Y-%m-%d")
echo "Backup date: $CURRENT_DATE"

# バックアップアーカイブの作成
tar -czf "backup-$(date +%Y%m%d).tar.gz" ./data

» プロセス置換(<(...))

コマンドの出力を一時的な名前付きパイプとして扱い、ファイルパスを要求するコマンドに直接渡すことができます。

# 2つのコマンドの出力結果の差分を直接比較(一時ファイルの作成が不要)
diff -u <(ls -1 dir_a) <(ls -1 dir_b)

実践編:変数・パラメータ展開・配列

変数の宣言と環境変数のエクスポート、特殊変数、誤動作を防ぐクォーティング規則、高度なパラメータ展開、および配列の操作方法を解説します。

変数の定義とスコープ

Bash では変数名と代入値の間にイコール(=)を記述します。イコールの前後に半角スペースを入れてはいけません(スペースを入れるとコマンド名や引数として誤認されます)。

# 正しい代入
TARGET_ENV="production"
PORT=8080

# 誤り(エラーになります)
# TARGET_ENV = "production"

» シェル変数と環境変数(export)

通常の変数はそのシェルプロセス内でのみ有効な「シェル変数」です。子プロセス(起動したコマンドや別スクリプト)にも変数を引き継ぎたい場合は export コマンドで「環境変数」として宣言します。

export API_KEY="secret_key_12345"

特殊変数(自動設定変数)

Bash がスクリプト実行時やコマンド終了時に自動的に値を設定する組み込み変数です。

変数内容
$0実行中のスクリプト名またはシェルの名前
$1〜$9コマンドライン引数(第1引数〜第9引数)
${10} 以降10番目以降の引数は波括弧で囲む
$#渡されたコマンドライン引数の総数
$@全引数のリスト("$@" で各引数を安全に個別展開)
$?直前のコマンドの終了ステータス(0 は成功、0 以外は失敗)
$$現在のシェルプロセスのプロセス ID(PID)
$!直前にバックグラウンドで起動されたプロセスの PID

クォーティングの原則

Bash スクリプトで最も不具合の原因になりやすいのがクォートの省略です。

  • クォートなし: 空白による単語分割(Word Splitting)やワイルドカード展開(Globbing)が発生します。ファイル名にスペースが含まれているとコマンドが分割され、意図しない挙動になります。
  • シングルクォート('...'): 内部のすべての文字を純粋な文字列として扱います。変数やコマンド置換は一切展開されません。
  • ダブルクォート("..."): 変数展開やコマンド置換を許可しつつ、スペースによる単語分割を防ぎます。

安全なスクリプトを書くための基本ルールとして、変数を展開する際は常にダブルクォートで囲む("$var") ことを徹底します。

パラメータ展開(Parameter Expansion)

波括弧構文 ${...} を用いることで、外部コマンドを呼び出さずにシェル内部だけで高速に文字列加工を行えます。

» 既定値の指定と検証

# 変数が未設定または空の場合、指定した既定値を使用する
ENV_NAME="${ENV_TARGET:-development}"

# 変数が未設定の場合、エラーメッセージを表示してスクリプトを中断する
CONFIG_FILE="${APP_CONFIG:?Config path is required}"

» 文字列の切り取りと置換

FILENAME="archive-2026.tar.gz"

# 前方一致削除(最短マッチ: #、最長マッチ: ##)
echo "${FILENAME#*-}"   # 2026.tar.gz
echo "${FILENAME##*.}"  # gz(拡張子の抽出)

# 後方一致削除(最短マッチ: %、最長マッチ: %%)
echo "${FILENAME%.*}"   # archive-2026.tar(拡張子の除去)
echo "${FILENAME%%-*}"  # archive

# 文字列置換(最初の一致: /、すべての一致: //)
TEXT="apple banana apple"
echo "${TEXT/apple/orange}"   # orange banana apple
echo "${TEXT//apple/orange}"  # orange banana orange

配列の操作

複数の要素を順序付けて管理する配列の構文です。

» インデックス配列

# 配列の定義
SERVERS=("web01" "web02" "db01")

# 要素の参照(インデックスは 0 始まり)
echo "${SERVERS[0]}"

# 全要素を安全に展開(for ループ等で使用)
for s in "${SERVERS[@]}"; do
    echo "Connecting to $s..."
done

# 要素数の取得
echo "Count: ${#SERVERS[@]}"

# 末尾に要素を追加
SERVERS+=("db02")

» 連想配列(ハッシュテーブル)

Bash 4.0 以降では、文字列をキーとする連想配列が利用可能です。使用前に declare -A で宣言する必要があります。

# 連想配列の宣言
declare -A PORT_MAP

PORT_MAP["http"]=80
PORT_MAP["https"]=443
PORT_MAP["ssh"]=22

echo "HTTPS port is: ${PORT_MAP["https"]}"

実践編:制御構文と条件分岐・繰り返し

条件判定を行うテスト構文([[ ... ]])、if や case による条件分岐、および for や while を用いた繰り返し処理の設計方法を解説します。

テスト構文(条件判定)

Bash では条件を判定する際に角括弧を使用します。従来の POSIX 標準である [ ... ](test コマンド)に加え、Bash 拡張の [[ ... ]] が利用可能です。

[[ ... ]] は単語分割やワイルドカードの不要な展開を防ぎ、正規表現もサポートするため、Bash スクリプトでは基本的に [[ ... ]] の使用が推奨されます。

» 文字列と数値の比較

Bash では文字列比較と数値比較で異なる演算子を使用します。

判定対象演算子例
文字列が一致==[[ "$name" == "Alice" ]]
文字列が不一致!=[[ "$name" != "Bob" ]]
空文字列(長さ0)-z[[ -z "$input" ]]
非空文字列-n[[ -n "$input" ]]
数値が等しい-eq[[ "$count" -eq 10 ]]
数値が大きい-gt[[ "$size" -gt 100 ]]
数値が小さい-lt[[ "$age" -lt 20 ]]

» ファイルとディレクトリの判定

ファイルシステムの検証で頻出する主要なテスト演算子です。

# 通常ファイルが存在するか確認
if [[ -f "/etc/hosts" ]]; then
    echo "hosts file exists."
fi

# ディレクトリが存在するか確認
if [[ -d "./build" ]]; then
    echo "build dir exists."
fi

# 実行権限があるか確認
if [[ -x "./deploy.sh" ]]; then
    echo "deploy.sh is executable."
fi

» 正規表現マッチング(=~)

=~ 演算子を用いて、入力値が正規表現パターンに合致するかを判定できます。マッチした部分は組み込み配列変数 BASH_REMATCH に格納されます。

INPUT="port:8080"

if [[ "$INPUT" =~ ^port:([0-9]+)$ ]]; then
    PORT_NUM="${BASH_REMATCH[1]}"
    echo "Matched port: $PORT_NUM"
fi

条件分岐(if と case)

状況に応じて異なる処理を実行するための構文です。

» if / elif / else / fi

STATUS=200

if [[ "$STATUS" -eq 200 ]]; then
    echo "Success: OK"
elif [[ "$STATUS" -eq 404 ]]; then
    echo "Error: Not Found"
else
    echo "Unexpected status code: $STATUS"
fi

» case / in / esac

コマンドライン引数の処理や、特定パターンの分類には case 文が適しています。各パターンの終端にはセミコロンを2つ(;;)記述します。

ACTION="$1"

case "$ACTION" in
    start)
        echo "Starting service..."
        ;;
    stop)
        echo "Stopping service..."
        ;;
    restart|reload)
        echo "Restarting service..."
        ;;
    *)
        echo "Usage: $0 {start|stop|restart}" >&2
        exit 1
        ;;
esac

繰り返し構文(for と while)

連続したデータやファイルを順次処理するループ構文です。

» for ループ

配列の要素やワイルドカードで展開されたファイル一覧を走査します。

# 配列の全要素をループ
REGIONS=("ap-northeast-1" "us-west-2" "eu-west-1")

for region in "${REGIONS[@]}"; do
    echo "Checking region: $region"
done

# C 言語スタイルの数値ループ
for ((i = 1; i <= 5; i++)); do
    echo "Step: $i"
done

» while ループとファイルの1行読み込み

条件が真である間、処理を繰り返します。ファイルを1行ずつ安全に読み込む定石パターンとして read -r との組み合わせが使われます。

# ファイルから1行ずつ読み込んで処理
while IFS= read -r line; do
    echo "Processing: $line"
done < "./data.txt"

-r オプションはバックスラッシュのエスケープ解釈を無効化し、IFS= は前後の不要な空白トリムを防止します。

実践編:関数とシェルスクリプト設計

シバンの選択、関数の定義とローカルスコープの保護、getopts によるコマンドライン引数の解析、および堅牢なスクリプト全体の構成設計を解説します。

シバン(Shebang)の正しい記述

シェルスクリプトの1行目には、スクリプトを実行するインタープリタを指定する「シバン(Shebang)」を記述します。

#!/usr/bin/env bash

» シバンの選択指針

  • 推奨: #!/usr/bin/env bash: 環境変数 PATH から bash 実行ファイルを探して起動します。OS ごとにインストール先(/bin/bash や /usr/local/bin/bash 等)が異なっていても移植性を維持できます。
  • 避けるべき: #!/bin/sh: sh は Ubuntu 等では dash(機能が限定された軽量シェル)にリンクされています。[[ ... ]] や配列などの Bash 固有構文(Bashism)が含まれていると構文エラーで失敗します。

関数の定義とローカル変数

再利用可能な処理を関数として整理します。

» 関数の定義方法

# POSIX 互換のシンプルな構文
backup_file() {
    local target="$1"
    local dest_dir="${2:-./backup}"

    if [[ ! -f "$target" ]]; then
        echo "Error: File $target not found." >&2
        return 1
    fi

    mkdir -p "$dest_dir"
    cp "$target" "$dest_dir/"
    return 0
}

# 関数の呼び出し
backup_file "config.json" "/var/backup"

» local 宣言によるスコープ保護

関数内で宣言する変数は、必ず local を先頭に付与します。local を省略するとグローバル変数として扱われ、呼び出し元の同名変数を意図せず上書きしてしまう原因になります。

» 関数の戻り値と出力

  • return <数字>: 0〜255 の終了ステータス(成功か失敗か)のみを返します。
  • 計算結果や文字列データを返したい場合は、関数内で echo や printf で標準出力に出力し、呼び出し側がコマンド置換 $(func) で受け取ります。
get_full_name() {
    local first="$1"
    local last="$2"
    printf "%s %s\n" "$last" "$first"
}

NAME=$(get_full_name "Taro" "Yamada")

引数のパース(getopts)

スクリプトに渡された -v(フラグ)や -f file.txt(値付きオプション)などの引数を標準的な手法で解析するには組み込みの getopts を使用します。

#!/usr/bin/env bash

VERBOSE=false
OUTPUT_FILE=""

while getopts ":vf:h" opt; do
    case "$opt" in
        v)
            VERBOSE=true
            ;;
        f)
            OUTPUT_FILE="$OPTARG"
            ;;
        h)
            echo "Usage: $0 [-v] [-f filename]"
            exit 0
            ;;
        \?)
            echo "Invalid option: -$OPTARG" >&2
            exit 1
            ;;
        :)
            echo "Option -$OPTARG requires an argument." >&2
            exit 1
            ;;
    esac
done

# 解析済みオプションを取り除き、残りの位置引数を整列
shift "$((OPTIND - 1))"

main 関数パターンによるスクリプト構造化

スクリプト全体の見通しを良くし、定義順序による不具合を防ぐため、「main 関数」パターンを採用することが推奨されます。

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

# ヘルパー関数
log_info() {
    echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $*"
}

# メイン処理
main() {
    local input_path="${1:-}"

    if [[ -z "$input_path" ]]; then
        echo "Error: Argument required." >&2
        exit 1
    fi

    log_info "Processing $input_path..."
    # 業務処理
    log_info "Finished successfully."
}

# スクリプトの末尾でエントリーポイントを実行
main "$@"

実践編:テキスト処理ツール(grep, sed, awk)の活用

Unix 環境における3大テキスト処理ツール(grep による検索、sed による置換・編集、awk による列抽出と集計)の特徴と連携手法を解説します。

3大ツールの役割分担

テキストストリームを加工・分析する際、目的に応じて最適なツールを使い分けます。

ツール主な役割代表的な処理
grepパターン検索・抽出特定の単語や正規表現を含む行の絞り込み
sed文字列置換・行編集設定ファイルの書き換え、不要な行の削除
awk列抽出・計算・レポートCSV や TSV の特定カラム抽出、数値の合計・平均

grep によるパターン検索

テキストから条件に合致する行を素早く検索・抽出します。

» 主要なオプション

# 大文字・小文字を区別せずに検索(-i)
grep -i "error" app.log

# 一致しない行を抽出(除外検索: -v)
grep -v "^#" config.conf  # コメント行を除外

# 一致した行数のみをカウント(-c)
grep -c "TIMEOUT" access.log

# 拡張正規表現を使用(-E または egrep)
grep -E "(FATAL|CRITICAL)" system.log

# ディレクトリ配下を再帰的に検索(-r)
grep -rn "TODO" ./src/

sed によるストリーム編集と置換

ファイルの特定パターンを置換したり、指定した行を削除したりします。

» 文字列の置換(s コマンド)

基本構文は sed 's/検索文字列/置換文字列/フラグ' です。末尾に g(グローバルフラグ)をつけると1行内のすべての出現箇所を置換します。

# 文字列の置換(標準出力に結果を表示)
sed 's/localhost/127.0.0.1/g' config.txt

# パスなどスラッシュを含む文字列を置換する場合は区切り文字を @ や # に変更可能
sed 's@/usr/local/bin@/opt/bin@g' path.txt

» 行の削除と特定範囲の抽出

# 1行目(ヘッダー行)を削除して出力
sed '1d' data.csv

# 空行をすべて削除
sed '/^$/d' document.txt

# 10行目から20行目までを抽出して表示(-n と p)
sed -n '10,20p' server.log

» ファイルの直接上書き編集(-i オプション)

ファイルを直接書き換える場合は -i を指定します。Linux(GNU sed)と macOS(BSD sed)で仕様が異なるため注意が必要です。

# Linux (GNU)
sed -i 's/DEBUG=true/DEBUG=false/' settings.env

# macOS (BSD): バックアップ拡張子の明示が必要(空文字 '' を指定)
sed -i '' 's/DEBUG=true/DEBUG=false/' settings.env

awk による列抽出と集計

スペースやカンマで区切られた表形式のデータを処理する強力なツールです。

» フィールドの参照と区切り文字

$1, $2 はそれぞれ第1列、第2列を表し、$0 は行全体を表します。$NF は末尾の列を表します。

# 空白区切りの第1列(IPアドレス)と第3列(ステータス)を表示
awk '{print $1, $3}' access.log

# カンマ区切り(CSV)を指定して第2列を出力(-F)
awk -F',' '{print $2}' users.csv

» 条件判定と集計(BEGIN / END ブロック)

# 第3列が 500 以上のエラー行のみを抽出
awk '$3 >= 500 {print "Error at line", NR, ":", $0}' access.log

# 第2列の数値を合計して平均値を計算
awk '{sum += $2} END {print "Total:", sum, "Average:", sum/NR}' metrics.txt

パイプライン連携の実用例

複数のツールを組み合わせて、Web サーバのアクセスログからアクセス数上位の IP アドレスを即座に集計します。

# 1. ログから IP アドレス(第1列)を抽出
# 2. ソートして出現回数を集計(uniq -c)
# 3. 件数の降順に並べ替え(sort -nr)
# 4. 上位5件を表示(head -n 5)
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -n 5

応用編:エラー処理・デバッグと安全な実行

Bash スクリプトの暴走やサイレント障害を防ぐ厳格モード(set -euo pipefail)、trap によるエラー捕捉、および効果的なデバッグ手法を解説します。

デフォルトの Bash が抱える危険性

Bash は既定の状態では以下のような挙動を示します。

  • コマンドの実行が途中で失敗しても、何事もなかったかのように後続の処理を継続する
  • 誤字などによる未定義の変数を参照しても、単に空文字として処理を続行する
  • パイプラインの途中のコマンドがエラーになっても、最後のコマンドが成功すれば成功とみなす

例えば rm -rf "$TARGET_DIR/*" において $TARGET_DIR が未定義の場合、空文字とみなされてルートディレクトリ直下(/*)が削除されるといった重大な事故に直結します。

厳格モード(set -euo pipefail)

スクリプトの冒頭で以下のオプションを設定することで、安全な実行環境(厳格モード)を確立します。

#!/usr/bin/env bash
set -euo pipefail
オプション略称役割
set -eerrexitコマンドが1つでも失敗(終了ステータスが非0)したら即座にスクリプトを中断する
set -unounset未定義の変数を参照した場合、エラーを表示してスクリプトを中断する
set -o pipefail-パイプライン内で1つでも失敗したコマンドがあれば、パイプライン全体を失敗とする

» 意図したエラーの許容

set -e 環境下で、特定コマンドのエラー(終了コード非0)を無視して継続したい場合は、|| true を付与するか if 文で判定します。

# grep で検索結果が0件(終了コード 1)でもスクリプトを停止させない
grep "optional_setting" config.txt || true

trap によるエラーの補足と通知

trap コマンドを使用すると、スクリプト内でエラー(ERR)が発生した瞬間に指定した関数を自動実行できます。組み込み変数 $LINENO(行番号)や $BASH_COMMAND(失敗したコマンド)を記録するのに役立ちます。

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

# エラー発生時のハンドラ関数
on_error() {
    local exit_code="$1"
    local line_no="$2"
    local command="$3"
    echo "[ERROR] Command '$command' failed at line $line_no with exit code $exit_code" >&2
}

# ERR シグナルをフック
trap 'on_error $? $LINENO "$BASH_COMMAND"' ERR

# 意図的にエラーを起こすテスト
ls /path/to/missing_file

デバッグ手法とトレース実行

スクリプトの不具合を素早く調査・特定するための技法です。

» 構文チェック(-n オプション)

スクリプトを実行せずに、文法エラー(if の閉じ忘れ等)の有無だけを静的に検証します。

bash -n script.sh

» 実行トレース(set -x)

実行される各コマンドとその引数を、実際に評価された値に展開した状態でターミナルに出力します。

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

# スクリプト内の特定ブロックだけトレースを有効化
set -x
critical_processing
set +x  # トレースを停止

» 構造化されたログ出力

標準出力と標準エラー出力を適切に分離し、タイムスタンプを付与したログ関数を用意します。

log_info()  { echo "[INFO]  $(date +%T) - $*"; }
log_warn()  { echo "[WARN]  $(date +%T) - $*" >&2; }
log_error() { echo "[ERROR] $(date +%T) - $*" >&2; }

応用編:シグナル処理・バックグラウンドジョブ・並行実行

コマンドのバックグラウンド実行、wait を用いた並行タスクの同期、Unix シグナルの捕捉、および安全な終了処理(クリーンアップ)の設計を解説します。

バックグラウンド実行とジョブ制御

ターミナルを占有せずに時間のかかる処理を実行する機能です。

» バックグラウンド起動(&)

コマンドの末尾にアンパサンド(&)を付与すると、プロセスをバックグラウンドで開始し、即座にプロンプトが戻ります。

# 重い処理をバックグラウンドで実行
python train_model.py &

» ジョブ制御コマンド

  • jobs: 現在のシェルで管理されているバックグラウンドジョブ一覧を表示
  • fg %1: 指定したジョブ番号のプロセスをフォアグラウンド(前面)に呼び戻す
  • bg %1: 一時停止中のジョブをバックグラウンドで再開
  • Ctrl + Z: 実行中のフォアグラウンドプロセスを一時停止(SIGTSTP)

wait による並行処理の同期

複数のバックグラウンドプロセスを同時に起動し、それらすべての完了を待ってから次の処理へ進む並行処理パターンです。

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

# 3つのタスクを並列にバックグラウンド実行
task_a() { sleep 3; echo "Task A done"; }
task_b() { sleep 2; echo "Task B done"; }
task_c() { sleep 1; echo "Task C done"; }

echo "Starting all tasks in parallel..."
task_a &
task_b &
task_c &

# すべてのバックグラウンドプロセスの完了を待機
wait

echo "All tasks finished."

個別プロセスの完了や成否を判定したい場合は、起動直後の $!(最新の PID)を配列等に退避し、wait $pid で個別に監視します。

Unix シグナルの捕捉(trap による後処理)

ユーザーによる強制中断(Ctrl + C)やシステム終了時にも、確実に一時ファイルを削除して安全に終了するためのシグナル処理です。

シグナル名番号発生契機
INT2ユーザーが Ctrl + C を押下したとき
TERM15kill コマンド等で終了が要求されたとき
HUP1ターミナルウィンドウが閉じられたとき
EXIT0正常終了・異常終了を問わず、シェルが終了するとき

» 一時ファイルの自動クリーンアップ

trap ... EXIT を設定すると、スクリプトがどのように終了した場合(途中で exit した場合やエラーが発生した場合も含む)でも、登録された後処理が確実に呼び出されます。

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

# 安全な一時ディレクトリを作成
TMP_DIR=$(mktemp -d "/tmp/my_app.XXXXXX")

# スクリプト終了時に必ず一時ディレクトリを削除
cleanup() {
    echo "Cleaning up temporary files: $TMP_DIR"
    rm -rf "$TMP_DIR"
}
trap cleanup EXIT

# 処理本体
echo "Work data" > "$TMP_DIR/data.txt"

ロックファイルによる多重起動の防止

cron 等で定期実行されるスクリプトが前の処理の完了前に二重起動することを防ぐため、mkdir や flock を使用した排他制御を行います。

LOCK_FILE="/var/lock/my_script.lock"

# flock コマンドを用いた安全なファイルロック
exec 200>"$LOCK_FILE"
flock -n 200 || {
    echo "Another instance is already running. Exiting." >&2
    exit 1
}

echo "Lock acquired. Executing critical tasks..."

応用編:セキュリティとシェルスクリプトのベストプラクティス

静的解析ツール ShellCheck による事前検証、コマンドインジェクションの防止、安全な一時ファイルの生成、および保守性を高めるコーディング規約を解説します。

ShellCheck による静的コード解析

ShellCheck は、シェルスクリプトのバグ、セキュリティ脆弱性、およびスタイル違反を検出する業界標準の静的解析ツールです。

CI/CD パイプラインやコミット前フックに ShellCheck を組み込むことで、レビュー工数を削減し致命的な事故を未然に防ぎます。

# スクリプトの検証を実行
shellcheck deploy.sh

ShellCheck は以下のような典型的な問題を的確に指摘します。

  • 変数のクォート漏れによる単語分割リスク(SC2086)
  • [[ ... ]] ではなく古い [ ... ] を使った比較ミス
  • 未定義の変数の参照やシバンの不整合

» 特定行での警告抑制

正当な理由で警告を無視したい場合は、直前の行にディレクティブを記述します。

# shellcheck disable=SC2086
cat $file_list

コマンドインジェクションの防止

外部からの入力値(引数、Web リクエスト、ファイル名等)をシェル内で扱う際は、コードとして解釈されないよう厳格に対処します。

» eval コマンドの完全な禁止

文字列をシェルコードとして実行する eval は、悪意あるコマンド(例: ; rm -rf /)を埋め込まれる危険が極めて高いため、使用してはなりません。配列やパラメータ展開で代替します。

» 入力値の厳格な検証

引数を処理する前に、想定された文字種(英数字のみ等)であることを正規表現で検証します。

USER_ID="$1"

if [[ ! "$USER_ID" =~ ^[a-zA-Z0-9_-]+$ ]]; then
    echo "Error: Invalid user ID format." >&2
    exit 1
fi

» 機密情報の平文露出防止

スクリプト内でパスワードを入力させる際は、画面に入力文字を表示させない -s オプションを使用します。

read -r -s -p "Enter password: " USER_PASS
echo

一時ファイルの安全な作成(mktemp)

/tmp/my_temp.txt のように固定のファイル名を使用すると、他ユーザーからの推測によるシンボリックリンク攻撃(悪意あるファイルの上書き)の標的になります。

一時ファイルやディレクトリは、必ず mktemp コマンドでランダムな名前を生成します。

# ランダムな一時ファイル(パーミッション 600 で作成される)
TEMP_FILE=$(mktemp)

# ランダムな一時ディレクトリ(パーミッション 700 で作成される)
TEMP_DIR=$(mktemp -d)

コーディング規約と可読性の向上

チーム開発や長期保守に耐えるスクリプトを書くための基本方針です。

» 1. コマンドの存在確認には command -v を使用する

外部ツールがインストールされているか確認する際、環境依存の強い which は避け、POSIX 準拠で組み込みの command -v を使用します。

if ! command -v jq > /dev/null 2>&1; then
    echo "Error: 'jq' command is required but not installed." >&2
    exit 1
fi

» 2. 変数の命名規則

  • 環境変数・定数: 大文字スネークケース(例: DATABASE_URL、MAX_RETRIES)
  • ローカル変数・ループ変数: 小文字スネークケース(例: local file_name、for item in ...)

命名を明確に分けることで、システムの環境変数を誤って上書きするリスクを低減できます。