Bash Basics | Basicsトップページ | トップページ

トレース情報の出力と記録

関数の呼び出し履歴をトレースして記録するサンプルスクリプトfuncname.shです。

#!/usr/bin/bash

# Trace Log
XTRACEFILE=~/logs/`basename $0`.log
exec {fd}> $XTRACEFILE
if [ $? == 0 ]; then
    BASH_XTRACEFD=$fd               # set -xのトレース情報をファイルに出力
    echo "Opened $fd"
    set -ex                         # set -eはエラー発生時にexit
else
    set -e                          # set -eはエラー発生時にexit
fi

set -o errtrace                     # ERRトラップを継承
trap catch ERR                      # ERRトラップの関数
trap finally EXIT                   # EXITトラップの関数

# Sub Function A
function func_A () {
    # func_A:21 is called from main:76
    echo ${FUNCNAME[0]}:${LINENO} is called from ${FUNCNAME[1]}:${BASH_LINENO[0]}
    func_B B1 B2
    return 0
}

# Sub Function B
function func_B () {
    # func_B:29 is called from func_A:22
    echo ${FUNCNAME[0]}:${LINENO} is called from ${FUNCNAME[1]}:${BASH_LINENO[0]}
    func_C C1 C2 C3 C4
    return 0
}

# Sub Function C
function func_C () {
    # func_C:37 is called from func_B:30
    echo ${FUNCNAME[0]}:${LINENO} is called from ${FUNCNAME[1]}:${BASH_LINENO[0]}

    # エラーを発生させる(実行するコマンドが存在しない)
    cmdcmd
    return 0
}

# トレースの表示
function catch () {
    echo "##### Call Trace #####" 1>&2
    for ((i=0, k=0; i<${#BASH_LINENO[*]}; i++))
    do
        if [ $i == 0 ]; then
            echo $((${#BASH_LINENO[*]}-$i)): ${BASH_SOURCE[$i]}: \
                 ${FUNCNAME[$i]}: "Current" 1>&2
        else
            echo $((${#BASH_LINENO[*]}-$i)): ${BASH_SOURCE[$i]}: \
                 ${FUNCNAME[$i]}: ${BASH_LINENO[$(($i-1))]} 1>&2
        fi
        # 引き数の表示
        for ((l=0, j=${BASH_ARGC[$i]}-1; j>=0; l++, j--))
        do
            echo "ARGV["$l"]: " ${BASH_ARGV[$((k+j))]}
        done
        x=$((k+=${BASH_ARGC[$i]-0}))
    done
    return 0
}

# 終了処理を記述
function finally () {
    echo "Exiting at ${FUNCNAME[1]}"        # exitする関数名を出力
    echo "Closing $fd"
    exec {fd}>&-                            # トレースファイルのクローズ
}

# メインの処理
shopt -s extdebug

func_A A1 A2 A3
echo "#### `basename $0` end ####"
  • スクリプトの最初にトレース情報を出力するファイル名をXTRACEFILEに設定しています。
  • execコマンドによるリダイレクションでトレース情報を出力するファイルをオープンしています。
  • set -xでスクリプトの実行をトレースするように設定しています。
  • set -eでスクリプトの実行でエラーが発生した場合にexitするように設定しています。
  • set -o errtraceでERRを継承できるように設定しています。
  • trapコマンドでERR、およびEXITをトラップする関数を設定しています。
  • func_A、func_B、func_C関数をネストして呼び出ししています。
  • func_C関数内で存在しないコマンド"cmdcmd"を実行しようとしてエラーを発生させています。
  • ERRが発生することで、catch関数を呼び出し、関数の呼び出し履歴と引き数を出力しています。
  • エラーによりスクリプトの実行を強制終了させています。
  • EXITシグナルが発生することで、finally関数を呼び出し、終了処理としてトレースファイルをクローズしています。
実行例:
$ ./funcname.sh
Opened 10
func_A:21 is called from main:76
func_B:29 is called from func_A:22
func_C:37 is called from func_B:30
./funcname.sh: 行 40: cmdcmd: コマンドが見つかりません
##### Call Trace #####
5: ./funcname.sh: catch: Current
4: ./funcname.sh: func_C: 40
ARGV[0]:  C1
ARGV[1]:  C2
ARGV[2]:  C3
ARGV[3]:  C4
3: ./funcname.sh: func_B: 30
ARGV[0]:  B1
ARGV[1]:  B2
2: ./funcname.sh: func_A: 22
ARGV[0]:  A1
ARGV[1]:  A2
ARGV[2]:  A3
1: ./funcname.sh: main: 67
Exiting at func_C
Closing 10
$ cat ~/logs/funcname.sh.log
+ set -o errtrace
+ trap catch ERR
+ trap finally EXIT
+ shopt -s extdebug
+ func_A A1 A2 A3
+ echo func_A:21 is called from main:76
+ func_B B1 B2
+ echo func_B:29 is called from func_A:22
+ func_C C1 C2 C3 C4
+ echo func_C:37 is called from func_B:30
+ cmdcmd
++ catch
++ echo '##### Call Trace #####'
++ (( i=0, k=0 ))
++ (( i<5 ))
++ '[' 0 == 0 ']'
++ echo 5: ./funcname.sh: catch: Current
++ (( l=0, j=0-1 ))
++ (( j>=0 ))
++ x=0
++ (( i++ ))
++ (( i<5 ))
++ '[' 1 == 0 ']'
++ echo 4: ./funcname.sh: func_C: 40
++ (( l=0, j=4-1 ))
++ (( j>=0 ))
++ echo 'ARGV[0]: ' C1
++ (( l++, j-- ))
++ (( j>=0 ))
++ echo 'ARGV[1]: ' C2
++ (( l++, j-- ))
++ (( j>=0 ))
++ echo 'ARGV[2]: ' C3
++ (( l++, j-- ))
++ (( j>=0 ))
++ echo 'ARGV[3]: ' C4
++ (( l++, j-- ))
++ (( j>=0 ))
++ x=4
++ (( i++ ))
++ (( i<5 ))
++ '[' 2 == 0 ']'
++ echo 3: ./funcname.sh: func_B: 30
++ (( l=0, j=2-1 ))
++ (( j>=0 ))
++ echo 'ARGV[0]: ' B1
++ (( l++, j-- ))
++ (( j>=0 ))
++ echo 'ARGV[1]: ' B2
++ (( l++, j-- ))
++ (( j>=0 ))
++ x=6
++ (( i++ ))
++ (( i<5 ))
++ '[' 3 == 0 ']'
++ echo 2: ./funcname.sh: func_A: 22
++ (( l=0, j=3-1 ))
++ (( j>=0 ))
++ echo 'ARGV[0]: ' A1
++ (( l++, j-- ))
++ (( j>=0 ))
++ echo 'ARGV[1]: ' A2
++ (( l++, j-- ))
++ (( j>=0 ))
++ echo 'ARGV[2]: ' A3
++ (( l++, j-- ))
++ (( j>=0 ))
++ x=9
++ (( i++ ))
++ (( i<5 ))
++ '[' 4 == 0 ']'
++ echo 1: ./funcname.sh: main: 76
++ (( l=0, j=-1 ))
++ (( j>=0 ))
++ x=9
++ (( i++ ))
++ (( i<5 ))
++ return 0
+ finally
+ echo 'Exiting at func_C'
+ echo 'Closing 10'
+ exec
  • func_A、func_B、func_Cを呼び出した時に、呼び出し元の関数名と行数が出力されています。
  • func_C内でエラーが発生したことで関数の呼び出し履歴と引き数が出力されています。
  • 終了処理が呼び出されてトレースファイルがクローズされています。
  • トレースファイルにはスクリプトが実行したコマンドとその引き数が記録されています。
  • set -eでスクリプトの実行でエラーが発生した場合にexitするように設定されているため、最後のechoコマンドは実行されていないことにご注意ください。


トレース情報の出力(DEBUGのトラップ)

DEBUGのトラップを利用した関数の呼び出し履歴をトレースして出力するサンプルスクリプトdebug.shです。

#!/usr/bin/bash

set -o functrace                    # DEBUGトラップを継承
trap 'debug ${LINENO}' DEBUG        # DEBUGトラップの関数

# Sub Function A
function func_A () {
    # func_A:21 is called from main:67
    echo ${FUNCNAME[0]}:${LINENO} is called from ${FUNCNAME[1]}:${BASH_LINENO[0]}
    func_B
    return 0
}

# Sub Function B
function func_B () {
    # func_B:29 is called from func_A:22
    echo ${FUNCNAME[0]}:${LINENO} is called from ${FUNCNAME[1]}:${BASH_LINENO[0]}
    func_C
    return 0
}

# Sub Function C
function func_C () {
    # func_C:37 is called from func_B:30
    echo ${FUNCNAME[0]}:${LINENO} is called from ${FUNCNAME[1]}:${BASH_LINENO[0]}

    # エラーを発生させる(実行するコマンドが存在しない)
    cmdcmd
    return 0
}

function debug () {
    echo line $1: ${BASH_COMMAND}
}

# メインの処理
func_A
echo "#### `basename $0` end ####"
  • set -o functraceでDEBUGを継承できるように設定しています。
  • trapコマンドでDEBUGをコマンドを実行する前にトラップする関数を設定しています。
  • func_A、func_B、func_C関数をネストして呼び出ししています。
  • func_C関数内で存在しないコマンド"cmdcmd"を実行しようとしてエラーを発生させています。
  • 単純なコマンドforコマンド、caseコマンド、selectコマンド、算術forコマンドの前、および関数内の最初のコマンドの実行前にdebug関数を呼び出し、実行しようとしている行番号とコマンド文字列を標準出力に出力します。
実行例:
$ ./debug.sh 
line 37: func_A
line 7: func_A
line 9: echo ${FUNCNAME[0]}:${LINENO} is called from ${FUNCNAME[1]}:${BASH_LINENO[0]}
func_A:9 is called from main:37
line 10: func_B
line 15: func_B
line 17: echo ${FUNCNAME[0]}:${LINENO} is called from ${FUNCNAME[1]}:${BASH_LINENO[0]}
func_B:17 is called from func_A:10
line 18: func_C
line 23: func_C
line 25: echo ${FUNCNAME[0]}:${LINENO} is called from ${FUNCNAME[1]}:${BASH_LINENO[0]}
func_C:25 is called from func_B:18
line 28: cmdcmd
./debug.sh: 行 28: cmdcmd: コマンドが見つかりません
line 29: return 0
line 19: return 0
line 11: return 0
line 38: echo "#### `basename $0` end ####"
#### line 38: basename $0
debug.sh end ####
  • メインの処理からfunc_A、func_B、func_C関数をネストして呼び出ししています。
  • 関数の呼び出し時(関数内の最初のコマンドの実行前)、コマンドの実行前にdebug関数が呼び出されて実行しようとしている行番号とコマンド文字列を標準出力に出力しています。


オプションのチェック

getoptsコマンドによるオプションと引き数の処理を行うサンプルスクリプトopt.shです。

#!/usr/bin/bash

while getopts ":ab:c:h" OPT
do
    case "$OPT" in
    "a" ) OPT_A="TRUE";;
    "b" ) OPT_B="TRUE"; VAL_B="$OPTARG";;
    "c" ) OPT_C="TRUE"; VAL_C="$OPTARG";;
    "?" ) echo "無効なオプション $OPTARG が指定されました";
          echo "Usage: `basename $0` [-a] [-b VAL] [-c VAL] [-h]" 1>&2;
          exit 1;;
    "h" ) echo "Usage: `basename $0` [-a] [-b VAL] [-c VAL] [-h]" 1>&2;
          exit 1;;
esac
done

if [ "$OPT_A" = "TRUE" ]; then
    echo '"-a"オプションが指定されました。'
fi
if [ "$OPT_B" = "TRUE" ]; then
    echo '"-b"オプションが指定されました。'
    echo "値は $VAL_B です。"
fi
if [ "$OPT_C" = "TRUE" ]; then
    echo '"-c"オプションが指定されました。'
    echo "値は $VAL_C です。"
fi

# getoptsで処理したオプションと引き数の数だけ位置パラメータをシフト
shift  $(($OPTIND - 1))
  • オプションabchがあり、そのうちbcには引き数があることを指定して1つずつ変数OPTに設定します。
    先頭に:を記述しているため、無効な引き数の処理はスクリプト内で記述します。
  • それぞれのオプションの処理を実施します。
    オプションaは、変数OPT_Aの値を"TRUE"に設定します。
    オプションbは、変数OPT_Bの値を"TRUE"に設定し、変数VAL_Bにその引き数を設定します。
    オプションcは、変数OPT_Cの値を"TRUE"に設定し、変数VAL_Cにその引き数を設定します。
    オプション?は、無効なオプションの指定を意味し、シェル変数OPTARGに指定されたオプションが設定されます。エラーメッセージとUsageを出力します。
    オプションhは、Usageを出力します。
  • 指定されたオプションとその引き数があれば出力します。
  • getoptsで処理したオプションと引き数の数だけ位置パラメータをシフトします。
実行例:
$ ./opt.sh -abこんにちは
"-a"オプションが指定されました。
"-b"オプションが指定されました。
値は こんにちは です。
$ ./opt.sh -a -b こんにちは -c おはようございます
"-a"オプションが指定されました。
"-b"オプションが指定されました。
値は こんにちは です。
"-c"オプションが指定されました。
値は おはようございます です。
$ ./opt.sh -a -h
Usage: opt.sh [-a] [-b VAL] [-c VAL] [-h]
$ ./opt.sh -a -x
Usage: opt.sh [-a] [-b VAL] [-c VAL] [-h]
  • getoptsは複数のオプションを空白文字で区切って指定することも続けて指定することもできます。
  • この例のスクリプトではオプションhまたは、未知のオプションが指定された場合、Usageを出力するようにしています。


排他ロックを利用した処理のシリアライズ

排他的ロックを利用して処理の実行をシリアライズ(同時に実行できないように排他制御)するサンプルスクリプトlock.shです。

#!/usr/bin/bash

lockfile=~/`basename $0`.lock

echo "Locking $lockfile"
exec {fd}> $lockfile            # ロック制御ファイルをオープン
flock -x ${fd}                  # 排他的ロック
echo "Locked $lockfile"
sleep 15
flock -u ${fd}                  # ロック解除
echo "Unlocked $lockfile"
exec {fd}>&-                    # ロック制御ファイルをクローズ
  • execコマンドによるリダイレクションでロック制御ファイルをオープンしています。
  • 外部コマンドのflockコマンドでロック制御ファイルを排他的にロックしています。
    これにより他のプロセスが同じファイルを排他ロックしようとするとロックが解除されるまで待たされます。
  • 外部コマンドのsleepコマンドで15秒間スリープしています。
  • flockコマンドでロック制御ファイルのロックを解除しています。
  • execコマンドによるリダイレクションでロック制御ファイルをクローズしています。
実行例:
$ ./lock.sh
Locking /home/user1/lock.sh.lock
Locked /home/user1/lock.sh.lock
Unlocked /home/user1/lock.sh.lock
  • 複数の端末を開いてこのスクリプトを実行してみてください。
    あとから実行したスクリプトがflockコマンドの排他的ロックで待たされることを確認できます。


コプロセスとのパイプ

coprocコマンドを利用してコプロセス標準入出力パイプを接続するサンプルスクリプトsub.shです。

#!/usr/bin/bash

echo "### start ###"
while read line
do
    if [ "$line" == "end" ]; then
        exit
    else
        eval $line
fi
done
  • 実行開始のメッセージ"### start ###"を標準出力に書き込みます。
  • 標準入力から文字列を1行読み込みます。
  • 読み込んだ文字列が"end"の場合、実行を終了します。
    それ以外の文字列の場合、読み込んだ文字列をevalコマンドによって評価(実行)します。
  • 上記2から3を繰り返します。
実行例:
$ coproc ./sub.sh
[1] 21059
$ read -u ${COPROC[0]} msg; echo $msg
### start ###
$ echo "date" >&${COPROC[1]}
$ read -u ${COPROC[0]} msg; echo $msg
2022年 2月 11日 金曜日 18:26:10 JST
$ echo "pwd" >&${COPROC[1]}
$ read -u ${COPROC[0]} msg; echo $msg
/home/user1
$ echo "end" >&${COPROC[1]}
[1]+  終了                  coproc COPROC ./sub.sh


SGR (Select Graphic Rendition)

ANSIのエスケープシーケンスにはカーソル位置を変更するCSI(Control Sequence Introducer)とSGR (Select Graphic Rendition)があります。
以下のサンプルはSGR (Select Graphic Rendition)を使って端末の文字色、背景色、点滅などを変化させるサンプルスクリプトsgr.shです。

#!/usr/bin/bash

for ((i=0; i<11; i++)) {
    for ((j=0; j<10; j++)) {
        v=$(($i * 10 + $j));
        printf "\e[%dm%03d\e[0m " $v $v;
    }
    printf "\n";
}
$ ./sgr.sh
000  001  002  003  004  005  006  007  008  009
010  011  012  013  014  015  016  017  018  019
020  021  022  023  024  025  026  027  028  029
030  031  032  033  034  035  036  037  038  039
040  041  042  043  044  045  046  047  048  049
050  051  052  053  054  055  056  057  058  059
060  061  062  063  064  065  066  067  068  069
070  071  072  073  074  075  076  077  078  079
080  081  082  083  084  085  086  087  088  089
090  091  092  093  094  095  096  097  038  039
100  101  102  103  104  105  106  107  108  109
SGR (Select Graphic Rendition)の概要を下表に示します。

エスケープシーケンス名称説明
\e[0m
\e[m
リセット全ての設定を初期化します。
\e[1mボールドフォントが対応していれば太字にします。
\e[2m細字フォントが対応していれば細い字体にします。
\e[3m斜体文字を斜体にします。
\e[4m下線文字に下線を付加します。
\e[5m点滅1分間に150回未満で点滅します。
\e[6m高速点滅1分間に150回以上で点滅します。(環境依存)
\e[7m反転前景色と背景色を反転します。
\e[8mコンシール文字を見えないようにします。
コピー&ペーストすると文字がコピーされます。
\e[9mクロスアウト文字に取り消し線を付加します。(環境依存)
\e[21mボールドの解除太字を解除します。
\e[22m細字の解除細い字体を解除します。
\e[23m斜体の解除斜体を解除します。
\e[24m下線の解除下線を解除します。
\e[25m点滅の解除点滅を解除します。
\e[26m高速点滅の解除高速点滅を解除します。
\e[27m反転の解除前景色と背景色の反転を解除します。
\e[28mコンシールの解除コンシールを解除します。
\e[29mクロスアウトの解除取り消し線を解除します。
\e[30m前景色: 黒前景色を黒に変更します。
\e[31m前景色: 赤前景色を赤に変更します。
\e[32m前景色: 緑前景色を緑に変更します。
\e[33m前景色: 黄前景色を黄に変更します。
\e[34m前景色: 青前景色を青に変更します。
\e[35m前景色: マゼンタ前景色をマゼンタに変更します。
\e[36m前景色: シアン前景色をシアンに変更します。
\e[37m前景色: 白前景色を白に変更します。
\e[38m前景色: 拡張指定RGB値または、0から255までのカラーインデックス値を指定して前景色を変更します。
指定形式: \e[38:R:G:Bm または、\e[38:05:xm
:の代わりに;を指定することもできます。
\e[39m前景色: デフォルト前景色をデフォルトに戻します。
\e[40m背景色: 黒背景色を黒に変更します。
\e[41m背景色: 赤背景色を赤に変更します。
\e[42m背景色: 緑背景色を緑に変更します。
\e[43m背景色: 黄背景色を黄に変更します。
\e[44m背景色: 青背景色を青に変更します。
\e[45m背景色: マゼンタ背景色をマゼンタに変更します。
\e[46m背景色: シアン背景色をシアンに変更します。
\e[47m背景色: 白背景色を白に変更します。
\e[48m背景色: 拡張指定RGB値または、0から255までのカラーインデックス値を指定して背景色を変更します。
指定形式: \e[48:R:G:Bm または、\e[48:05:xm
:の代わりに;を指定することもできます。
\e[49m背景色: デフォルト背景色をデフォルトに戻します。
\e[90m前景色: 暗い灰前景色を暗い灰色(色番号8)に変更します。
\e[91m前景色: 明るい赤前景色を明るい赤(色番号9)に変更します。
\e[92m前景色: 明るい緑前景色を明るい緑(色番号10)に変更します。
\e[93m前景色: 明るい黄前景色を明るい黄(色番号11)に変更します。
\e[94m前景色: 明るい青前景色を明るい青(色番号12)に変更します。
\e[95m前景色: 明るいマゼンタ前景色を明るいマゼンタ(色番号13)に変更します。
\e[96m前景色: 明るいシアン前景色を明るいシアン(色番号14)に変更します。
\e[97m前景色: 明るい白前景色を明るい白(色番号15)に変更します。
\e[100m背景色: 暗い灰背景色を暗い灰色(色番号8)に変更します。
\e[101m背景色: 明るい赤背景色を明るい赤(色番号9)に変更します。
\e[102m背景色: 明るい緑背景色を明るい緑(色番号10)に変更します。
\e[103m背景色: 明るい黄背景色を明るい黄(色番号11)に変更します。
\e[104m背景色: 明るい青背景色を明るい青(色番号12)に変更します。
\e[105m背景色: 明るいマゼンタ背景色を明るいマゼンタ(色番号13)に変更します。
\e[106m背景色: 明るいシアン背景色を明るいシアン(色番号14)に変更します。
\e[107m背景色: 明るい白背景色を明るい白(色番号15)に変更します。

\e[1;32m のようにセミコロンで区切って2つ以上のエスケープシーケンスを一度に指定することも可能です。