Linux & IT ノート

シェルスクリプトの基本:変数・条件分岐・ループを使う

管理人 約12分で読めます

コマンドラインで作業していると、「同じコマンドを何度も実行している」「毎回手で書き換えるのが面倒」という場面がよく出てきます。シェルスクリプトは、そうした手作業をファイルにまとめて自動化するための仕組みです。

bash(Bourne Again SHell)はほとんどの Linux ディストリビューションでデフォルトのシェルとして採用されており、スクリプト言語としても広く使われています。ファイルのバックアップ、ログのローテーション、定期的なデプロイ処理など、「コマンドをまとめて実行したい」場面であればどこでも活躍します。Python や Ruby ほど汎用的ではありませんが、コマンドとの親和性が高く、既存のツールを組み合わせる用途では bash スクリプトが今でも最速の選択肢です。

スクリプトファイルの作り方

スクリプトは拡張子 .sh のテキストファイルです(.sh は慣習であり必須ではありません)。最初の行に shebang(シバン)と呼ばれる #! で始まる行を書き、どのインタープリターで実行するかを指定します。

#!/usr/bin/env bash

echo "Hello, World!"

/usr/bin/env bash と書く理由は、bash のパスが環境によって異なる場合に対応するためです。#!/bin/bash と直接書いても多くの環境で動きますが、env 経由のほうが移植性が高いです。

スクリプトを保存したら、実行権限を付与します。

chmod +x script.sh

実行方法は2通りあります。

# 実行権限がある場合(カレントディレクトリを明示する ./ が必要)
./script.sh

# bash コマンドで直接実行(実行権限不要)
bash script.sh

bash script.sh は実行権限がなくても動きますが、./script.sh 形式のほうがシェバンを活かして使えるため、配布するスクリプトには chmod +x をしておくのが一般的です。

変数:データを名前で扱う

代入と参照

変数への代入は = を使います。= の前後に空白を入れてはいけません。空白があるとシェルがコマンドとして解釈しようとしてエラーになります。

name="Alice"
count=10
greeting="Hello, World"

変数の値を参照するには $ を前置きします。$var でも ${var} でも同じ値が得られますが、文字列に連結する場合などは ${var} の形が安全です。

name="Alice"
echo $name         # Alice
echo ${name}       # Alice
echo "${name}san"  # Alicesan(変数名と後続文字を明確に区別)

コマンド置換

$(command) の形で、コマンドの出力を変数に格納できます(コマンド置換)。

today=$(date +%Y-%m-%d)
echo "今日の日付: $today"

files=$(ls *.sh)
echo "シェルスクリプト一覧: $files"

クオートの使い分け

クオートの種類によって変数の展開挙動が変わります。これは bash の重要なポイントです。

name="Alice"

echo "$name"   # Alice(ダブルクォート:変数展開される)
echo '$name'   # $name(シングルクォート:リテラル文字列、展開されない)
echo $name     # Alice(クォートなし:展開されるが、スペースや特殊文字で予期せず分割される)

クォートなしで変数を使うと、値にスペースが含まれている場合に複数の引数として解釈されてしまいます。

filename="my file.txt"

# 危険:スペースで分割され "my" と "file.txt" の2引数になる
cat $filename

# 安全:ダブルクォートで囲む
cat "$filename"

変数は基本的に常にダブルクォートで囲むのが安全な習慣です。

位置パラメータ

スクリプトに渡した引数は $1$2$3 … で参照できます。これを位置パラメータと呼びます。関数での活用も含めた詳細な使い方は次回(関数と引数処理)で扱います。

#!/usr/bin/env bash
echo "最初の引数: $1"
echo "2番目の引数: $2"
./script.sh foo bar
# 最初の引数: foo
# 2番目の引数: bar

条件分岐:if 文

基本構文

if 文の基本形は次の通りです。

if [ 条件 ]; then
  # 条件が真のとき実行
elif [ 別の条件 ]; then
  # 別の条件が真のとき実行
else
  # どれも当てはまらない場合
fi

[ ... ]test コマンドの別名です。bash では [[ ... ]](二重角括弧)も使えます。

[ ] vs [[ ]]

[[ ]] は bash 拡張であり、[ ] より使い勝手が良い点がいくつかあります。

  • &&|| を使った複数条件の組み合わせがそのまま書ける
  • 変数のクォートを忘れてもスペース分割の問題が起きにくい
  • =~ で正規表現マッチができる
name="Alice"

# [ ] の場合(&& ではなく -a を使う)
if [ "$name" = "Alice" -a 1 -eq 1 ]; then
  echo "ok"
fi

# [[ ]] の場合(より自然に書ける)
if [[ "$name" = "Alice" && 1 -eq 1 ]]; then
  echo "ok"
fi

移植性(POSIX sh との互換性)が必要なケースでなければ、[[ ]] を使うほうが書きやすく安全です。

数値比較と文字列比較

数値の比較には専用の演算子を使います。

count=5

if [[ $count -eq 5 ]]; then echo "等しい"; fi    # -eq: equal
if [[ $count -ne 3 ]]; then echo "等しくない"; fi # -ne: not equal
if [[ $count -lt 10 ]]; then echo "より小さい"; fi # -lt: less than
if [[ $count -le 5 ]]; then echo "以下"; fi       # -le: less than or equal
if [[ $count -gt 3 ]]; then echo "より大きい"; fi  # -gt: greater than
if [[ $count -ge 5 ]]; then echo "以上"; fi       # -ge: greater than or equal

文字列の比較は =(一致)、!=(不一致)のほか、空文字の判定には -z(zero: 空なら真)と -n(non-zero: 非空なら真)を使います。

str="hello"

if [[ "$str" = "hello" ]]; then echo "一致"; fi
if [[ "$str" != "world" ]]; then echo "不一致"; fi
if [[ -z "" ]]; then echo "空文字"; fi
if [[ -n "$str" ]]; then echo "非空文字"; fi

ファイルテスト

ファイルやディレクトリの存在確認もよく使います。

if [[ -f "/etc/hosts" ]]; then echo "ファイルが存在する"; fi   # -f: regular file
if [[ -d "/tmp" ]]; then echo "ディレクトリが存在する"; fi      # -d: directory
if [[ -e "/etc/hosts" ]]; then echo "パスが存在する"; fi       # -e: exists(種別問わず)
if [[ -r "/etc/hosts" ]]; then echo "読み取り可能"; fi         # -r: readable
if [[ -x "/usr/bin/bash" ]]; then echo "実行可能"; fi         # -x: executable

ループ:繰り返し処理

for ループ

リストの各要素に対して処理を繰り返すには for を使います。

# リストを直接列挙
for fruit in apple banana cherry; do
  echo "果物: $fruit"
done

# seq で数値の連番を生成
for i in $(seq 1 5); do
  echo "番号: $i"
done

# C 言語スタイル(bash 拡張)
for ((i=1; i<=5; i++)); do
  echo "カウント: $i"
done

# グロブで現在ディレクトリの .sh ファイルに対して処理
for file in *.sh; do
  echo "スクリプト: $file"
done

for file in *.sh のようにグロブを使う場合、ファイルが存在しないとリテラル *.sh が渡されてしまうことがあります。shopt -s nullglob を事前に設定するか、ファイルの存在をチェックするなどの対処が必要な場合があります。

while ループ

条件が真の間、処理を繰り返します。

count=1
while [[ $count -le 5 ]]; do
  echo "count: $count"
  ((count++))
done

ファイルを行ごとに読み込む処理でもよく使われます。

while read -r line; do
  echo "行: $line"
done < /etc/hosts

read -r-r はバックスラッシュをエスケープとして解釈させないオプションです。ファイル読み込み時はほぼ必須と考えてください。

break と continue

ループの制御には break(ループを抜ける)と continue(次のイテレーションへスキップ)を使います。

for i in $(seq 1 10); do
  if [[ $i -eq 5 ]]; then
    break    # i が 5 になったらループを終了
  fi
  echo "$i"
done

for i in $(seq 1 10); do
  if [[ $((i % 2)) -eq 0 ]]; then
    continue  # 偶数はスキップ
  fi
  echo "$i"  # 奇数だけ表示
done

実践的なコツ

変数は常にダブルクォートで囲む

スペースや特殊文字を含む値を安全に扱うため、変数参照は "$var" と書く習慣をつけましょう。例外は数値演算($((...)) 内)くらいです。

条件分岐には [[ ]] を使う

bash スクリプトを書くなら [[ ]] のほうが安全で読みやすいです。[ ] は POSIX 互換が必要な場合(/bin/sh で実行するスクリプトなど)に限定するのが現実的です。

スクリプトの動作を確認しながら書く

bash -x script.sh で実行すると、各コマンドが実行される前に内容が表示されるデバッグモードになります。変数の展開結果やどのコマンドが実行されているかを確認できるため、バグ調査に役立ちます。

bash -x script.sh
# + name=Alice
# + echo Alice
# Alice

まとめ

  • shebang #!/usr/bin/env bash を先頭に書き、chmod +x で実行権限を付ける
  • 変数代入は = の前後に空白を入れない。参照は "$var" とダブルクォートで囲む
  • $(command) でコマンドの出力を変数に代入できる(コマンド置換)
  • 条件分岐には if [[ ... ]]; then ... fi を使う。数値は -eq / -lt など、文字列は = / -z / -n、ファイルは -f / -d / -e
  • for はリスト・連番・グロブに対して繰り返す。while は条件が真の間繰り返す
  • break でループを抜け、continue で次のイテレーションへスキップする

次回は関数と引数処理を扱います。処理をまとめて再利用できる関数の定義方法、引数の受け取り方($1$@getopts)など、より大きなスクリプトを書くための道具を学びます。