このチュートリアルで学ぶこと
- シェルスクリプトの基本構造
- 変数の使い方
- 条件分岐(if文)
- ループ処理(for, while)
- 実践的な自動化スクリプト
シェルスクリプトとは?なぜ学ぶのか
シェルスクリプトの誕生
シェルスクリプトの歴史は1971年のThompson shell(sh)に遡ります。1979年にはStephen BourneがBourne Shell(sh)を開発し、これが現代のシェルスクリプトの基礎となりました。
1989年にはBrian FoxがGNUプロジェクトの一環としてBash(Bourne Again Shell)を開発。Bashは現在最も広く使われているシェルです。
「シェルスクリプトは、繰り返し行う作業を自動化するための最も手軽な方法である」— Unix環境の古くからの知恵
なぜシェルスクリプトを学ぶのか
- 自動化: 繰り返しタスクを自動化し、時間を節約
- システム管理: サーバー管理、デプロイ、バックアップに必須
- CI/CD: GitHub Actions、Jenkinsなどで多用される
- 移植性: ほぼすべてのUnix系システムで動作
Bashとその他のシェル
| シェル | 特徴 | 主な用途 |
|---|---|---|
| bash | 最も普及。POSIX互換 + 拡張機能 | 一般的なスクリプト |
| sh | POSIX標準。最小限の機能 | 移植性重視のスクリプト |
| zsh | bash互換 + 強力な補完機能 | 対話シェル |
| fish | ユーザーフレンドリー | 対話シェル |
| dash | 軽量・高速 | システムスクリプト |
ベストプラクティス: 移植性を重視する場合は
#!/bin/shを、Bash固有の機能を使う場合は#!/bin/bashを使用します。
最初のシェルスクリプト
まずは「Hello World」から始めましょう。
hello.sh
#!/bin/bash
# これは最初のシェルスクリプトです
echo "Hello, World!"
実行方法
# 実行権限を付与
chmod +x hello.sh
# 実行
./hello.sh
# または、bashを明示的に指定
bash hello.sh
シバン(Shebang)について
1行目の#!/bin/bashはシバン(shebang)と呼ばれ、このスクリプトをどのインタプリタで実行するかを指定します。
#!/bin/bash # bashで実行
#!/bin/sh # POSIX shで実行(移植性が高い)
#!/usr/bin/env bash # PATHからbashを探して実行(推奨)
公式ドキュメント: GNU Bash Manual
変数の使い方
基本的な変数
#!/bin/bash
# 変数の定義(=の前後にスペースを入れない!)
name="太郎"
age=25
# 変数の参照
echo "名前: $name"
echo "年齢: ${age}歳"
# コマンドの結果を変数に代入
current_date=$(date +%Y-%m-%d)
echo "今日の日付: $current_date"
# 旧式の書き方(バッククォート)- 非推奨
old_style=`date +%Y-%m-%d`
よくある間違い: スペース
# 間違い(スペースがあるとエラー)
name = "太郎" # command not found: name
name= "太郎" # 空の変数としてコマンド "太郎" を実行
# 正解
name="太郎"
変数のスコープ
#!/bin/bash
# グローバル変数
global_var="I am global"
function example() {
# ローカル変数(関数内のみ有効)
local local_var="I am local"
echo "$local_var"
echo "$global_var"
}
example
echo "$local_var" # 空(関数外では見えない)
特殊変数
#!/bin/bash
echo "スクリプト名: $0"
echo "第1引数: $1"
echo "第2引数: $2"
echo "引数の数: $#"
echo "すべての引数: $@"
echo "すべての引数(文字列): $*"
echo "直前のコマンドの終了ステータス: $?"
echo "現在のプロセスID: $$"
echo "バックグラウンドプロセスのPID: $!"
実行例:
$ ./script.sh arg1 arg2 arg3
スクリプト名: ./script.sh
第1引数: arg1
第2引数: arg2
引数の数: 3
すべての引数: arg1 arg2 arg3
環境変数
# 環境変数の参照
echo "ホームディレクトリ: $HOME"
echo "ユーザー名: $USER"
echo "現在のディレクトリ: $PWD"
echo "シェル: $SHELL"
echo "パス: $PATH"
# 環境変数の設定(サブプロセスに継承される)
export MY_VAR="some value"
# 一時的に環境変数を設定してコマンドを実行
DEBUG=true ./my_script.sh
クォートの違い
name="World"
# ダブルクォート: 変数展開される
echo "Hello, $name" # Hello, World
# シングルクォート: そのまま出力
echo 'Hello, $name' # Hello, $name
# バックスラッシュでエスケープ
echo "Hello, \$name" # Hello, $name
条件分岐(if文)
基本構文
#!/bin/bash
age=20
if [ $age -ge 20 ]; then
echo "成人です"
elif [ $age -ge 13 ]; then
echo "ティーンエイジャーです"
else
echo "子供です"
fi
テスト構文の種類
# [ ] - 古典的なtest構文(POSIX互換)
if [ $a -eq $b ]; then
# [[ ]] - Bash拡張構文(推奨)
if [[ $a == $b ]]; then
# (( )) - 算術評価
if (( a > b )); then
ベストプラクティス: Bashを使う場合は
[[ ]]を推奨。パターンマッチングや正規表現が使えます。
数値比較演算子
| 演算子 | 意味 | 例 |
|---|---|---|
-eq | 等しい (equal) | [ $a -eq $b ] |
-ne | 等しくない (not equal) | [ $a -ne $b ] |
-lt | より小さい (less than) | [ $a -lt $b ] |
-le | 以下 (less or equal) | [ $a -le $b ] |
-gt | より大きい (greater than) | [ $a -gt $b ] |
-ge | 以上 (greater or equal) | [ $a -ge $b ] |
文字列比較
#!/bin/bash
str="hello"
# 文字列の比較(クォートで囲むことが重要!)
if [ "$str" = "hello" ]; then
echo "文字列が一致"
fi
# 空文字列のチェック
if [ -z "$str" ]; then
echo "文字列は空です"
fi
# 空でないかチェック
if [ -n "$str" ]; then
echo "文字列は空ではありません"
fi
# [[ ]]を使ったパターンマッチング
if [[ "$str" == h* ]]; then
echo "hで始まります"
fi
# 正規表現マッチング(Bash 3.0以降)
if [[ "$str" =~ ^[a-z]+$ ]]; then
echo "小文字のみです"
fi
ファイル・ディレクトリの判定
#!/bin/bash
# ファイルの存在確認
if [ -f "config.txt" ]; then
echo "config.txtが存在します"
fi
# ディレクトリの存在確認
if [ -d "logs" ]; then
echo "logsディレクトリが存在します"
fi
# ファイルまたはディレクトリが存在
if [ -e "path" ]; then
echo "pathが存在します"
fi
# 読み取り可能
if [ -r "file.txt" ]; then
echo "読み取り可能です"
fi
# 書き込み可能
if [ -w "file.txt" ]; then
echo "書き込み可能です"
fi
# 実行可能
if [ -x "script.sh" ]; then
echo "実行可能です"
fi
# ファイルが空でない
if [ -s "file.txt" ]; then
echo "ファイルは空ではありません"
fi
論理演算子
# AND
if [ $a -gt 0 ] && [ $a -lt 10 ]; then
echo "0 < a < 10"
fi
# OR
if [ $a -eq 0 ] || [ $a -eq 1 ]; then
echo "a is 0 or 1"
fi
# NOT
if [ ! -f "file.txt" ]; then
echo "file.txt does not exist"
fi
# [[ ]] では && と || が使える
if [[ $a -gt 0 && $a -lt 10 ]]; then
echo "0 < a < 10"
fi
case文
複数の条件分岐にはcase文が便利です:
#!/bin/bash
fruit="apple"
case $fruit in
apple)
echo "りんごです"
;;
banana|orange)
echo "バナナかオレンジです"
;;
*)
echo "不明な果物です"
;;
esac
ループ処理
forループ
#!/bin/bash
# リストを順に処理
for fruit in apple banana orange; do
echo "フルーツ: $fruit"
done
# 数値の範囲(Bash拡張)
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 "Index: $i"
done
# ファイルを処理
for file in *.txt; do
echo "ファイル: $file"
done
# コマンドの出力を処理
for user in $(cat users.txt); do
echo "ユーザー: $user"
done
whileループ
#!/bin/bash
count=1
while [ $count -le 5 ]; do
echo "カウント: $count"
count=$((count + 1))
done
# ファイルを1行ずつ読み込み(推奨パターン)
while IFS= read -r line; do
echo "行: $line"
done < input.txt
# 無限ループ
while true; do
echo "Press Ctrl+C to stop"
sleep 1
done
untilループ
#!/bin/bash
count=1
until [ $count -gt 5 ]; do
echo "カウント: $count"
count=$((count + 1))
done
ループ制御
# breakでループを抜ける
for i in {1..10}; do
if [ $i -eq 5 ]; then
break
fi
echo $i
done
# continueで次のイテレーションへ
for i in {1..5}; do
if [ $i -eq 3 ]; then
continue
fi
echo $i
done
関数の定義
#!/bin/bash
# 関数の定義(2つの書き方)
function greet() {
local name=$1
echo "こんにちは、${name}さん!"
}
# または
greet2() {
echo "Hello, $1!"
}
# 関数の呼び出し
greet "太郎"
greet2 "World"
# 戻り値(終了ステータス)
is_even() {
if (( $1 % 2 == 0 )); then
return 0 # 成功(偶数)
else
return 1 # 失敗(奇数)
fi
}
if is_even 4; then
echo "4は偶数です"
fi
# 値を返す場合はechoを使う
add() {
echo $(( $1 + $2 ))
}
result=$(add 3 5)
echo "3 + 5 = $result"
実践: バックアップスクリプト
学んだ内容を使って、実用的なバックアップスクリプトを作成します。
backup.sh
#!/bin/bash
#
# 自動バックアップスクリプト
# 使用方法: ./backup.sh [source_dir] [backup_dir]
#
set -euo pipefail # エラー時に即座に終了
# 設定
SOURCE_DIR="${1:-./src}"
BACKUP_DIR="${2:-./backups}"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_$DATE.tar.gz"
RETENTION_DAYS=7
# ログ関数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# バックアップディレクトリの確認・作成
if [ ! -d "$BACKUP_DIR" ]; then
log "バックアップディレクトリを作成します..."
mkdir -p "$BACKUP_DIR"
fi
# ソースディレクトリの確認
if [ ! -d "$SOURCE_DIR" ]; then
log "エラー: $SOURCE_DIR が見つかりません"
exit 1
fi
# バックアップ実行
log "バックアップを開始します: $SOURCE_DIR -> $BACKUP_DIR/$BACKUP_NAME"
tar -czf "$BACKUP_DIR/$BACKUP_NAME" "$SOURCE_DIR"
if [ $? -eq 0 ]; then
log "完了: $BACKUP_DIR/$BACKUP_NAME"
log "サイズ: $(du -h "$BACKUP_DIR/$BACKUP_NAME" | cut -f1)"
else
log "エラー: バックアップに失敗しました"
exit 1
fi
# 古いバックアップの削除
log "古いバックアップを削除中(${RETENTION_DAYS}日以上前)..."
deleted=$(find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete -print | wc -l)
log "削除されたファイル数: $deleted"
log "すべて完了しました!"
スクリプトのベストプラクティス
-
set -euo pipefailを使う-e: エラー時に即座に終了-u: 未定義変数をエラーにする-o pipefail: パイプ内のエラーも検出
-
変数は常にクォートする:
"$variable" -
関数を活用する: 再利用性と可読性向上
-
ログ出力を入れる: デバッグと監視のため
エラーハンドリング
#!/bin/bash
# トラップでエラー時の処理を定義
cleanup() {
echo "クリーンアップ処理を実行..."
# 一時ファイルの削除など
}
trap cleanup EXIT # スクリプト終了時に実行
trap 'echo "エラーが発生しました: 行 $LINENO"' ERR
# エラーを無視したい場合
command_that_might_fail || true
# エラー時の代替処理
config_file="config.txt"
if [ -f "$config_file" ]; then
source "$config_file"
else
echo "設定ファイルがありません。デフォルト値を使用します。"
default_value="fallback"
fi
デバッグのコツ
# スクリプト全体をデバッグモードで実行
bash -x script.sh
# スクリプト内でデバッグを有効化
set -x # デバッグ開始
# ... デバッグしたいコード ...
set +x # デバッグ終了
# エラー時に即座に終了(推奨)
set -e
# 未定義変数をエラーにする(推奨)
set -u
# パイプのエラーも検出(推奨)
set -o pipefail
# よく使う組み合わせ
set -euo pipefail
ShellCheckで静的解析
ShellCheckは、シェルスクリプトの問題を検出する静的解析ツールです。
# インストール
# macOS
brew install shellcheck
# Ubuntu/Debian
sudo apt install shellcheck
# 使い方
shellcheck script.sh
ShellCheckが検出する問題の例:
# 警告: 変数のクォート漏れ
echo $USER # SC2086: Double quote to prevent globbing
# 修正後
echo "$USER"
Google Shell Style Guideでも、ShellCheckの使用が推奨されています。
よくあるパターン
引数のバリデーション
#!/bin/bash
if [ $# -lt 2 ]; then
echo "使用方法: $0 <source> <destination>"
exit 1
fi
source=$1
destination=$2
デフォルト値の設定
# 変数が未設定または空の場合にデフォルト値を使用
name="${1:-World}"
echo "Hello, $name"
# 変数が未設定の場合のみデフォルト値を使用
name="${1-World}"
一時ファイルの安全な作成
#!/bin/bash
# mktempを使用(推奨)
temp_file=$(mktemp)
temp_dir=$(mktemp -d)
# 終了時に削除
trap "rm -rf $temp_file $temp_dir" EXIT
echo "一時ファイル: $temp_file"
echo "一時ディレクトリ: $temp_dir"
ユーザー入力の取得
#!/bin/bash
# 入力を読み取る
read -p "名前を入力してください: " name
echo "こんにちは、$name さん"
# パスワード入力(非表示)
read -sp "パスワード: " password
echo ""
# タイムアウト付き
read -t 5 -p "5秒以内に入力してください: " input || echo "タイムアウト"
次のステップ
シェルスクリプトの基本をマスターしたら、次のステップへ進みましょう:
- 正規表現と
grep、sed、awk - cronジョブによるスケジュール実行
- より高度な自動化(Ansible、Makefileなど)
参考リンク
公式ドキュメント
- GNU Bash Manual - Bash公式リファレンス
- POSIX Shell Command Language - POSIX標準
スタイルガイド・ベストプラクティス
- Google Shell Style Guide - Googleのシェルスクリプトスタイルガイド
- ShellCheck - シェルスクリプトの静的解析ツール
学習リソース
- Bash Hackers Wiki - Bashの高度なテクニック
- explainshell.com - コマンドを入力すると解説してくれる
- Pure Bash Bible - Bashのみで実装するレシピ集
チートシート
- Bash Scripting Cheat Sheet - よく使う構文一覧
- Advanced Bash-Scripting Guide - 詳細なBashガイド(英語)