はじめに
シェルスクリプト(bash)は、Linux/macOS の自動化・バッチ処理・CI/CDに欠かせない技術です。しかし文法が独特で「なんとなく動いている」状態になりがちです。本記事では文法を体系的に整理します。
0. 基本のキ
シバン(shebang)
スクリプトの1行目に書く、使用するシェルの指定です。
#!/bin/bash
#!/usr/bin/env bash # より移植性が高い書き方
実行権限と実行
chmod +x script.sh # 実行権限を付与
./script.sh # 実行
bash script.sh # bash で直接実行(権限不要)
コメント
# これはコメント
echo "hello" # 行末コメントも可
1. 変数
変数の定義と参照
name="田中" # = の前後にスペースを入れない(重要!)
echo $name # 田中
echo "${name}さん" # 田中さん(波括弧で区切りを明示)
よくあるミス:
name = "田中"はコマンドとして解釈されエラーになります。
変数の種類
# ローカル変数(そのシェルのみ有効)
count=10
# 環境変数(子プロセスにも引き継がれる)
export PATH="$PATH:/usr/local/bin"
# 読み取り専用
readonly MAX=100
# 変数の削除
unset count
クォートの使い分け
| 記法 | 変数展開 | 用途 |
|---|---|---|
"二重引用符" |
される | スペースを含む値・変数展開あり |
'一重引用符' |
されない | リテラル文字列 |
`バッククォート` |
コマンド実行 | 非推奨($() を使う) |
name="世界"
echo "Hello $name" # Hello 世界(変数展開される)
echo 'Hello $name' # Hello $name(展開されない)
コマンド置換
コマンドの出力を変数に代入します。
today=$(date +%Y-%m-%d)
echo "今日は $today です"
files=$(ls *.txt)
2. 特殊変数
| 変数 | 意味 |
|---|---|
$0 |
スクリプト名 |
$1 $2 ... |
第1引数、第2引数... |
$# |
引数の個数 |
$@ |
全引数(各引数を個別に扱う) |
$* |
全引数(1つの文字列として扱う) |
$? |
直前のコマンドの終了ステータス(0=成功) |
$$ |
現在のシェルのPID |
$! |
直前にバックグラウンド実行したコマンドのPID |
#!/bin/bash
echo "スクリプト名: $0"
echo "引数の数: $#"
echo "第1引数: $1"
echo "第2引数: $2"
echo "全引数: $@"
$ ./script.sh foo bar
スクリプト名: ./script.sh
引数の数: 2
第1引数: foo
第2引数: bar
全引数: foo bar
3. 算術演算
a=10
b=3
# $(( )) で算術演算
echo $((a + b)) # 13
echo $((a - b)) # 7
echo $((a * b)) # 30
echo $((a / b)) # 3(整数除算)
echo $((a % b)) # 1(余り)
echo $((a ** b)) # 1000(べき乗)
# インクリメント
count=0
((count++))
echo $count # 1
# let コマンド
let result=a*b
echo $result # 30
浮動小数点が必要な場合は
bcコマンドを使います。echo "scale=2; 10/3" | bc # 3.33
4. 文字列操作
str="Hello, World"
# 文字列長
echo ${#str} # 12
# 部分文字列(${変数:開始:長さ})
echo ${str:0:5} # Hello
echo ${str:7} # World
# 前方一致削除(最短)
echo ${str#Hello, } # World
# 前方一致削除(最長)
echo ${str##*,} # World
# 後方一致削除(最短)
echo ${str%,*} # Hello
# 置換(最初の1つ)
echo ${str/World/Shell} # Hello, Shell
# 置換(すべて)
echo ${str//l/L} # HeLLo, WorLd
# デフォルト値(変数が未定義または空のとき)
echo ${name:-"名無し"} # name が空なら「名無し」
# 大文字・小文字変換(bash 4.0+)
echo ${str^^} # HELLO, WORLD
echo ${str,,} # hello, world
5. 条件分岐
if 文
if [ 条件 ]; then
処理
elif [ 条件 ]; then
処理
else
処理
fi
条件式の種類
文字列比較
if [ "$a" = "$b" ]; then # 等しい(= を使う)
if [ "$a" != "$b" ]; then # 等しくない
if [ -z "$a" ]; then # 空文字
if [ -n "$a" ]; then # 空文字でない
数値比較
| 演算子 | 意味 |
|---|---|
-eq |
等しい(equal) |
-ne |
等しくない(not equal) |
-lt |
より小さい(less than) |
-le |
以下(less or equal) |
-gt |
より大きい(greater than) |
-ge |
以上(greater or equal) |
a=10
if [ $a -gt 5 ]; then
echo "5より大きい"
fi
ファイル・ディレクトリ判定
| 演算子 | 意味 |
|---|---|
-e file |
存在する |
-f file |
ファイルとして存在する |
-d file |
ディレクトリとして存在する |
-r file |
読み取り可能 |
-w file |
書き込み可能 |
-x file |
実行可能 |
-s file |
サイズが0より大きい |
if [ -f "config.txt" ]; then
echo "設定ファイルが存在します"
fi
論理演算子
# AND
if [ $a -gt 0 ] && [ $a -lt 100 ]; then
# OR
if [ $a -eq 0 ] || [ $a -eq 1 ]; then
# NOT
if ! [ -f "file.txt" ]; then
[[ ]] vs [ ]
[[ ]] は bash 拡張で、より安全で高機能です。
# [[ ]] はパターンマッチ・正規表現が使える
if [[ "$name" == 田* ]]; then # 前方一致
if [[ "$str" =~ ^[0-9]+$ ]]; then # 正規表現
# [[ ]] は変数をクォートしなくてもスペースで壊れない
if [[ $name == "田中 太郎" ]]; then # OK
if [ $name == "田中 太郎" ]; then # NG(スペースで引数が分割される)
case 文
case "$1" in
start)
echo "起動します"
;;
stop)
echo "停止します"
;;
restart)
echo "再起動します"
;;
*)
echo "使い方: $0 {start|stop|restart}"
exit 1
;;
esac
6. ループ
for 文
# リストのループ
for item in apple banana cherry; do
echo "$item"
done
# 数値範囲(seq)
for i in $(seq 1 5); do
echo "$i"
done
# C言語スタイル(bash拡張)
for ((i=0; i<5; i++)); do
echo "$i"
done
# ファイルのループ
for file in *.txt; do
echo "処理中: $file"
done
# 配列のループ
fruits=("apple" "banana" "cherry")
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
while 文
count=0
while [ $count -lt 5 ]; do
echo "count = $count"
((count++))
done
# ファイルを1行ずつ読む
while IFS= read -r line; do
echo "$line"
done < input.txt
until 文
while の逆:条件が 偽 の間ループします。
count=0
until [ $count -ge 5 ]; do
echo "count = $count"
((count++))
done
break / continue
for i in 1 2 3 4 5; do
if [ $i -eq 3 ]; then
continue # 3をスキップ
fi
if [ $i -eq 5 ]; then
break # 5で終了
fi
echo "$i"
done
# 出力: 1 2 4
7. 関数
# 定義
greet() {
local name="$1" # local で関数内ローカル変数
echo "こんにちは、${name}さん"
}
# 呼び出し
greet "田中" # こんにちは、田中さん
# 戻り値(終了ステータス 0〜255)
is_even() {
if (($1 % 2 == 0)); then
return 0 # 成功(偶数)
else
return 1 # 失敗(奇数)
fi
}
if is_even 4; then
echo "偶数"
fi
# 文字列を返したい場合はechoを使う
get_date() {
echo "$(date +%Y-%m-%d)"
}
today=$(get_date)
echo "$today"
localを使わないと変数がグローバルになり、意図しない副作用が出ます。関数内の変数は必ずlocalを付けましょう。
8. 入出力・リダイレクト
標準入出力
| ストリーム | fd | 説明 |
|---|---|---|
| 標準入力(stdin) | 0 | キーボード入力 |
| 標準出力(stdout) | 1 | 通常の出力 |
| 標準エラー出力(stderr) | 2 | エラーメッセージ |
リダイレクト
# 標準出力をファイルへ(上書き)
echo "hello" > output.txt
# 標準出力をファイルへ(追記)
echo "world" >> output.txt
# 標準エラー出力をファイルへ
command 2> error.log
# 標準出力・標準エラーを同じファイルへ
command > all.log 2>&1
command &> all.log # bash 4+ の短縮形
# 標準エラーを捨てる
command 2>/dev/null
# ファイルから標準入力
command < input.txt
# ヒアドキュメント(複数行の入力)
cat <<EOF
1行目
2行目
3行目
EOF
# ヒアストリング(1行の入力)
grep "pattern" <<< "検索対象の文字列"
パイプ
コマンドの標準出力を次のコマンドの標準入力につなぎます。
# 基本
ls -l | grep ".txt"
# 複数つなげる
cat access.log | grep "ERROR" | sort | uniq -c | sort -rn | head -10
# tee(ファイルにも書きながらパイプ)
command | tee output.txt | grep "ERROR"
9. 終了ステータスとエラーハンドリング
終了ステータス
ls /tmp
echo $? # 0(成功)
ls /存在しないパス
echo $? # 0以外(失敗)
set オプション(安全なスクリプト)
#!/bin/bash
set -e # エラーで即終了
set -u # 未定義変数の参照でエラー
set -o pipefail # パイプ途中のエラーも検出
# まとめて書く
set -euo pipefail
trap(終了時の処理)
#!/bin/bash
set -euo pipefail
# スクリプト終了時に必ず実行(クリーンアップ)
trap 'echo "終了処理"; rm -f /tmp/work.$$' EXIT
# エラー時の処理
trap 'echo "エラーが発生しました(行: $LINENO)"' ERR
10. 配列
# 定義
fruits=("apple" "banana" "cherry")
# 参照
echo ${fruits[0]} # apple
echo ${fruits[1]} # banana
echo ${fruits[@]} # apple banana cherry(全要素)
echo ${#fruits[@]} # 3(要素数)
# 追加
fruits+=("grape")
# 削除
unset fruits[1] # インデックス1を削除
# スライス
echo ${fruits[@]:1:2} # インデックス1から2個
連想配列(bash 4.0+)
declare -A user
user["name"]="田中"
user["age"]=30
echo ${user["name"]} # 田中
echo ${!user[@]} # キー一覧: name age
echo ${user[@]} # 値一覧
11. よく使うパターン集
スクリプトのあるディレクトリを取得
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
引数チェック
if [ $# -lt 2 ]; then
echo "使い方: $0 <入力ファイル> <出力ファイル>"
exit 1
fi
ファイルが存在しなければ作成
[[ -f "config.txt" ]] || touch "config.txt"
コマンドが存在するか確認
if command -v jq &>/dev/null; then
echo "jq が使えます"
fi
ログ出力関数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
log "処理を開始します"
数値かどうか確認
is_number() {
[[ "$1" =~ ^[0-9]+$ ]]
}
if is_number "123"; then
echo "数値です"
fi
演習問題
⭐ 問題1:FizzBuzz
1〜20 を出力し、3の倍数なら「Fizz」、5の倍数なら「Buzz」、両方なら「FizzBuzz」を出力してください。
模範解答
#!/bin/bash
for ((i=1; i<=20; i++)); do
if ((i % 15 == 0)); then
echo "FizzBuzz"
elif ((i % 3 == 0)); then
echo "Fizz"
elif ((i % 5 == 0)); then
echo "Buzz"
else
echo "$i"
fi
done
⭐⭐ 問題2:ファイルバックアップ
引数で指定したファイルを ファイル名.YYYYMMDD.bak という名前でコピーするスクリプトを作成してください。ファイルが存在しない場合はエラーメッセージを出して終了してください。
模範解答
#!/bin/bash
set -euo pipefail
if [ $# -ne 1 ]; then
echo "使い方: $0 <ファイル名>"
exit 1
fi
src="$1"
if [ ! -f "$src" ]; then
echo "エラー: ファイル '$src' が存在しません"
exit 1
fi
date_str=$(date +%Y%m%d)
dst="${src}.${date_str}.bak"
cp "$src" "$dst"
echo "バックアップ完了: $dst"
⭐⭐⭐ 問題3:CSVの集計
以下のCSVファイルを読み込み、各部署の合計金額を出力してください。
営業部,1000
開発部,2000
営業部,1500
開発部,3000
人事部,500
模範解答
#!/bin/bash
set -euo pipefail
declare -A totals
while IFS=, read -r dept amount; do
totals["$dept"]=$(( ${totals["$dept"]:-0} + amount ))
done < sales.csv
for dept in "${!totals[@]}"; do
echo "${dept}: ${totals[$dept]}"
done
実行結果(順序は不定):
営業部: 2500
開発部: 5000
人事部: 500
まとめ
| 項目 | ポイント |
|---|---|
| 変数 |
= の前後にスペースなし。参照は ${} で括る |
| 条件分岐 | 文字列比較は =、数値比較は -eq など。[[ ]] を推奨 |
| ループ |
for in・C言語スタイル・while read を使い分ける |
| 関数 |
local で変数をローカルに。戻り値は終了ステータスかecho |
| リダイレクト |
2>&1 でstdoutとstderrをまとめる |
| 安全策 |
set -euo pipefail を先頭に書く習慣を |
参考
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!