シェルスクリプト基礎
シェルスクリプトは CLI の操作を自動化する最も直接的な方法。日常の繰り返し作業から本格的なシステム管理まで、あらゆるレベルで活用できる。
82 分で読めます40,560 文字
シェルスクリプト基礎
シェルスクリプトは CLI の操作を自動化する最も直接的な方法。日常の繰り返し作業から本格的なシステム管理まで、あらゆるレベルで活用できる。
この章で学ぶこと
- シェルスクリプトの基本構文を理解する
- 変数・条件分岐・ループを使える
- 関数とスクリプトの引数処理ができる
- 入出力・リダイレクト・ヒアドキュメントを使いこなす
- デバッグ手法を身につける
- 実務で使えるスクリプトテンプレートを持つ
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
1. スクリプトの基本
1.1 シバン(Shebang)
スクリプトの1行目に記述する #! で始まる行をシバン(shebang)と呼ぶ。OS がこの行を読み取り、指定されたインタプリタでスクリプトを実行する。
#!/bin/bash
# ↑ シバン(shebang): このスクリプトを実行するインタプリタを指定
# スクリプトの実行方法
chmod +x script.sh # 実行権限を付与
./script.sh # 直接実行
bash script.sh # bash で明示的に実行
source script.sh # 現在のシェルで実行(. script.sh と同じ)
# シバンの種類
#!/bin/bash # bash
#!/bin/zsh # zsh
#!/usr/bin/env bash # PATH から bash を探す(推奨)
#!/usr/bin/env python3 # Python スクリプト
#!/usr/bin/env perl # Perl スクリプト
#!/bin/sh # POSIX sh(移植性最優先の場合)1.2 #!/bin/bash と #!/usr/bin/env bash の違い
# #!/bin/bash
# → /bin/bash を直接指定
# → bash が /bin/ にない環境(一部の BSD、NixOS 等)では動かない
# → パスが確定しているため起動が微妙に速い
# #!/usr/bin/env bash
# → $PATH を検索して bash を見つける
# → 異なる環境間での移植性が高い(推奨)
# → pyenv, rbenv 等のバージョン管理ツールとも相性が良い
# 確認方法
which bash # bash のパスを表示
type bash # bash の種類を表示
bash --version # バージョン確認1.3 スクリプトの実行方法の違い
# 方法1: 直接実行(サブシェルで実行)
chmod +x script.sh
./script.sh
# → 新しいプロセスが起動する
# → スクリプト内の cd, export は呼び出し元に影響しない
# 方法2: bash コマンドで実行(サブシェル)
bash script.sh
# → 実行権限不要
# → シバン行は無視される
# 方法3: source で実行(現在のシェルで実行)
source script.sh
# または
. script.sh
# → 現在のシェルで直接実行
# → スクリプト内の cd, export, alias が現在のシェルに影響する
# → .bashrc, .zshrc の読み込みに使われる
# 実践例: 環境変数を設定するスクリプト
# --- env.sh ---
#!/bin/bash
export APP_ENV="production"
export APP_PORT="8080"
# --- ここまで ---
# NG: サブシェルで実行しても現在のシェルに反映されない
./env.sh
echo $APP_ENV # → (空)
# OK: source で実行すれば反映される
source env.sh
echo $APP_ENV # → production1.4 スクリプトファイルの慣例
# ファイル名の慣例
# - 拡張子 .sh を付ける(必須ではないが推奨)
# - 実行可能スクリプトは拡張子なしも一般的(/usr/local/bin/mycommand)
# - ハイフン区切り: deploy-app.sh, run-tests.sh
# - アンダースコアも可: deploy_app.sh
# ファイルの構成(推奨テンプレート)
#!/usr/bin/env bash
#
# スクリプト名: deploy.sh
# 説明: アプリケーションのデプロイを実行する
# 作成者: Gaku
# 作成日: 2025-01-01
# 使い方: ./deploy.sh [--env production|staging] [--branch main]
#
set -euo pipefail
# 定数
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# メイン処理
main() {
echo "Starting $SCRIPT_NAME..."
# ここに処理を書く
}
main "$@"1.5 コメントの書き方
# 行コメント(# から行末まで)
echo "Hello" # インラインコメント
# 複数行コメント(ヒアドキュメントを利用したテクニック)
: <<'COMMENT'
この部分は実行されない
複数行のコメントとして使える
ただしインデントの制約がある
COMMENT
# ドキュメンテーションコメントの慣例
#######################################
# データベースのバックアップを実行する
# Globals:
# DB_HOST
# DB_NAME
# Arguments:
# $1 - 出力ディレクトリ
# Returns:
# 0 - 成功
# 1 - 失敗
#######################################
backup_database() {
local output_dir="$1"
# ...
}2. 変数
2.1 変数の基本
# 変数の代入(= の前後にスペースを入れない)
name="Gaku" # 文字列
count=42 # 数値
path="/var/log" # パス
empty="" # 空文字列
# よくあるミス
# name = "Gaku" # NG: スペースがあるとコマンドと解釈される
# name ="Gaku" # NG: 同上
# name= "Gaku" # NG: 同上
# 変数の参照
echo "$name" # Gaku
echo "${name}_suffix" # Gaku_suffix(区切りが必要な場合)
echo "Hello, $name!" # Hello, Gaku!
echo "${name}san" # Gakusan(直後に文字が続く場合は {} が必要)
# クォートの違い
echo "Hello, $name" # → Hello, Gaku(変数展開される)
echo 'Hello, $name' # → Hello, $name(リテラル)
echo Hello, $name # → Hello, Gaku(クォートなし: ワード分割が起こる)
# クォートの重要性
file="my file.txt"
# cat $file # NG: "my" と "file.txt" の2引数として解釈
cat "$file" # OK: "my file.txt" として1引数
# ルール: 変数は常にダブルクォートで囲む
# コマンド置換
today=$(date +%Y-%m-%d) # 推奨: $() 形式
files=`ls` # 旧形式: バッククォート(非推奨)
# $() はネストできる
backup_name="backup_$(date +%Y%m%d)_$(hostname).tar.gz"
# ネストの例
inner_result=$(echo "The date is $(date +%Y-%m-%d)")
# バッククォートではネストが困難: `echo "The date is \`date +%Y-%m-%d\`"`2.2 算術演算
# 算術演算
result=$((3 + 5)) # → 8
count=$((count + 1)) # インクリメント
echo $((10 / 3)) # → 3(整数除算)
echo $((10 % 3)) # → 1(剰余)
echo $((2 ** 10)) # → 1024(べき乗)
# 複合代入演算子
(( count += 5 )) # count = count + 5
(( count -= 3 )) # count = count - 3
(( count *= 2 )) # count = count * 2
(( count /= 4 )) # count = count / 4
(( count ++ )) # count = count + 1
(( count -- )) # count = count - 1
# 複雑な計算
width=640
height=480
area=$(( width * height ))
echo "Area: $area pixels" # → Area: 307200 pixels
# 16進数・8進数
echo $(( 0xFF )) # → 255(16進数)
echo $(( 077 )) # → 63(8進数)
echo $(( 2#1010 )) # → 10(2進数)
# 小数点計算(bc を使用)
echo "scale=2; 10 / 3" | bc # → 3.33
pi=$(echo "scale=10; 4*a(1)" | bc -l) # → 3.1415926535
result=$(echo "1.5 + 2.3" | bc) # → 3.8
# awk での計算(より高機能)
awk 'BEGIN { printf "%.2f\n", 10/3 }' # → 3.33
awk 'BEGIN { printf "%.4f\n", sqrt(2) }' # → 1.41422.3 特殊変数
# スクリプト引数関連
echo $0 # スクリプト名
echo $1 # 第1引数
echo $2 # 第2引数
echo ${10} # 第10引数(2桁以上は {} が必要)
echo $# # 引数の数
echo $@ # 全引数(個別に展開)
echo $* # 全引数(1つの文字列)
# $@ と $* の違い(重要)
# スクリプトを ./test.sh "hello world" foo bar で実行した場合:
# "$@" → "hello world" "foo" "bar"(3つの引数として展開)
# "$*" → "hello world foo bar"(1つの文字列として展開)
# 実践: 引数を他のコマンドに渡す場合は "$@" を使う
wrapper() {
echo "Calling command with args: $@"
some_command "$@" # 引数を正しく渡す
}
# ステータス・プロセス関連
echo $? # 直前のコマンドの終了ステータス(0=成功)
echo $$ # 現在のプロセスID
echo $! # 直前のバックグラウンドプロセスのPID
echo $- # 現在のシェルオプション
echo $_ # 直前のコマンドの最後の引数
# 終了ステータスの活用
ls /nonexistent 2>/dev/null
if [[ $? -ne 0 ]]; then
echo "ディレクトリが存在しません"
fi
# より簡潔な書き方
if ls /nonexistent 2>/dev/null; then
echo "存在する"
else
echo "存在しない"
fi
# PIDの活用
long_process &
bg_pid=$!
echo "バックグラウンドプロセス: PID=$bg_pid"
wait $bg_pid
echo "プロセス完了: 終了ステータス=$?"2.4 環境変数
# 環境変数の設定
export MY_VAR="value" # 子プロセスに引き継ぐ
MY_VAR="value" # 現在のシェルのみ(子プロセスに渡されない)
# よく使う環境変数
echo "$HOME" # ホームディレクトリ(/home/gaku)
echo "$USER" # ユーザー名(gaku)
echo "$PATH" # コマンド検索パス
echo "$PWD" # カレントディレクトリ
echo "$OLDPWD" # 前のディレクトリ(cd - で使われる)
echo "$SHELL" # ログインシェル(/bin/bash 等)
echo "$HOSTNAME" # ホスト名
echo "$LANG" # ロケール(ja_JP.UTF-8 等)
echo "$TERM" # ターミナルの種類
echo "$EDITOR" # デフォルトエディタ
echo "$RANDOM" # 0-32767 のランダムな整数
echo "$SECONDS" # シェルの起動からの秒数
echo "$LINENO" # 現在の行番号
echo "$BASH_VERSION" # Bash のバージョン
# 環境変数の一覧
env # 全環境変数を表示
printenv # 同上
printenv PATH # 特定の環境変数を表示
# 1回限りの環境変数設定
MY_VAR=value command # command 実行時のみ MY_VAR を設定
LANG=C sort file.txt # sort を C ロケールで実行
DEBUG=1 ./myapp.sh # デバッグモードで実行
# 環境変数の削除
unset MY_VAR # 変数を削除2.5 文字列操作
str="Hello, World!"
# 長さ
echo ${#str} # → 13
# 部分文字列
echo ${str:0:5} # → Hello(位置0から5文字)
echo ${str:7} # → World!(位置7から末尾)
echo ${str: -6} # → orld!(末尾から6文字。スペース必要)
echo ${str:(-6):3} # → orl(末尾から6文字目から3文字)
# 置換
echo ${str/World/Bash} # → Hello, Bash!(最初の1つ)
echo ${str//l/L} # → HeLLo, WorLd!(全て置換)
echo ${str/#Hello/Hi} # → Hi, World!(先頭マッチのみ置換)
echo ${str/%\!/\?} # → Hello, World?(末尾マッチのみ置換)
# 削除(パターン除去)
filename="archive.tar.gz"
echo ${filename%.gz} # → archive.tar(末尾から最短一致)
echo ${filename%%.*} # → archive(末尾から最長一致)
echo ${filename#*.} # → tar.gz(先頭から最短一致)
echo ${filename##*.} # → gz(先頭から最長一致)
# 実践: ファイル名とディレクトリの分離
filepath="/home/user/documents/report.pdf"
echo ${filepath##*/} # → report.pdf(basename 相当)
echo ${filepath%/*} # → /home/user/documents(dirname 相当)
# 拡張子の取得と変更
file="photo.jpg"
ext="${file##*.}" # → jpg
name="${file%.*}" # → photo
new_file="${name}.png" # → photo.png
# 大文字・小文字変換(Bash 4+)
str="Hello World"
echo "${str^^}" # → HELLO WORLD(全て大文字)
echo "${str,,}" # → hello world(全て小文字)
echo "${str^}" # → Hello World(先頭のみ大文字)
echo "${str,}" # → hello World(先頭のみ小文字)
# デフォルト値
echo ${undefined:-"default"} # 未定義なら "default" を表示(代入はしない)
echo ${undefined:="default"} # 未定義なら "default" を代入して表示
echo ${undefined:+"set"} # 定義済みなら "set" を表示、未定義なら空
echo ${undefined:?"error msg"} # 未定義ならエラーメッセージを表示して終了
# 実践: デフォルト値の利用
LOG_DIR="${LOG_DIR:-/var/log/myapp}"
PORT="${PORT:-8080}"
ENV="${ENV:-development}"
echo "Starting on port $PORT in $ENV mode"
# 変数の間接参照
var_name="PATH"
echo "${!var_name}" # → $PATH の内容を表示
# env | grep "^${var_name}=" # 同等の処理2.6 配列の基本(概要)
# インデックス配列の基本(詳細は 01-advanced-scripting.md)
fruits=("apple" "banana" "cherry")
echo "${fruits[0]}" # → apple
echo "${fruits[@]}" # → 全要素
echo "${#fruits[@]}" # → 3(要素数)
# 追加
fruits+=("date")
# ループ
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# コマンドの出力を配列に格納
mapfile -t lines < /etc/hosts # ファイルの各行を配列に
IFS=$'\n' read -d '' -ra output <<< "$(ls -1)" # コマンド出力を配列に3. 条件分岐
3.1 if 文の基本
# if文の基本構文
if [ "$name" = "Gaku" ]; then
echo "Welcome, Gaku!"
elif [ "$name" = "admin" ]; then
echo "Welcome, admin!"
else
echo "Who are you?"
fi
# [[ ]] 形式(bash拡張、推奨)
if [[ "$name" == "Gaku" ]]; then
echo "Match!"
fi
# [ ] と [[ ]] の違い
# [ ] (test コマンド)
# - POSIX 準拠(sh でも使える)
# - 文字列比較は = を使用
# - 論理演算は -a, -o を使用
# - ワード分割が起こるため変数はクォート必須
#
# [[ ]] (bash 拡張)
# - bash/zsh で使える(sh では不可)
# - 文字列比較は == を使用
# - 論理演算は &&, || を使用
# - ワード分割が起こらない(安全)
# - パターンマッチ・正規表現が使える
# - 推奨: 特にこだわりがなければ [[ ]] を使う
# 1行の条件分岐(短い場合に便利)
[[ -f "$file" ]] && echo "File exists"
[[ -f "$file" ]] || echo "File not found"
[[ -d "$dir" ]] && cd "$dir" || echo "Directory not found"3.2 文字列の比較
# 文字列比較演算子
[[ "$a" == "$b" ]] # 等しい
[[ "$a" != "$b" ]] # 等しくない
[[ -z "$str" ]] # 空文字列(zero length)
[[ -n "$str" ]] # 空でない(non-zero length)
[[ "$str" =~ ^[0-9]+$ ]] # 正規表現マッチ
[[ "$str" == pattern* ]] # グロブパターンマッチ(* はワイルドカード)
# 文字列の比較(辞書順)
[[ "$a" < "$b" ]] # a が辞書順で先
[[ "$a" > "$b" ]] # a が辞書順で後
# 正規表現マッチの詳細
input="2025-01-15"
if [[ "$input" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then
echo "Year: ${BASH_REMATCH[0]}" # → 2025-01-15(全体)
echo "Year: ${BASH_REMATCH[1]}" # → 2025
echo "Month: ${BASH_REMATCH[2]}" # → 01
echo "Day: ${BASH_REMATCH[3]}" # → 15
fi
# パターンマッチの例
filename="report_2025.csv"
if [[ "$filename" == *.csv ]]; then
echo "CSV file"
fi
if [[ "$filename" == report_* ]]; then
echo "Report file"
fi
# 空文字列チェックの実践
validate_input() {
local input="$1"
if [[ -z "$input" ]]; then
echo "Error: Input is empty" >&2
return 1
fi
echo "Input: $input"
}3.3 数値の比較
# 数値比較演算子([[ ]] 内で使用)
[[ $a -eq $b ]] # 等しい(equal)
[[ $a -ne $b ]] # 等しくない(not equal)
[[ $a -lt $b ]] # より小さい(less than)
[[ $a -gt $b ]] # より大きい(greater than)
[[ $a -le $b ]] # 以下(less or equal)
[[ $a -ge $b ]] # 以上(greater or equal)
# (( )) 形式(数値演算専用。より直感的)
if (( count > 10 )); then
echo "Too many"
fi
if (( age >= 18 && age <= 65 )); then
echo "Working age"
fi
if (( score == 100 )); then
echo "Perfect!"
fi
# (( )) の注意点
# - 文字列比較はできない
# - 変数名の $ は省略可能: (( count > 10 )) と (( $count > 10 )) は同じ
# - 0 は偽、非0 は真: (( 0 )) → false, (( 1 )) → true
# - 未定義変数は 0 として扱われる3.4 ファイルの判定
# ファイルの存在・種類
[[ -f "$file" ]] # 通常ファイルが存在
[[ -d "$dir" ]] # ディレクトリが存在
[[ -e "$path" ]] # 存在する(種類問わず)
[[ -L "$path" ]] # シンボリックリンク
[[ -p "$path" ]] # 名前付きパイプ(FIFO)
[[ -S "$path" ]] # ソケット
[[ -b "$path" ]] # ブロックデバイス
[[ -c "$path" ]] # キャラクタデバイス
# ファイルの属性
[[ -r "$file" ]] # 読み取り可能
[[ -w "$file" ]] # 書き込み可能
[[ -x "$file" ]] # 実行可能
[[ -s "$file" ]] # サイズが0でない
[[ -O "$file" ]] # 現在のユーザーが所有者
[[ -G "$file" ]] # 現在のグループが所有グループ
# ファイルの比較
[[ "$file1" -nt "$file2" ]] # file1 が新しい(newer than)
[[ "$file1" -ot "$file2" ]] # file1 が古い(older than)
[[ "$file1" -ef "$file2" ]] # 同じinode(同一ファイルかハードリンク)
# 実践: ファイル存在チェック付きの処理
config_file="/etc/myapp/config.yml"
if [[ ! -f "$config_file" ]]; then
echo "Error: Config file not found: $config_file" >&2
exit 1
fi
if [[ ! -r "$config_file" ]]; then
echo "Error: Cannot read config file: $config_file" >&2
exit 1
fi
echo "Loading config from $config_file..."
# ディレクトリの作成(存在しない場合のみ)
log_dir="/var/log/myapp"
if [[ ! -d "$log_dir" ]]; then
mkdir -p "$log_dir"
echo "Created log directory: $log_dir"
fi3.5 論理演算
# [[ ]] 内の論理演算
[[ $a -gt 0 && $a -lt 100 ]] # AND
[[ $a -eq 0 || $a -eq 1 ]] # OR
[[ ! -f "$file" ]] # NOT
# 複数条件の組み合わせ
if [[ -f "$file" && -r "$file" && -s "$file" ]]; then
echo "File exists, is readable, and is not empty"
fi
# 条件の優先順位(グルーピング)
if [[ ( $a -gt 0 && $a -lt 10 ) || $a -eq 100 ]]; then
echo "a is 1-9 or 100"
fi
# コマンドの論理演算
command1 && command2 # command1 が成功したら command2 を実行
command1 || command2 # command1 が失敗したら command2 を実行
command1 && command2 || command3 # 成功なら command2、失敗なら command3
# 実践パターン
cd "$dir" && echo "Moved to $dir" || echo "Failed to move to $dir"
grep -q "pattern" "$file" && echo "Found" || echo "Not found"3.6 case 文
# case文の基本
case "$1" in
start)
echo "Starting..."
;;
stop)
echo "Stopping..."
;;
restart)
echo "Restarting..."
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
# パターンマッチの活用
case "$filename" in
*.tar.gz|*.tgz)
tar xzf "$filename"
;;
*.tar.bz2)
tar xjf "$filename"
;;
*.zip)
unzip "$filename"
;;
*.gz)
gunzip "$filename"
;;
*.tar)
tar xf "$filename"
;;
*)
echo "Unknown archive format: $filename"
;;
esac
# case の高度なパターン
case "$input" in
[0-9])
echo "Single digit"
;;
[0-9][0-9])
echo "Two digits"
;;
[a-zA-Z]*)
echo "Starts with a letter"
;;
"")
echo "Empty string"
;;
*)
echo "Other"
;;
esac
# yes/no の確認プロンプト
read -p "Continue? [y/N] " response
case "$response" in
[yY]|[yY][eE][sS])
echo "Proceeding..."
;;
*)
echo "Aborted."
exit 0
;;
esac
# Bash 4+ の case: ;& と ;;&
# ;; → マッチしたら case を終了(通常の動作)
# ;& → 次のパターンも無条件で実行(C の fall-through)
# ;;& → 次のパターンもチェックして実行(続行チェック)
case "$level" in
critical)
echo "Paging on-call"
;& # fall-through
error)
echo "Sending alert email"
;& # fall-through
warning)
echo "Logging to file"
;;
esac3.7 条件分岐の実践パターン
# パターン1: コマンドの存在チェック
if command -v docker &>/dev/null; then
echo "Docker is installed"
else
echo "Docker is not installed"
exit 1
fi
# パターン2: OS判定
case "$(uname -s)" in
Linux)
echo "Running on Linux"
PACKAGE_MANAGER="apt"
;;
Darwin)
echo "Running on macOS"
PACKAGE_MANAGER="brew"
;;
CYGWIN*|MINGW*|MSYS*)
echo "Running on Windows"
;;
*)
echo "Unknown OS"
;;
esac
# パターン3: 数値の範囲判定
validate_port() {
local port="$1"
if ! [[ "$port" =~ ^[0-9]+$ ]]; then
echo "Error: Not a number: $port" >&2
return 1
fi
if (( port < 1 || port > 65535 )); then
echo "Error: Port must be 1-65535: $port" >&2
return 1
fi
if (( port < 1024 )); then
echo "Warning: Privileged port (requires root): $port" >&2
fi
return 0
}
# パターン4: 複数条件のバリデーション
validate_config() {
local errors=0
if [[ -z "$DB_HOST" ]]; then
echo "Error: DB_HOST is not set" >&2
(( errors++ ))
fi
if [[ -z "$DB_NAME" ]]; then
echo "Error: DB_NAME is not set" >&2
(( errors++ ))
fi
if [[ -z "$DB_USER" ]]; then
echo "Error: DB_USER is not set" >&2
(( errors++ ))
fi
if (( errors > 0 )); then
echo "Found $errors configuration error(s)" >&2
return 1
fi
return 0
}4. ループ
4.1 for ループ
# 基本的な for ループ
for i in 1 2 3 4 5; do
echo "Number: $i"
done
# C言語風 for
for ((i = 0; i < 10; i++)); do
echo "Index: $i"
done
# 範囲指定(brace expansion)
for i in {1..10}; do # 1〜10
echo "$i"
done
for i in {0..100..5}; do # 0〜100, 5刻み
echo "$i"
done
for letter in {a..z}; do # a〜z
echo "$letter"
done
# seq コマンド(変数で範囲指定が必要な場合)
max=10
for i in $(seq 1 "$max"); do
echo "$i"
done
for i in $(seq 0 5 100); do # 0から100まで5刻み
echo "$i"
done
# ファイルに対するループ
for file in *.txt; do
echo "Processing: $file"
done
# 複数のパターン
for file in *.txt *.csv *.json; do
echo "File: $file"
done
# パターンにマッチするファイルがない場合の対策
shopt -s nullglob # マッチしない場合は空に展開
for file in *.txt; do
echo "Processing: $file"
done
shopt -u nullglob # 元に戻す
# コマンド出力に対するループ
for user in $(cut -d: -f1 /etc/passwd); do
echo "User: $user"
done
# 配列のループ
servers=("web1" "web2" "web3" "db1")
for server in "${servers[@]}"; do
echo "Checking: $server"
done
# 引数のループ
for arg in "$@"; do
echo "Argument: $arg"
done
# インデックス付きのループ
items=("apple" "banana" "cherry")
for i in "${!items[@]}"; do
echo "$i: ${items[$i]}"
done4.2 while ループ
# 基本的な while ループ
count=0
while [[ $count -lt 5 ]]; do
echo "Count: $count"
((count++))
done
# 無限ループ
while true; do
echo "Running..."
sleep 1
# break で脱出
done
# (( )) を使った while
n=1
while (( n <= 100 )); do
echo "$n"
(( n++ ))
done
# ファイルを1行ずつ読む(重要パターン)
while IFS= read -r line; do
echo "Line: $line"
done < input.txt
# IFS= と -r の意味
# IFS= → 先頭と末尾の空白を保持する
# -r → バックスラッシュを特殊文字として扱わない
# この組み合わせで行を正確に読み取る
# パイプからの読み取り(サブシェルに注意)
# NG: パイプの右側はサブシェルなので変数が残らない
count=0
cat file.txt | while read -r line; do
((count++))
done
echo "Count: $count" # → 0(サブシェル内の変数は失われる)
# OK: リダイレクトを使う
count=0
while read -r line; do
((count++))
done < file.txt
echo "Count: $count" # → 正しい値
# OK: プロセス置換を使う
count=0
while read -r line; do
((count++))
done < <(cat file.txt)
echo "Count: $count" # → 正しい値
# CSVファイルの処理
while IFS=',' read -r name age city; do
echo "Name: $name, Age: $age, City: $city"
done < data.csv
# /etc/passwd の処理
while IFS=':' read -r user _ uid gid _ home shell; do
echo "User: $user (UID: $uid, Home: $home)"
done < /etc/passwd
# コマンド出力を行単位で処理
while read -r pid user cpu mem command; do
if (( $(echo "$cpu > 50" | bc -l) )); then
echo "High CPU: $command ($cpu%)"
fi
done < <(ps aux --no-headers)4.3 until ループ
# until ループ(条件が真になるまで繰り返す)
until ping -c1 server.com &>/dev/null; do
echo "Waiting for server..."
sleep 5
done
echo "Server is up!"
# サービスの起動待ち
until curl -s http://localhost:8080/health > /dev/null 2>&1; do
echo "Waiting for application to start..."
sleep 2
done
echo "Application is ready!"
# タイムアウト付き待機
timeout=60
elapsed=0
until docker ps | grep -q "my-container"; do
if (( elapsed >= timeout )); then
echo "Timeout: Container did not start within ${timeout}s"
exit 1
fi
echo "Waiting for container... (${elapsed}s)"
sleep 5
(( elapsed += 5 ))
done
echo "Container is running!"4.4 ループ制御
# break / continue
for i in {1..10}; do
[[ $i -eq 3 ]] && continue # 3をスキップ
[[ $i -eq 8 ]] && break # 8で終了
echo "$i"
done
# 出力: 1 2 4 5 6 7
# ネストされたループでの break/continue
for i in {1..3}; do
for j in {1..3}; do
if (( i == 2 && j == 2 )); then
break 2 # 外側のループも break(数字で深さ指定)
fi
echo "$i,$j"
done
done
# continue でもネストの深さを指定可能
for i in {1..3}; do
for j in {1..3}; do
if (( j == 2 )); then
continue 2 # 外側のループの次のイテレーションへ
fi
echo "$i,$j"
done
done4.5 ループの実践パターン
# パターン1: リトライ処理
max_retries=5
retry_count=0
until some_command; do
((retry_count++))
if (( retry_count >= max_retries )); then
echo "Failed after $max_retries retries"
exit 1
fi
echo "Retry $retry_count/$max_retries..."
sleep $(( retry_count * 2 )) # 指数バックオフ
done
# パターン2: ファイルの一括処理(find + while)
find /var/log -name "*.log" -mtime +30 -print0 | while IFS= read -r -d '' file; do
echo "Compressing: $file"
gzip "$file"
done
# -print0 と -d '' でファイル名のスペース・改行に対応
# パターン3: プログレスバー
total=100
for ((i = 1; i <= total; i++)); do
percent=$(( i * 100 / total ))
bar=$(printf '%*s' $(( percent / 2 )) '' | tr ' ' '#')
printf "\r[%-50s] %d%%" "$bar" "$percent"
sleep 0.05
done
echo ""
# パターン4: メニューの表示(select)
PS3="Select an option: "
select opt in "Start" "Stop" "Status" "Quit"; do
case "$opt" in
"Start") echo "Starting..."; ;;
"Stop") echo "Stopping..."; ;;
"Status") echo "Running"; ;;
"Quit") break; ;;
*) echo "Invalid option"; ;;
esac
done
# パターン5: ディレクトリの再帰処理
process_dir() {
local dir="$1"
for item in "$dir"/*; do
if [[ -d "$item" ]]; then
process_dir "$item" # 再帰呼び出し
elif [[ -f "$item" ]]; then
echo "File: $item"
fi
done
}
process_dir "/path/to/start"5. 関数
5.1 関数の定義と呼び出し
# 関数定義(2つの書き方)
greet() {
local name="$1" # local: 関数内変数
echo "Hello, $name!"
}
function greet2 {
local name="$1"
echo "Hi, $name!"
}
# 呼び出し
greet "Gaku" # → Hello, Gaku!
greet2 "World" # → Hi, World!
# 関数の引数
show_args() {
echo "Function name: $FUNCNAME"
echo "Argument count: $#"
echo "First arg: $1"
echo "Second arg: $2"
echo "All args: $@"
}
show_args "hello" "world" "foo"5.2 戻り値
# return で終了ステータスを返す(0=成功、1-255=失敗)
is_even() {
if (( $1 % 2 == 0 )); then
return 0 # 成功(真)
else
return 1 # 失敗(偽)
fi
}
if is_even 4; then
echo "4 is even"
fi
# 値を返す(stdout 経由)
get_timestamp() {
date +%Y%m%d_%H%M%S
}
ts=$(get_timestamp)
echo "Timestamp: $ts"
# 複数の値を返す
get_dimensions() {
local file="$1"
local width=$(identify -format "%w" "$file" 2>/dev/null)
local height=$(identify -format "%h" "$file" 2>/dev/null)
echo "$width $height" # スペース区切りで出力
}
read -r width height <<< "$(get_dimensions photo.jpg)"
echo "Width: $width, Height: $height"
# 配列を返す(改行区切り)
list_users() {
cut -d: -f1 /etc/passwd | sort
}
mapfile -t users < <(list_users)
echo "Total users: ${#users[@]}"
# グローバル変数で結果を返す(非推奨だが知っておくべき)
calculate() {
RESULT=$(( $1 + $2 )) # グローバル変数
}
calculate 3 5
echo "Result: $RESULT" # → 85.3 local 変数とスコープ
# local を使わないと全てグローバル
bad_function() {
x=100 # グローバル!
}
bad_function
echo "$x" # → 100(関数外でもアクセス可能)
# local で関数内に閉じ込める
good_function() {
local x=100 # 関数内のみ
}
good_function
echo "$x" # → (空。アクセスできない)
# 実践: 常に local を使う
process_file() {
local file="$1"
local content
local line_count
content=$(cat "$file")
line_count=$(wc -l < "$file")
echo "File: $file ($line_count lines)"
}
# nameref(Bash 4.3+)— 参照渡し
set_value() {
local -n ref=$1 # nameref: 呼び出し元の変数を参照
ref="new value"
}
my_var="old value"
set_value my_var
echo "$my_var" # → new value5.4 エラーハンドリング付き関数
# エラーハンドリング付き関数
safe_cd() {
local dir="$1"
if [[ ! -d "$dir" ]]; then
echo "Error: Directory '$dir' not found" >&2
return 1
fi
cd "$dir" || return 1
}
# die 関数(スクリプト全体を終了する場合)
die() {
echo "FATAL: $*" >&2
exit 1
}
# 使用例
safe_cd "/opt/myapp" || die "Cannot access application directory"
# バリデーション関数
require_command() {
local cmd="$1"
if ! command -v "$cmd" &>/dev/null; then
die "Required command not found: $cmd"
fi
}
require_command "docker"
require_command "git"
require_command "jq"
# ファイル操作の安全なラッパー
safe_rm() {
local target="$1"
# 危険なパスを拒否
case "$target" in
/|/home|/usr|/var|/etc|/tmp)
die "Refusing to delete critical directory: $target"
;;
esac
if [[ -e "$target" ]]; then
rm -rf "$target"
echo "Removed: $target"
else
echo "Warning: Does not exist: $target" >&2
fi
}5.5 関数の実践パターン
# パターン1: ログ出力関数群
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[0;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%H:%M:%S') $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%H:%M:%S') $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%H:%M:%S') $*" >&2; }
log_debug() { [[ "${DEBUG:-0}" == "1" ]] && echo -e "${BLUE}[DEBUG]${NC} $(date '+%H:%M:%S') $*"; }
# パターン2: 確認プロンプト
confirm() {
local message="${1:-Continue?}"
local default="${2:-n}"
local prompt
if [[ "$default" == "y" ]]; then
prompt="$message [Y/n] "
else
prompt="$message [y/N] "
fi
read -p "$prompt" response
response="${response:-$default}"
[[ "$response" =~ ^[yY] ]]
}
# 使用例
if confirm "Delete all logs?"; then
rm -rf /var/log/myapp/*
fi
# パターン3: スピナー表示
spinner() {
local pid=$1
local spin='|/-\'
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r%s" "${spin:$((i++ % 4)):1}"
sleep 0.1
done
printf "\r"
}
# 使用例
long_process &
spinner $!
wait $!
# パターン4: タイムアウト付きの関数
wait_for_port() {
local host="$1"
local port="$2"
local timeout="${3:-30}"
local elapsed=0
while ! nc -z "$host" "$port" 2>/dev/null; do
if (( elapsed >= timeout )); then
echo "Timeout waiting for $host:$port" >&2
return 1
fi
sleep 1
(( elapsed++ ))
done
echo "$host:$port is available"
return 0
}
wait_for_port "localhost" 5432 60 || die "Database not available"6. 入出力
6.1 標準入力の読み取り
# 基本的な読み取り
read -p "Enter your name: " name
read -sp "Enter password: " password # -s: 非表示
echo "" # 改行(-s は改行を出力しない)
read -t 10 -p "Quick! " answer # -t: タイムアウト(秒)
read -r line # -r: バックスラッシュを特殊扱いしない
read -n 1 -p "Press any key..." key # -n: 指定文字数で自動確定
# 複数の変数に分割して読む
echo "John 25 Tokyo" | read -r name age city
# ↑ パイプだとサブシェルになるので注意
# 正しいやり方
read -r name age city <<< "John 25 Tokyo"
echo "$name is $age years old from $city"
# 配列として読み取り
read -ra words <<< "hello world foo bar"
echo "${words[0]}" # → hello
echo "${words[2]}" # → foo
# デフォルト値付きの入力
read -p "Port [8080]: " port
port="${port:-8080}"
# 入力のバリデーション
while true; do
read -p "Enter a number (1-100): " num
if [[ "$num" =~ ^[0-9]+$ ]] && (( num >= 1 && num <= 100 )); then
break
fi
echo "Invalid input. Please try again."
done
echo "You entered: $num"6.2 リダイレクト
# 基本的なリダイレクト
echo "hello" > file.txt # 上書き(ファイルが存在すれば消去して書き込み)
echo "world" >> file.txt # 追記
command 2> error.log # 標準エラーをファイルへ
command > out.log 2>&1 # 標準出力と標準エラーの両方をファイルへ
command &> both.log # 同上(bash省略形)
command > /dev/null 2>&1 # 全出力を捨てる
command &>/dev/null # 同上(bash省略形)
# ファイルディスクリプタ
# 0 = stdin(標準入力)
# 1 = stdout(標準出力)
# 2 = stderr(標準エラー)
# 標準エラーのみ表示(標準出力は捨てる)
command > /dev/null # stdout だけ捨てる
command 2>/dev/null # stderr だけ捨てる
# 標準出力と標準エラーを別ファイルに
command > stdout.log 2> stderr.log
# 標準出力と標準エラーを入れ替え
command 3>&1 1>&2 2>&3 # stdout ↔ stderr を入れ替え
# 追加のファイルディスクリプタ
exec 3> output.log # FD3 を output.log に割り当て
echo "Log message" >&3 # FD3 に書き込み
exec 3>&- # FD3 を閉じる
# 入力リダイレクト
command < input.txt # ファイルからの入力
sort < unsorted.txt > sorted.txt # 入力と出力のリダイレクト
# noclobber(上書き防止)
set -o noclobber # > での上書きを禁止
echo "test" > existing_file # → エラー
echo "test" >| existing_file # >| で強制上書き
set +o noclobber # 解除
# tee コマンド(画面とファイルの両方に出力)
command | tee output.log # stdout に表示しつつファイルにも保存
command | tee -a output.log # 追記モード
command 2>&1 | tee output.log # stderr も含めて6.3 ヒアドキュメント
# ヒアドキュメント(変数展開あり)
cat <<EOF
Hello, $name!
Today is $(date).
Your home directory is $HOME
EOF
# ヒアドキュメント(変数展開なし)
cat <<'EOF'
$name は展開されない
$(date) も展開されない
EOF
# ヒアドキュメントでファイルを作成
cat > /tmp/config.ini <<EOF
[database]
host=$DB_HOST
port=$DB_PORT
name=$DB_NAME
EOF
# インデント付きヒアドキュメント(<<- でタブを除去)
if true; then
cat <<-EOF
Hello, $name!
This is indented with tabs.
EOF
fi
# 注意: <<- はタブのみ除去。スペースは除去されない
# ヒアストリング(1行の入力)
grep "pattern" <<< "$variable"
read -r first rest <<< "hello world foo"
echo "$first" # → hello
echo "$rest" # → world foo
bc <<< "10 * 20 + 5" # → 205
# 実践: SSH でリモートコマンド実行
ssh user@server <<'REMOTE'
cd /opt/myapp
git pull
npm install
pm2 restart all
REMOTE
# 実践: SQL の実行
mysql -u root -p"$DB_PASS" <<EOF
USE mydb;
SELECT COUNT(*) FROM users WHERE active = 1;
EOF6.4 パイプとプロセス置換
# パイプ(コマンドの出力を次のコマンドの入力に)
ls -la | sort -k5 -n # ファイルサイズ順にソート
cat access.log | grep "404" | wc -l # 404 エラーの数
# 名前付きパイプ(FIFO)
mkfifo /tmp/mypipe
command1 > /tmp/mypipe &
command2 < /tmp/mypipe
rm /tmp/mypipe
# プロセス置換(一時ファイルの代わり)
diff <(ls dir1) <(ls dir2) # 2つのディレクトリの内容を比較
diff <(sort file1) <(sort file2) # ソートしながら比較
# 実践: 2つのコマンドの出力を比較
diff <(ssh server1 "cat /etc/nginx/nginx.conf") \
<(ssh server2 "cat /etc/nginx/nginx.conf")
# プロセス置換で複数の出力先
tee >(gzip > output.gz) >(wc -l > count.txt) < input.txt
# パイプのステータス(PIPESTATUS 配列)
false | true | false
echo "${PIPESTATUS[@]}" # → 1 0 1(各コマンドの終了ステータス)7. デバッグ
7.1 デバッグオプション
# set -x: 実行コマンドを表示(最も重要なデバッグツール)
set -x
echo "hello" # + echo hello が表示される
set +x # デバッグ表示を終了
# スクリプト全体をデバッグモードで実行
bash -x script.sh
# 部分的なデバッグ
set -x
# デバッグしたい処理
problematic_function
set +x
# set -v: コマンドを読み取り時に表示(展開前の状態)
set -v
echo "$HOME" # echo "$HOME" が表示される(展開前)
set +v
# PS4 でデバッグ出力をカスタマイズ
export PS4='+ ${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x
# 出力例: + script.sh:25: main(): echo hello
# BASH_XTRACEFD でデバッグ出力を別ファイルに
exec 4> /tmp/debug.log
export BASH_XTRACEFD=4
set -x
# デバッグ出力は /tmp/debug.log に書き込まれる
# stdout/stderr は通常通り画面に表示7.2 エラーのトラブルシューティング
# よくあるエラーと対処法
# 1. "unexpected end of file"
# → if, while, for, case の閉じ忘れ
# → クォートの閉じ忘れ
# 2. "command not found"
# → PATH の確認: echo $PATH
# → 実行権限の確認: ls -la script.sh
# → シバンの確認
# 3. "unbound variable"
# → set -u を使用中に未定義変数を参照
# → デフォルト値を設定: ${var:-default}
# 4. "ambiguous redirect"
# → 変数が空の場合: > $file → > "" になる
# → クォート: > "$file"
# 5. "too many arguments"
# → [ ] 内でクォートなしの変数
# → [[ ]] を使う
# シンタックスチェック(実行せずに文法確認)
bash -n script.sh # 文法エラーのみ検出
# ShellCheck(静的解析ツール。強力に推奨)
# brew install shellcheck
shellcheck script.sh # 潜在的な問題を検出
shellcheck -s bash script.sh # bash 方言を指定
shellcheck -e SC2086 script.sh # 特定の警告を除外
# ShellCheck が検出する典型的な問題
# SC2086: Double quote to prevent globbing and word splitting
# SC2034: Variable appears unused
# SC2046: Quote this to prevent word splitting
# SC2016: Expressions don't expand in single quotes7.3 デバッグの実践テクニック
# トラップで実行行を表示
trap 'echo "DEBUG: Line $LINENO: $BASH_COMMAND"' DEBUG
# 関数のトレース
func_trace() {
echo "TRACE: ${FUNCNAME[1]}() called from line ${BASH_LINENO[0]}"
}
# 変数の状態を確認するヘルパー
dump_vars() {
echo "=== Variable Dump ==="
echo "PWD=$PWD"
echo "count=$count"
echo "file=$file"
echo "===================="
}
# タイミング情報付きデバッグ
debug_time() {
echo "[$(date '+%H:%M:%S.%N')] $*" >&2
}
# 実践: 問題の切り分け
# 1. まず bash -n で文法チェック
# 2. bash -x で実行トレース
# 3. shellcheck で静的解析
# 4. 問題箇所の前後に echo を入れて変数値を確認
# 5. set -x / set +x で部分的にデバッグ8. 実践的なスクリプトテンプレート
8.1 堅牢なスクリプトのテンプレート
#!/usr/bin/env bash
#
# スクリプト名: template.sh
# 説明: 堅牢なスクリプトのテンプレート
# 使い方: ./template.sh [OPTIONS] ARG
#
set -euo pipefail
# 定数
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly VERSION="1.0.0"
# デフォルト値
VERBOSE=false
DRY_RUN=false
OUTPUT_DIR="."
# 色定義
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[0;33m'
readonly NC='\033[0m'
# ── ログ関数 ──
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
die() { log_error "$@"; exit 1; }
# ── クリーンアップ ──
TMPDIR=""
cleanup() {
local exit_code=$?
if [[ -n "$TMPDIR" && -d "$TMPDIR" ]]; then
rm -rf "$TMPDIR"
fi
exit $exit_code
}
trap cleanup EXIT
# ── ヘルプ ──
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] FILE...
Description:
このスクリプトの説明を書く
Options:
-h, --help このヘルプを表示
-V, --version バージョンを表示
-v, --verbose 詳細出力
-n, --dry-run 実際には実行しない
-o, --output DIR 出力ディレクトリ (default: .)
Examples:
$SCRIPT_NAME -v input.txt
$SCRIPT_NAME --output /tmp -n *.csv
EOF
}
# ── 引数パース ──
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-V|--version) echo "$SCRIPT_NAME $VERSION"; exit 0 ;;
-v|--verbose) VERBOSE=true; shift ;;
-n|--dry-run) DRY_RUN=true; shift ;;
-o|--output) OUTPUT_DIR="$2"; shift 2 ;;
--) shift; break ;;
-*) die "Unknown option: $1" ;;
*) break ;;
esac
done
# 残りの引数
FILES=("$@")
# バリデーション
if [[ ${#FILES[@]} -eq 0 ]]; then
die "No input files specified. Use -h for help."
fi
}
# ── メイン処理 ──
main() {
parse_args "$@"
# 一時ディレクトリ
TMPDIR=$(mktemp -d)
log_info "Processing ${#FILES[@]} file(s)..."
for file in "${FILES[@]}"; do
if [[ ! -f "$file" ]]; then
log_warn "File not found: $file"
continue
fi
if [[ "$VERBOSE" == true ]]; then
log_info "Processing: $file"
fi
if [[ "$DRY_RUN" == true ]]; then
log_info "[DRY RUN] Would process: $file"
continue
fi
# ここに実際の処理を書く
process_file "$file"
done
log_info "Done!"
}
process_file() {
local file="$1"
# 処理の実装
echo "Processing: $file"
}
main "$@"8.2 バックアップスクリプトの例
#!/usr/bin/env bash
set -euo pipefail
# 設定
readonly BACKUP_SOURCE="/home/user/data"
readonly BACKUP_DEST="/mnt/backup"
readonly MAX_BACKUPS=7
readonly DATE=$(date +%Y%m%d_%H%M%S)
readonly BACKUP_NAME="backup_${DATE}.tar.gz"
readonly LOG_FILE="/var/log/backup.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# バックアップ先の空き容量チェック
check_disk_space() {
local available
available=$(df -BM "$BACKUP_DEST" | awk 'NR==2 {print $4}' | tr -d 'M')
local source_size
source_size=$(du -sm "$BACKUP_SOURCE" | awk '{print $1}')
if (( available < source_size * 2 )); then
log "WARNING: Low disk space. Available: ${available}MB, Source: ${source_size}MB"
return 1
fi
}
# 古いバックアップの削除
cleanup_old_backups() {
local count
count=$(ls -1 "$BACKUP_DEST"/backup_*.tar.gz 2>/dev/null | wc -l)
if (( count > MAX_BACKUPS )); then
local to_delete=$(( count - MAX_BACKUPS ))
ls -1t "$BACKUP_DEST"/backup_*.tar.gz | tail -n "$to_delete" | while read -r file; do
log "Removing old backup: $file"
rm -f "$file"
done
fi
}
# メイン
main() {
log "=== Backup started ==="
check_disk_space || exit 1
log "Creating backup: $BACKUP_NAME"
tar czf "$BACKUP_DEST/$BACKUP_NAME" -C "$(dirname "$BACKUP_SOURCE")" "$(basename "$BACKUP_SOURCE")"
local size
size=$(du -sh "$BACKUP_DEST/$BACKUP_NAME" | awk '{print $1}')
log "Backup created: $BACKUP_NAME ($size)"
cleanup_old_backups
log "=== Backup completed ==="
}
main "$@"8.3 ファイル監視スクリプトの例
#!/usr/bin/env bash
set -euo pipefail
# ファイルの変更を検知して処理を実行する
readonly WATCH_DIR="${1:-.}"
readonly INTERVAL="${2:-5}"
declare -A file_mtimes
get_mtime() {
stat -c %Y "$1" 2>/dev/null || echo "0"
}
scan_files() {
local changed=false
for file in "$WATCH_DIR"/*; do
[[ -f "$file" ]] || continue
local current_mtime
current_mtime=$(get_mtime "$file")
local basename
basename=$(basename "$file")
if [[ -z "${file_mtimes[$basename]:-}" ]]; then
file_mtimes[$basename]="$current_mtime"
echo "[NEW] $basename"
changed=true
elif [[ "${file_mtimes[$basename]}" != "$current_mtime" ]]; then
file_mtimes[$basename]="$current_mtime"
echo "[MOD] $basename"
changed=true
fi
done
if [[ "$changed" == true ]]; then
echo "[$(date '+%H:%M:%S')] Changes detected. Running build..."
# ここにビルドコマンドなどを入れる
fi
}
echo "Watching $WATCH_DIR every ${INTERVAL}s..."
while true; do
scan_files
sleep "$INTERVAL"
doneFAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
まとめ
| 構文 | 用途 | 例 |
|---|---|---|
$var / ${var} |
変数参照 | echo "$name" |
$(command) |
コマンド置換 | today=$(date) |
$((expr)) |
算術演算 | $((count + 1)) |
[[ cond ]] |
条件判定 | [[ -f "$file" ]] |
(( expr )) |
数値条件 | (( count > 10 )) |
for / while / until |
ループ | for i in {1..10}; do |
case |
パターンマッチ分岐 | case "$1" in ... |
func() { ... } |
関数定義 | greet() { echo "Hi"; } |
local var |
関数内ローカル変数 | local name="$1" |
set -euo pipefail |
堅牢性設定 | スクリプト冒頭に |
trap '...' SIGNAL |
シグナルハンドラ | trap cleanup EXIT |
> / >> / 2> |
リダイレクト | cmd > file 2>&1 |
<<EOF |
ヒアドキュメント | 複数行入力 |
<<< |
ヒアストリング | 1行入力 |
次に読むべきガイド
参考文献
- Shotts, W. "The Linux Command Line." 2nd Ed, Ch.24-36, 2019.
- "Bash Reference Manual." GNU, 2024.
- "Google Shell Style Guide." google.github.io/styleguide.
- Cooper, M. "Advanced Bash-Scripting Guide." TLDP, 2014.
- "ShellCheck Wiki." github.com/koalaman/shellcheck/wiki.
- Robbins, A., Beebe, N. "Classic Shell Scripting." O'Reilly, 2005.
- Taylor, D. "Wicked Cool Shell Scripts." 2nd Ed, No Starch Press, 2016.