Bashシェルスクリプトとは

シェルスクリプトとは、普段対話的に入力するコマンドをファイルにまとめて記述し、まとめて実行できるようにしたものです。単純なコマンドの羅列だけでなく、条件分岐(if、case)、繰り返し処理(for、while)、外部からの入力受け取り(read)、コマンドの実行結果に応じた処理の分岐($?、||、&&)などを組み合わせることで、定型的な運用作業を自動化できます。


スクリプトの実行の仕組み

シバン(shebang)

画像

シェルスクリプトのファイルの先頭1行目には、通常次のような行を書きます。

#!/bin/bash

この#!で始まる行をシバン(shebang)と呼びます。#!の直後に、このスクリプトを解釈させたいインタプリタ(プログラム)の絶対パスを指定します。./script.shのようにファイルを直接実行すると、OS(カーネル)はこのシバン行を読み取り、指定されたインタプリタ(この例では/bin/bash)にスクリプトファイルのパスを渡して実行します。

#!/bin/bashのように直接パスを書く代わりに、次のようにenvコマンドを経由する書き方も広く使われます。

#!/usr/bin/env bash

envはPATH(後述)を検索してbashの実際の場所を見つけて実行するため、bashのインストール場所がシステムによって異なる場合(/bin/bashではなく/usr/local/bin/bashなど)でも動作するという利点があります。

シバン行が無い場合や、#!で始まらないファイルを直接実行しようとした場合の挙動は、実行方法(直接実行するか、bash script.shのように明示的にインタプリタを指定して実行するか)やシステムによって異なります例えばbash script.shのように明示的にBashへスクリプトを渡して実行する場合は、シバン行が無くても(あるいは#!で始まる行が無くても)Bashがそのまま読み込んで実行します。一方./script.shのように実行権限を使って直接実行しようとした場合は、シバン行が無いと正しく実行できない、あるいは想定と異なるインタプリタで実行されてしまう可能性があります。「シバンが無くても常に問題なく動く」とは言えないため、スクリプトの先頭には明示的にシバンを書くことが強く推奨されます。[1]。


実行権限とファイルの所有者・グループ:chmod、chown

画像

スクリプトファイルを./script.shのように直接実行するには、そのファイルに実行権限が設定されている必要があります。実行権限を付与するにはchmodコマンドを使います。

chmod +x script.sh

chmodは、ファイルの読み取り(r)・書き込み(w)・実行(x)権限を、所有者(u)・グループ(g)・その他(o)ごとに設定するコマンドです。+xのような記号(シンボル)モードのほか、chmod 755 script.shのように数値(8進数)モードでも指定できます。

chownは、ファイルの所有者・所有グループを変更するコマンドです。

chown ユーザー名:グループ名 script.sh

管理者(root)が用意したスクリプトを特定のユーザーに使わせたい場合など、ファイルの所有者・所有グループと実行権限を適切に組み合わせて管理する必要があります。

SUID/SGIDビット

SUID(Set User ID)とSGID(Set Group ID)は、通常の読み取り・書き込み・実行権限に加えて設定できる特殊なパーミッションです。

実行ファイルに設定した場合

実行可能なバイナリファイルにSUIDビットを設定すると、そのプログラムは実行したユーザーではなく、原則としてファイル所有者の実効ユーザーID(EUID)で動作します。

同様に、SGIDビットを設定した実行可能ファイルは、原則としてファイル所有グループの実効グループID(EGID)で動作します。

特殊パーミッション記号モード数値モード実行時の効果
SUIDu+s4000ファイル所有者の権限で実行
SGIDg+s2000ファイル所有グループの権限で実行

設定例は次のとおりです。

chmod u+s program
chmod g+s program

数値モードでも設定できます。

chmod 4755 program    # SUIDを設定
chmod 2755 program    # SGIDを設定
chmod 6755 program    # SUIDとSGIDの両方を設定

SUID/SGIDビットを解除する場合は、次のように実行します。

chmod u-s program
chmod g-s program

ls -lでの表示

SUIDビットは、所有者の実行権限を表す位置に表示されます。

-rwsr-xr-x 1 root root ... program
   ^

SGIDビットは、グループの実行権限を表す位置に表示されます。

-rwxr-sr-x 1 root group ... program
      ^

対応する実行権限も設定されている場合は小文字のs、実行権限が設定されていない場合は大文字のSで表示されます。

-rwSr--r--    # SUIDは設定されているが、所有者の実行権限がない
-rwxr-Sr--    # SGIDは設定されているが、グループの実行権限がない

ディレクトリとSGIDビットの関係

ディレクトリにSGIDビットを設定すると、そのディレクトリ内に新しく作成されたファイルやディレクトリは、原則として親ディレクトリの所有グループを引き継ぎます。

chmod g+s shared_directory

数値モードでは、先頭に2を指定します。

chmod 2775 shared_directory

複数のユーザーが同じグループでファイルを共有するディレクトリなどで利用されます。

Linuxでは、ディレクトリに設定したSUIDビットには通常、特別な効果はありません。

スクリプトとSUID/SGIDビットの関係

実行可能なバイナリファイルにSUIDビットを設定すると、通常はそのファイルの所有者の実効ユーザーIDでプログラムが実行されます。しかし、Linuxではシェルスクリプトなどのインタプリタ型スクリプトに設定されたSUID/SGIDビットは無視されます。

そのため、次のようにシェルスクリプトへSUIDビットを設定しても、ファイル所有者の権限では実行されません。

chmod u+s script.sh

これはセキュリティ上の理由によるものです。管理者権限など、実行ユーザーとは異なる権限で特定の処理を許可したい場合は、スクリプトにSUIDビットを設定するのではなく、sudoと/etc/sudoersによる権限委譲を利用します。

なお、SUIDは必ずしもroot権限を与える機能ではありません。実行時に使用されるのは、あくまでそのファイルの所有者の実効ユーザーIDです。ファイルの所有者がrootの場合に、root権限で動作することになります。

SUID/SGIDビットが設定されたファイルの検索

findコマンドの-perm条件を使用すると、SUID/SGIDビットが設定されたファイルを検索できます。

SUIDビットが設定されたファイル

find / -type f -perm -4000

権限不足によるエラーメッセージを表示したくない場合は、標準エラー出力を/dev/nullへリダイレクトします。

find / -type f -perm -4000 2>/dev/null

SGIDビットが設定されたファイル

find / -type f -perm -2000 2>/dev/null

SUIDまたはSGIDのどちらかが設定されたファイル

-oはOR条件を表します。丸括弧はシェルに解釈されないように、バックスラッシュでエスケープします。

find / -type f \( -perm -4000 -o -perm -2000 \) 2>/dev/null

GNU版findでは、-perm /modeを使って、指定したビットのうちいずれかが設定されたファイルを検索することもできます。

find / -type f -perm /6000 2>/dev/null

6000はSUIDの4000とSGIDの2000を合わせた値です。この例では、SUIDまたはSGIDの少なくとも一方が設定されたファイルが検索されます。

一方、次の指定はSUIDとSGIDの両方が設定されたファイルだけを検索します。

find / -type f -perm -6000 2>/dev/null

検索結果の詳細情報も確認したい場合は、-lsを付けます。

find / -type f -perm /6000 -ls 2>/dev/null

-permの指定方法

指定例意味
-perm 4000パーミッションが指定値と完全に一致する
-perm -4000指定したすべてのビットが設定されている
-perm /6000指定したビットのいずれかが設定されている

SUID/SGIDファイルを検索するときは、通常、ほかのパーミッションも同時に設定されているため、完全一致の-perm 4000ではなく、-perm -4000や-perm /6000を使用します。


コマンドの実行結果を扱う:終了ステータスと$?

画像

Linuxのコマンドは、実行が終わると終了ステータス(exit status)と呼ばれる数値をOSに返します。慣習として、0は成功、0以外(1〜255)は成功以外の状態を表します。0以外はエラーだけでなく、検索条件に一致しなかったことなどを表す場合もあります。

直前に実行したコマンドの終了ステータスは、特殊変数$?で参照できます。

grep "pattern" file.txt
echo $?    # grepが1件でも見つかれば0、見つからなければ1

$?は「直前に実行したコマンド」の終了ステータスしか保持しません。$?を確認する前に別のコマンド(echoなども含む)を実行してしまうと、その別のコマンドの終了ステータスで上書きされてしまう点に注意が必要です。

exit:スクリプト自身の終了ステータスを指定する

exitは、スクリプト(またはシェル)自体を終了させ、指定した数値を終了ステータスとして返すコマンドです。

exit 0    # 成功として終了
exit 1    # 失敗として終了

引数を省略したexitは、その時点での$?(直前に実行したコマンドの終了ステータス)をそのままスクリプトの終了ステータスとして使います。

&&と||:終了ステータスに応じてコマンドをつなげる

&&と||は、直前のコマンドの終了ステータスに応じて、次のコマンドを実行するかどうかを制御する演算子です。

演算子説明
コマンドA && コマンドBコマンドAが成功(終了ステータス0)した場合のみ、コマンドBを実行します。
コマンドA || コマンドBコマンドAが失敗(終了ステータス0以外)した場合のみ、コマンドBを実行します。

これらは、if文を使わずに簡潔に「成功したら〜する」「失敗したら〜する」という処理を書くための、よく使われる書き方です。

mkdir /tmp/work && cd /tmp/work
ping -c 1 example.com || echo "接続できませんでした"

条件分岐

test(および[ ]、[[ ]])

画像

testは、指定した条件式を評価し、その結果を終了ステータス(真なら0、偽なら1)として返すコマンドです。

test -f /etc/passwd    # /etc/passwdが通常ファイルとして存在すれば0

test 式は、[ 式 ](角括弧、末尾の]の前にスペースが必要)という書き方でも同じ意味になります。角括弧[自体がtestと同じ機能を持つ独立したコマンド(多くのシステムでは/usr/bin/[としても実装されています)だからです。

[ -f /etc/passwd ]

代表的な比較演算子には次のものがあります。数値比較と文字列比較で使う演算子が異なる点が、試験でもよく問われるポイントです。

数値比較

演算子説明
-eq等しい
-ne等しくない
-ltより小さい
-le以下
-gtより大きい
-ge以上

文字列比較

演算子説明
= または ==等しい
!=等しくない
-z文字列が空
-n文字列が空でない

ファイル関連

演算子説明
-f通常ファイルが存在する
-dディレクトリが存在する
-e何らかのファイルが存在する
-x実行権限がある

Bashには、test/[ ]をBash独自に拡張した[[ 式 ]]という書き方もあります。[[ ]]はBashのキーワードとして特別に解釈されるため、[ ]と違って単語分割やパス名展開が行われず、&&・||・パターンマッチング(==でのワイルドカード比較)なども式の中で直接使えます。ただし[[ ]]はBash(および一部の高機能なシェル)の拡張であり、shとして動作する最小限のシェル(dashなど)では使えない場合がある点に注意してください。

if

画像

ifは、コマンド(多くの場合test/[ ])の終了ステータスに応じて処理を分岐させる制御構文です。

if [ -f /etc/passwd ]; then
    echo "存在します"
elif [ -d /etc/passwd ]; then
    echo "ディレクトリです"
else
    echo "存在しません"
fi

ifが評価しているのは、[ ]やtestが返す終了ステータスに限りません。任意のコマンドの終了ステータスを条件として使えます。

if grep -q "pattern" file.txt; then
    echo "見つかりました"
fi

case

caseは、1つの値を複数のパターンと順に照合し、最初に一致したパターンに対応する処理を実行する制御構文です。多数のelifを重ねるよりも見通しよく書けます。

case "$1" in
    start)
        echo "起動します"
        ;;
    stop)
        echo "停止します"
        ;;
    restart|reload)
        echo "再起動します"
        ;;
    *)
        echo "使い方: $0 {start|stop|restart|reload}"
        ;;
esac

パターンには、正規表現ではなく*・?・[abc]のようなグロブ(glob)形式のワイルドカードを使います。|で複数のパターンをまとめて指定でき、*はどのパターンにも一致しなかった場合の既定の処理(フォールバック)としてよく使われます。


繰り返し処理

画像

for

forは、リストの要素を1つずつ変数に代入しながら、同じ処理を繰り返す制御構文です。

for file in *.txt; do
    echo "$file"
done

Bashの既定設定では、*.txtに一致するファイルがない場合、*.txtは展開されず、その文字列がそのまま繰り返し対象になります。

繰り返す対象には、上記のようなファイル名のパターン(グロブ)展開のほか、後述するseqやコマンド置換($(コマンド))の実行結果、Bash独自の数値の範囲展開({1..10})などを使えます。

for i in $(seq 1 5); do
    echo "$i回目"
done

Bashには、C言語に似た書き方のfor(for ((i=0; i<10; i++)); do ... done)もありますが、これはBash独自の拡張であり、POSIX準拠のshでは使えません。

while

whileは、指定した条件(コマンドの終了ステータス)が真である間、処理を繰り返す制御構文です。

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

whileの条件が最初から偽であれば、ループの中身は一度も実行されません。「条件が真の間だけ繰り返す」whileに対して、「条件が偽の間だけ繰り返す(真になったら終了する)」untilという対になる構文もあります。

seq

seqは、指定した範囲の連続した数値を1行ずつ出力する外部コマンドです。

seq 1 10        # 1から10まで
seq 1 2 10      # 1から10まで2刻み(1 3 5 7 9)

for・whileループと組み合わせて、指定回数だけ処理を繰り返す用途でよく使われます。for・while・test・ifがシェルの組み込み機能(キーワードや組み込みコマンド)であるのに対し、seqはGNU coreutilsに含まれる外部コマンドである点は区別しておく価値がありますこの違いにより、seqが入っていない最小限の環境(一部の組み込み系・コンテナ用ディストリビューションなど)では、代わりにBash独自の範囲展開{1..10}やC言語風forを使う、といった選択が必要になる場合があります。[2]。

シェルスクリプトの配列

Bashでは、複数の値を1つの変数名で管理するために配列を利用できます。
配列は array=(red green blue) のように定義します。
各要素は0から始まる添字で指定し、${array[0]} のように参照します。

colors=(red green blue)
echo "${colors[0]}"    # red
echo "${colors[@]}"    # 全要素を表示
echo "${#colors[@]}"   # 要素数(3)を表示

要素の追加・変更は colors[3]=yellow、削除は unset 'colors[1]' と記述します。

入力を受け取る:read

readは、標準入力(キーボード入力やパイプ経由のデータ)から1行を読み取り、変数に代入するシェルの組み込みコマンドです。

read -p "名前を入力してください: " name
echo "こんにちは、${name}さん"
オプション説明
-p 文字列入力を促すプロンプト文字列の表示
-rバックスラッシュ(\)をエスケープ文字として特別扱いしない(意図しない解釈防止のため推奨される設定)
-s入力内容の非表示(パスワード入力などに使用)
-a 配列名読み取った内容の IFS(既定値:空白・タブ・改行)に基づく分割および配列変数への代入
-t 秒数指定秒数経過時のタイムアウト処理

readに複数の変数名を指定すると(read a b c)、入力をIFSに従って分割してそれぞれの変数に順に代入し、余った分はまとめて最後の変数に入ります。

スクリプトへの引数(位置パラメータ)

スクリプトを実行する際に指定した引数は、位置パラメータと呼ばれる特殊変数でスクリプト内から参照できます。

./script.sh arg1 arg2 arg3
特殊変数説明
$0シェルに渡されたスクリプト自身の名前またはコマンド名
$1〜$91番目〜9番目の引数 ※10番目以降は${10}のように波括弧で囲む
$#渡された引数の個数
$*もしくは$@すべての引数をまとめて参照
"$*"全引数が1つの文字列として展開
"$@"各引数がそれぞれ独立した文字列として展開("$1" "$2" "$3"と書いたのと同じ)

"$@"と"$*"の違いは、引数にスペースを含む値(例:"file name.txt")が渡された場合に顕著になります。他のコマンドに引数をそのまま渡したい場合は、"$@"を使うのが基本です。

例)"$@"の場合

root@miti-shirube:~# cat test.sh
#!/bin/bash
cnt=1
for arg in "$@"; do
  echo "$cnt:$arg"
  cnt=$((cnt + 1))
done
root@miti-shirube:~# ./test.sh "Hello World" "Hello Bash"
1:Hello World
2:Hello Bash

例)"$*"の場合

root@miti-shirube:~# cat test.sh
#!/bin/bash
cnt=1
for arg in "$*"; do
  echo "$cnt:$arg"
  cnt=$((cnt + 1))
done
root@miti-shirube:~# ./test.sh "Hello World" "Hello Bash"
1:Hello World Hello Bash

getoptsによるオプションの解析

getoptsは、シェルスクリプトに渡された1文字のオプション(例:-a、-f filename)を解析するためのシェルの組み込みコマンドで、構文は以下の通りです。

getopts "オプション文字列" 変数名
  • オプション文字列

使用を許可するオプション文字を並べます。

  • 変数名

解析したオプション文字が格納されます。たとえば、-aを解析するとaが格納されます。

  • 引数を必要とするオプションには、オプション文字の直後に:(コロン)を付けます。

たとえば、-aは引数を取らず、-fは引数を取る場合、オプション文字列は次のように指定します。

"af:"

getoptsでは以下の変数が利用可能です。

変数概要
OPTARG引数を必要とするオプションに指定された値が格納されます。たとえば、-f filenameではfilenameが格納されます。
OPTIND次に解析する位置引数の番号が格納されます。初期値は1で、オプションの解析に応じて更新されます。

以下はgetoptsを利用したスクリプトの例です。

#!/bin/bash

while getopts "af:" option
do
    case "$option" in
        a)
            echo "-a が指定されました"
            ;;
        f)
            echo "-f の引数は $OPTARG です"
            ;;
        \?)
            echo "不正なオプションです" >&2
            exit 1
            ;;
    esac
done

shift $((OPTIND - 1))

実行例は次のとおりです。

./sample.sh -a -f filename

出力例:

-a が指定されました
-f の引数は filename です

shift $((OPTIND - 1))を実行すると、解析済みのオプションとその引数を位置パラメータから取り除けます。その後は、オプションではない残りの引数を$1、$2などで処理できます。

shift

shiftは、位置パラメータを1つ左にずらす($2が新しい$1になる、$1は破棄される)コマンドです。引数の数が不定なスクリプトで、先頭から順に1つずつ引数を処理していく場合によく使われます。

while [ "$#" -gt 0 ]; do
    echo "処理中: $1"
    shift
done

exec:プロセスを置き換える/リダイレクトを変更する

execは、大きく分けて2つの使い方があります。

1. コマンドを指定した場合:現在のシェルプロセスを、指定したコマンドの実行に置き換えます。新しいプロセスを生成(フォーク)するのではなく、既存のプロセスの中身をそのまま入れ替える点が特徴です。

exec /usr/bin/some-program
echo "この行は実行されません"

execでコマンドの実行に成功すると、元のシェル(スクリプト)の処理はそこで終わり、execより後ろの行は実行されません。シェルのプロセスID(PID)は変わらないまま、実行されているプログラムだけが置き換わります。

2. コマンドを指定せず、リダイレクトだけを指定した場合:現在のシェルの入出力先を、以降ずっと変更します。

exec > output.log 2>&1
echo "この行以降の出力はすべてoutput.logに書き込まれます"

この使い方は、スクリプトの冒頭で標準出力・標準エラー出力をまとめてログファイルへリダイレクトしたい場合によく使われます。


関数:function

functionは、複数のコマンドをひとまとめにして名前を付け、何度でも呼び出せるようにする仕組みです。

function greet {
    echo "こんにちは、$1さん"
}

greet "太郎"

Bashではfunctionキーワードを使わず、greet() { ... }という書き方POSIX準拠のshでも使える書き方[3]で関数を定義することもできます。どちらの書き方でも、関数は定義した時点では実行されず、名前を呼び出したときに初めて実行されます。関数の中では、関数に渡された引数を$1・$2・$#などの位置パラメータとして参照できます(スクリプト自体の位置パラメータとは別物として扱われます)。関数内で宣言した変数は、localを付けない限り既定でスクリプト全体から参照できるグローバル変数として扱われる点にも注意が必要です。


シグナルを扱う:trap

trapは、スクリプトの実行中にシグナル(Ctrl+CによるSIGINT、killコマンドによるSIGTERMなど)を受け取ったとき、あるいはスクリプトが終了するときに、指定した処理を実行させるための組み込みコマンドです。

trap 'echo "中断されました"; exit 1' INT
trap 'rm -f "$TMPFILE"' EXIT
  • 1つ目の例は、スクリプト実行中にCtrl+C(SIGINT)が送られたときにメッセージを表示して終了する設定です。
  • 2つ目の例は、スクリプトが(正常終了・異常終了を問わず)終了するタイミングで一時ファイルを削除する設定です。EXITはシグナル名ではなく、Bashが用意している「スクリプトの終了時」を表す疑似シグナル(擬似イベント)です。

trapを使うことで、スクリプトが途中で中断された場合にも、一時ファイルの削除やロックの解放といった後片付け処理を行えます。ただし、SIGKILLやSIGSTOPのようにtrapで捕捉できないシグナルもあるため、すべての終了状況で後片付けを保証できるわけではありません。trap - INTのように-をアクションとして指定すると、そのシグナルの扱いを既定の動作に戻せます。

trapで指定する主なシグナルおよび特殊なトラップ条件

シグナル名/条件番号主な契機・機能
EXIT0シェルやスクリプトが終了する直前に実行される特殊なトラップ条件EXITはシグナル番号0の実在するシグナルではありません。trapにおいて、引数0はEXITと同じ意味を持ちます。[4]。正常終了だけでなく、exitやエラーなどによる終了時にも後処理を実行できるEXITを設定していても、SIGKILLによる強制終了などでは後処理を実行できません。外部からのSIGTERMやSIGINTにも確実に対応させたい場合は、それらも明示的に指定します。[5]
SIGHUP1制御端末の切断やハングアップなどで送信される
SIGINT2キーボードからの割り込み時。通常は Ctrl+C で送信される
SIGQUIT3キーボードからの終了要求時。通常は Ctrl+\ で送信され、デフォルト動作ではコアダンプを伴って終了する
SIGTERM15プロセスに通常の終了を要求する。killコマンドでシグナルを省略した場合のデフォルト
ERR―コマンドが0以外の終了ステータスを返したときに実行される特殊なトラップ条件OSのシグナルではなく、Bashなどが提供する特殊なトラップ条件です。[6]。ただし、条件式や&&、||などの文脈では実行されない場合がある「非0なら必ず実行」ではありません。例えば、ifやwhileの条件部分、&&や||で制御されているコマンド、!で終了ステータスを反転している場合などは対象外になることがあります。[7]
DEBUG―各単純コマンドなどを実行する直前に実行される、主にデバッグ用の特殊なトラップ条件OSのシグナルではなく、Bashなどが提供する特殊なトラップ条件です。[8] DEBUGの厳密な対象は、Bashでは単純コマンド、for、case、算術コマンドなどです。[9]

コマンドの検索パス:PATH

PATHは、lsやgrepのようにパスを指定せずに外部コマンド名だけを入力したとき、シェルがそのコマンドの実行可能ファイルをどのディレクトリから探すかを定義する環境変数です。複数のディレクトリをコロン(:)区切りで並べて指定します。Bashの関数や組み込みコマンドなどの解決は、PATHによる外部実行可能ファイルの検索とは別に行われます。

echo $PATH
# 例: /usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin

シェルは、PATHに列挙されたディレクトリを先頭から順に探し、最初に見つかった同名の実行可能ファイルを実行します。同じ名前のコマンドが複数のディレクトリに存在する場合、PATH内での並び順がどちらが優先されるかを左右します。

PATHには、セキュリティを意識した設定ではカレントディレクトリ(.)を含めないのが一般的です。これは、悪意のあるファイルを「よく使うコマンドと同じ名前」でカレントディレクトリに置いておき、ユーザーが気付かずにそれを実行してしまう、という攻撃を防ぐためのセキュリティ上の配慮です。PATHにカレントディレクトリが含まれない設定では、カレントディレクトリにあるスクリプトを実行する際、script.shのようにコマンド名だけを入力してもPATHからは見つかりません。そのため、./script.shのように明示的にパス(この場合は相対パス)を指定しますPATHにカレントディレクトリを追加する設定自体は可能ですが、上記のセキュリティ上の理由から一般には推奨されていません。[10]。


コマンド置換

コマンド置換とは、コマンドの実行結果である標準出力を取り出し、別のコマンドの引数や変数などに埋め込む機能です。

コマンド置換には、次の2つの書式があります。

  • $(command):現在一般的に使用される書式
  • `command`:従来から使用されている書式

以下は、文字列の中にdateコマンドの実行結果を埋め込む例です。

$ echo "今日は$(date '+%Y/%m/%d')です"
今日は2026/09/05です

$ echo "今日は`date '+%Y/%m/%d'`です"
今日は2026/09/05です

どちらも同じ結果になりますが、$(command)のほうが構造を把握しやすいため、一般的にこちらが使用されます。


スクリプトのデバッグ:bash -v、bash -x

Bashスクリプトが期待どおりに動かないとき、次の2つのオプションを使うと、スクリプトの実行過程を確認できます。

  • bash -v スクリプト(verboseモード):スクリプトの各行を、シェルが読み込んだ(変数展開などをする前の)そのままの内容で表示してから実行します。
  • bash -x スクリプト(xtraceモード):スクリプトの各コマンドを、変数展開やコマンド置換を行った後の、実際に実行される形で(先頭に+を付けて)表示しながら実行します。
bash -x script.sh
# + count=1
# + '[' 1 -le 5 ']'
# + echo 1回目
# 1回目

-vは「スクリプトに書かれた文字どおりの行」を、-xは「変数展開後の、実際に実行された内容」を表示するという違いがあります。変数に想定と違う値が入っていないかを確認したい場合は-xが、スクリプトの読み込み自体(構文エラーの箇所など)を追いたい場合は-vが向いています。両方を同時に有効にするbash -vx スクリプトという使い方もできます。スクリプトの先頭にset -x・set +xと書くことで、スクリプトの一部区間だけxtraceモードを有効・無効にすることも可能です。


補足

  1. 例えばbash script.shのように明示的にBashへスクリプトを渡して実行する場合は、シバン行が無くても(あるいは#!で始まる行が無くても)Bashがそのまま読み込んで実行します。一方./script.shのように実行権限を使って直接実行しようとした場合は、シバン行が無いと正しく実行できない、あるいは想定と異なるインタプリタで実行されてしまう可能性があります。「シバンが無くても常に問題なく動く」とは言えないため、スクリプトの先頭には明示的にシバンを書くことが強く推奨されます。 ↩
  2. この違いにより、seqが入っていない最小限の環境(一部の組み込み系・コンテナ用ディストリビューションなど)では、代わりにBash独自の範囲展開{1..10}やC言語風forを使う、といった選択が必要になる場合があります。 ↩
  3. POSIX準拠のshでも使える書き方 ↩
  4. EXITはシグナル番号0の実在するシグナルではありません。trapにおいて、引数0はEXITと同じ意味を持ちます。 ↩
  5. EXITを設定していても、SIGKILLによる強制終了などでは後処理を実行できません。外部からのSIGTERMやSIGINTにも確実に対応させたい場合は、それらも明示的に指定します。 ↩
  6. OSのシグナルではなく、Bashなどが提供する特殊なトラップ条件です。 ↩
  7. 「非0なら必ず実行」ではありません。例えば、ifやwhileの条件部分、&&や||で制御されているコマンド、!で終了ステータスを反転している場合などは対象外になることがあります。 ↩
  8. OSのシグナルではなく、Bashなどが提供する特殊なトラップ条件です。 ↩
  9. DEBUGの厳密な対象は、Bashでは単純コマンド、for、case、算術コマンドなどです。 ↩
  10. PATHにカレントディレクトリを追加する設定自体は可能ですが、上記のセキュリティ上の理由から一般には推奨されていません。 ↩