シェルスクリプト実践 - 関数・配列・ファイル操作の応用テクニック
シェルスクリプトの基礎をマスターしたら、実践的な高度技術を身につけよう。関数、配列、ファイル操作、エラー処理から実際の業務で使える実用例まで、プロフェッショナルなスクリプト作成技術を解説する。
関数
関数の定義と呼び出し
基本的な関数
#!/bin/bash
greet() {
echo "こんにちは、$1さん!"
}
greet "太郎"
greet "花子"戻り値のある関数
#!/bin/bash
square() {
local num=$1
local result=$((num * num))
echo $result
}
number=5
result=$(square $number)
echo "$number の平方は $result です"複数引数の関数
#!/bin/bash
file_info() {
local filepath=$1
if [ -f "$filepath" ]; then
echo "ファイル: $filepath"
echo "サイズ: $(wc -c < "$filepath") bytes"
echo "行数: $(wc -l < "$filepath") lines"
echo "最終更新: $(stat -c %y "$filepath")"
else
echo "ファイル $filepath が存在しません"
return 1
fi
}
file_info "/etc/passwd"
file_info "nonexistent.txt"高度な関数
ローカル変数とグローバル変数
#!/bin/bash
global_var="グローバル"
demo_scope() {
local local_var="ローカル"
global_var="変更されたグローバル"
echo "関数内: local_var = $local_var"
echo "関数内: global_var = $global_var"
}
echo "関数呼び出し前: global_var = $global_var"
demo_scope
echo "関数呼び出し後: global_var = $global_var"
echo "関数外: local_var = $local_var" # 空になる再帰関数
#!/bin/bash
factorial() {
local n=$1
if [ $n -le 1 ]; then
echo 1
else
local prev=$(factorial $((n - 1)))
echo $((n * prev))
fi
}
for i in {1..5}; do
result=$(factorial $i)
echo "$i! = $result"
doneエラーハンドリング付き関数
#!/bin/bash
safe_mkdir() {
local dir_path=$1
if [ -z "$dir_path" ]; then
echo "エラー: ディレクトリパスが指定されていません" >&2
return 1
fi
if [ -d "$dir_path" ]; then
echo "ディレクトリ $dir_path は既に存在します"
return 0
fi
if mkdir -p "$dir_path" 2>/dev/null; then
echo "ディレクトリ $dir_path を作成しました"
return 0
else
echo "エラー: ディレクトリ $dir_path の作成に失敗しました" >&2
return 1
fi
}
safe_mkdir "/tmp/test_dir"
safe_mkdir "/root/forbidden" # 権限エラーの例配列
配列の基本操作
配列の作成と要素アクセス
#!/bin/bash
fruits=("apple" "banana" "orange" "grape")
numbers=(1 2 3 4 5)
echo "最初の果物: ${fruits[0]}"
echo "3番目の数字: ${numbers[2]}"
echo "全ての果物: ${fruits[@]}"
echo "果物の数: ${#fruits[@]}"配列への要素追加と削除
#!/bin/bash
colors=("red" "green" "blue")
echo "初期配列: ${colors[@]}"
colors+=("yellow")
echo "追加後: ${colors[@]}"
unset colors[1] # "green"を削除
echo "削除後: ${colors[@]}"
echo "インデックス: ${!colors[@]}"連想配列(Bash 4.0以降)
#!/bin/bash
declare -A person
person["name"]="田中太郎"
person["age"]="30"
person["city"]="東京"
echo "名前: ${person["name"]}"
echo "年齢: ${person["age"]}"
echo "全キー: ${!person[@]}"配列の実践的な使用例
#!/bin/bash
log_patterns=("/var/log/*.log" "/tmp/*.log" "$HOME/*.log")
for pattern in "${log_patterns[@]}"; do
echo "パターン: $pattern"
files=($pattern)
if [ ${#files[@]} -gt 0 ] && [ -f "${files[0]}" ]; then
for file in "${files[@]}"; do
if [ -f "$file" ]; then
size=$(wc -c < "$file")
echo " - $file ($size bytes)"
fi
done
else
echo " - マッチするファイルなし"
fi
doneファイル操作
ファイルの読み書き
ファイル読み込みの様々な方法
#!/bin/bash
filename="data.txt"
# 方法1: while read を使用
while IFS= read -r line || [ -n "$line" ]; do
echo "行: $line"
done < "$filename"
# 方法2: 配列への読み込み
mapfile -t lines < "$filename"
for i in "${!lines[@]}"; do
echo "行 $((i+1)): ${lines[i]}"
doneファイル書き込み
#!/bin/bash output_file="output.txt" echo "新しいファイル内容" > "$output_file" echo "追加の行1" >> "$output_file" cat << EOF >> "$output_file" 複数行のテキスト 2行目 3行目 EOF cat "$output_file"
CSVファイルの処理
#!/bin/bash
csv_file="employees.csv"
cat << EOF > "$csv_file"
名前,年齢,部署
田中,30,開発部
佐藤,25,営業部
鈴木,35,管理部
EOF
tail -n +2 "$csv_file" | while IFS=',' read -r name age dept; do
echo "社員: $name (${age}歳) - $dept"
doneファイル操作の高度な例
#!/bin/bash
source_dir="/path/to/source"
backup_dir="/path/to/backup"
backup_files() {
local src="$1"
local dest="$2"
if [ ! -d "$src" ]; then
echo "エラー: ソースディレクトリが存在しません: $src"
return 1
fi
mkdir -p "$dest"
rsync -av --delete "$src/" "$dest/"
echo "バックアップ完了: $src -> $dest"
}
log_file="/tmp/backup.log"
{
echo "バックアップ開始: $(date)"
backup_files "$source_dir" "$backup_dir"
echo "バックアップ終了: $(date)"
} >> "$log_file" 2>&1エラー処理
エラー処理のベストプラクティス
set オプションによる厳密なエラー処理
#!/bin/bash set -euo pipefail # set -e: コマンドが失敗したら即座に終了 # set -u: 未定義変数を使用したらエラー # set -o pipefail: パイプラインで一つでも失敗したらエラー trap 'echo "エラーが発生しました: 行番号 $LINENO" >&2' ERR echo "正常処理1"
手動エラーチェック
#!/bin/bash
process_file() {
local filename="$1"
if [ ! -f "$filename" ]; then
echo "エラー: ファイル '$filename' が見つかりません" >&2
return 1
fi
if [ ! -r "$filename" ]; then
echo "エラー: ファイル '$filename' の読み取り権限がありません" >&2
return 1
fi
local line_count
if ! line_count=$(wc -l < "$filename" 2>/dev/null); then
echo "エラー: 行数カウントに失敗しました" >&2
return 1
fi
echo "ファイル '$filename' の行数: $line_count"
return 0
}
if process_file "test.txt"; then
echo "処理が正常に完了しました"
else
echo "処理が失敗しました"
exit 1
fiログ記録付きエラー処理
#!/bin/bash
LOG_FILE="/tmp/script.log"
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
handle_error() {
local exit_code=$?
local line_no=$1
log "ERROR" "スクリプトがエラーで終了しました (終了コード: $exit_code, 行: $line_no)"
exit $exit_code
}
trap 'handle_error $LINENO' ERR
log "INFO" "スクリプト開始"
log "INFO" "スクリプト終了"実践的なスクリプト例
例1: システムバックアップスクリプト
#!/bin/bash
set -euo pipefail
BACKUP_ROOT="/backup"
LOG_FILE="/var/log/backup.log"
RETENTION_DAYS=7
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIRS=(
"/etc"
"/home"
"/var/www"
"/usr/local/bin"
)
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
cleanup() {
log "古いバックアップの削除を開始"
find "$BACKUP_ROOT" -type f -name "backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete
log "古いバックアップの削除完了"
}
main_backup() {
local backup_file="$BACKUP_ROOT/backup_$DATE.tar.gz"
log "バックアップ開始: $backup_file"
mkdir -p "$BACKUP_ROOT"
if tar -czf "$backup_file" "${BACKUP_DIRS[@]}" 2>/dev/null; then
local size=$(du -h "$backup_file" | cut -f1)
log "バックアップ成功: $backup_file (サイズ: $size)"
else
log "エラー: バックアップの作成に失敗しました"
exit 1
fi
}
if [ "$(id -u)" -ne 0 ]; then
echo "このスクリプトはroot権限で実行してください"
exit 1
fi
log "システムバックアップ処理開始"
main_backup
cleanup
log "システムバックアップ処理完了"例2: ログ監視スクリプト
#!/bin/bash
set -euo pipefail
LOG_FILE="/var/log/syslog"
ALERT_PATTERNS=("ERROR" "CRITICAL" "FAILED")
CHECK_INTERVAL=10
LAST_CHECK_FILE="$HOME/.log_monitor_last_check" # 固定の /tmp よりホームディレクトリが安全
send_alert() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] ALERT: $message" >> "/var/log/alerts.log"
logger "LOG_MONITOR_ALERT: $message"
}
monitor_logs() {
local last_position=0
if [ -f "$LAST_CHECK_FILE" ]; then
last_position=$(cat "$LAST_CHECK_FILE")
fi
local current_size=$(wc -c < "$LOG_FILE")
if [ "$current_size" -gt "$last_position" ]; then
local new_lines=$(tail -c +$((last_position + 1)) "$LOG_FILE")
for pattern in "${ALERT_PATTERNS[@]}"; do
if echo "$new_lines" | grep -q "$pattern"; then
local matches=$(echo "$new_lines" | grep "$pattern")
send_alert "検出されたパターン '$pattern': $matches"
fi
done
echo "$current_size" > "$LAST_CHECK_FILE"
fi
}
echo "ログ監視開始: $LOG_FILE"
while true; do
monitor_logs
sleep "$CHECK_INTERVAL"
done例3: ファイル整理スクリプト
#!/bin/bash
set -euo pipefail
SOURCE_DIR="$HOME/Downloads"
ORGANIZE_ROOT="$HOME/Organized"
LOG_FILE="/tmp/file_organizer.log"
declare -A FILE_TYPES=(
["pdf"]="Documents/PDF"
["doc,docx"]="Documents/Word"
["jpg,jpeg,png,gif"]="Images"
["mp4,avi,mov"]="Videos"
["zip,rar,7z,tar,gz"]="Archives"
)
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
get_file_type() {
local filename="$1"
local extension="${filename##*.}"
extension=$(echo "$extension" | tr '[:upper:]' '[:lower:]')
for types in "${!FILE_TYPES[@]}"; do
if [[ ",$types," == *",$extension,"* ]]; then
echo "${FILE_TYPES[$types]}"
return 0
fi
done
echo "Others"
}
organize_files() {
log "ファイル整理開始: $SOURCE_DIR"
if [ ! -d "$SOURCE_DIR" ]; then
log "エラー: ソースディレクトリが存在しません: $SOURCE_DIR"
exit 1
fi
local file_count=0
while IFS= read -r -d '' file; do
if [ -f "$file" ]; then
local file_type=$(get_file_type "$(basename "$file")")
local dest_dir="$ORGANIZE_ROOT/$file_type"
mkdir -p "$dest_dir"
mv "$file" "$dest_dir/"
log "移動完了: $(basename "$file") -> $file_type/"
file_count=$((file_count + 1))
fi
done < <(find "$SOURCE_DIR" -maxdepth 1 -type f -print0)
log "ファイル整理完了: $file_count 個のファイルを処理しました"
}
organize_filesよくある間違いと落とし穴
間違い1: 配列の誤った使用
NG(スペースで分割される)
files=("file1.txt" "file 2.txt" "file3.txt")
for file in $files; do # スペース区切りで分割される
echo $file
doneOK(正しい展開)
files=("file1.txt" "file 2.txt" "file3.txt")
for file in "${files[@]}"; do # 配列として正しく展開
echo "$file"
done"${array[@]}" で配列全体を安全に展開する。
間違い2: 関数のスコープ理解不足
NG(グローバル変数が意図せず変更される)
counter=0
increment() {
counter=$((counter + 1)) # グローバル変数を変更
local result=$counter
echo $result
}
increment
echo "Global counter: $counter" # 意図しない変更OK(適切なスコープ管理)
global_counter=0
increment() {
local local_counter=$1
local_counter=$((local_counter + 1))
echo $local_counter
}
result=$(increment $global_counter)
global_counter=$result間違い3: パイプラインでの変数変更の罠
NG(変数変更が反映されない)
count=0
cat file.txt | while IFS= read -r line; do
count=$((count + 1))
done
echo "Lines: $count" # 0のまま(サブシェルで実行されるため)OK(リダイレクションを使用)
count=0
while IFS= read -r line; do
count=$((count + 1))
done < file.txt
echo "Lines: $count" # 正しくカウントされる
# または
count=$(wc -l < file.txt)間違い4: エラー処理の不備
NG(エラーが連鎖する)
cp source.txt backup.txt rm source.txt # コピーが失敗していても削除 curl -o data.json http://api.example.com/data process_data data.json # ダウンロード失敗でも処理続行
OK(堅牢なエラー処理)
set -euo pipefail
if cp source.txt backup.txt; then
echo "バックアップ成功"
rm source.txt
else
echo "エラー: バックアップに失敗しました" >&2
exit 1
fi間違い5: セキュリティを考慮しない実装
NG(セキュリティ上の問題)
read -p "コマンドを入力: " user_command eval $user_command # 任意のコマンド実行(危険) temp_file="/tmp/script_data.txt" # 予測可能なファイル名 echo "sensitive data" > $temp_file mysql -u user -p'password123' -e "SELECT * FROM users" # パスワードがログに残る
OK(セキュアな実装)
# 入力値の検証
read -p "ファイル名を入力: " filename
if [[ "$filename" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "処理: $filename"
else
echo "エラー: 無効なファイル名" >&2
exit 1
fi
# 安全な一時ファイル作成
temp_file=$(mktemp)
trap 'rm -f "$temp_file"' EXIT
# 設定ファイルからパスワード読み込み(chmod 600 ~/.mysql_config で保護必須)
if [ -f ~/.mysql_config ]; then
source ~/.mysql_config
mysql -u "$DB_USER" -p"$DB_PASS" -e "SELECT * FROM users"
fi間違い6: パフォーマンスを考慮しない実装
NG(非効率な処理)
for file in /path/to/large/directory/*; do
wc -l "$file" # ファイルごとにプロセス起動
done
for i in {1..1000}; do
current_time=$(date +%s) # 毎回dateコマンド実行
echo "Processing $i at $current_time"
doneOK(効率的な実装)
# バッチ処理で効率化
find /path/to/large/directory -name "*" -type f -exec wc -l {} +
# 一度だけ取得して再利用
start_time=$(date +%s)
for i in {1..1000}; do
current_time=$((start_time + i))
echo "Processing $i at $current_time"
doneベストプラクティス
コーディング規約
- 変数名は意味のある名前を使用
- 関数は1つの責任に集中
- コメントで処理の意図を明確に
- インデントを統一(2スペース推奨)
セキュリティ
- 入力値の検証を必ず実施
- 一時ファイルは
mktempで安全に作成 - 権限は最小限に設定
- 機密情報をハードコードしない
デバッグ
set -xでデバッグモード有効化- 適切なログ出力を実装
- 段階的にテストして開発
- ShellCheck などの静的解析ツール活用
まとめ
シェルスクリプトの実践技術をマスターすることで、効率的で信頼性の高い自動化が実現できる。
- 関数 でコードの再利用性と保守性を向上
- 配列 で複雑なデータ処理を効率化
- 適切なエラー処理 で堅牢なスクリプトを作成
- 実践例 を参考に業務に応用