最近はシェルプログラムの勉強と実践が多いけど、今日はとても簡単なシェルスクリプトを書いたけど、結構ハマったので、忘れないように記録しておきたい。
プログラムの仕様
とても簡単なもので、複数のエンドポイントの、死活情報をCUIの画面として表示するといった内容。出来たのはこんな感じ。
何故か最初の一行だけズレているのは理由がわかりませんが、継続調査。まずは、動くものを提供するのが重要と考えました。
こんな簡単なモノなのに、色々ハマりました。シェルプログラムおそるべし。まぁ、ラーニングコースで学び終わった程度なので最初はそんなモノかも。わからなかった部分を整理していきたいと思います。
ハマりポイント
ハマったポイントを解説付きで整理していきます。
read コマンド
最初はパラメータだったのですが、複数のエンドポイントのデータを表示するので、stdin から入力するようなスタイルにしました。
$ dashboard < endpoints.txt
のような形で。内容は次の通り。
endpoint.txt
https://qiita.com/TsuyoshiUshio@github/items/33f4236fdb0717a783c1
https://github.com/
https://www.tldp.org/LDP/abs/html/varsubn.html
ところが、read endpoints
などで読み込もうとしたら、何故か1件しか読み込まれません。IFS='\n'
とやって、区切り文字を変えても改行が認識されない感じです。なんでやねんと思いましがが、こういう時は、仕様に戻るのが正解です。改めて、manual を見てみると、read は改行までの1行を読むので、1回しか読まないと。最初の1行しか取得出来ないのは当然です。
ですので、終わりまで読み込みましょう。ちなみに下記のコードになっている理由は、あまり意識せずにかくと、read -r ep
の結果で判断してしまいそうですが、そうすると、最終行でEOFが来る時に、read は、false を返します。だから、最後の1行まで読むためには、EOFファイルが来ても、そこに行があったらフラッシュしておきます。
DONE=false
declare -a endpoints
declare -i index=0
until $DONE ; do
read -r ep || DONE=true
endpoints[$index]=$ep
(( index++ ))
done
こうすれば、配列にエンドポイント情報が代入されます。
Arrayへの代入
Array を定義して、Array に値を代入する方法も書き方が最初わかりませんでした。結論としてはこんな感じ。Arrayの定義は、declare -a
フラグで。その変数への個別の代入は、endpoint[$index] で。さらにややこしいことに、Arrayのループを回したい時は、
for endpoint in ${endpoints}; do ではダメで、
for endpoint in ${endpoints[@]} にしないと、ループになってくれません。 また、個別の値を参照したい場合の書き方は、
${endpoints[index]}` 中のindex には$が不要なことがポイントです。
readfile.sh
#!/bin/bash
DONE=false
declare -a endpoints
declare -i index=0
until $DONE ; do
read -r ep || DONE=true
endpoints[$index]=$ep
echo "${index}:$ep :${endpoints[index]}"
(( index++ ))
done
echo ${#endpoints[@]}
declare -i i=0
for endpoint in ${endpoints[@]} ; do
i="${i}+1"
echo "${i}: $endpoint"
done
複数の空白を出力する
これが最大限にハマりました。空白を出力するコードを書いてあげて、普通に表示すると、下記の感じになって、思いっきりスペースが無視されます。これは、Bashでは、スペースがデリミタであるので、普通に出力するとスペースを全く表示してくれません。
*** DevOps OpeanHack health report ***
------------------------------------------------------------------------------------------------
| Timestamp | Status | Endpoint |
例えば、次のプログラムはうまく動きません。
whitespace.sh
#!/bin/bash
lines() {
declare -i number=$1
declare delimiter=$2
result=""
for (( i=0; i<$number; ++i )); do
result="$delimiter$result"
done
printf '%b\n' $result
}
echo $(lines 10 ",")
echo "start:$(lines 10 ' '):end"
結果
,
はちゃんと出力されていますが、
は無視されています。
$ ./whitespace.sh
,,,,,,,,,,
start::end
いろんな方法を試して、やっといい感じでかける書き方がわかりました。
declare endpoint=$2
declare max_endpoint_length=$3
declare -i space_length=0
(( space_length=$max_endpoint_length-${#endpoint} ))
spaces=$(printf "%-${space_length}s" " ")
ポイントとしては、printf を使っていることと、%-${..}s
というテンプレートを使っています。これは、特定の1文字を数字だけ表示するというテンプレートです。例えば
$ printf "%-20s:finish" " "
:finish
という感じで変数にホワイトスペースを格納することが出来ます。
-
を表示する
他に困ったのが、-
の表示です。Printf の方がフォーマットができるので、便利なのですが、シェルはメソッドサブスティチューションなので、そのまま printf にすると、次のようなエラーが発生して、オプションと勘違いされます。そうしないためには、先頭に---
をつけて、パラメータの意味をキャンセルするか、echoで表示するか、フォーマットしてしまうかになります。
$ b="-----"
$ echo "$b"
-----
$ printf "$b"
-bash: printf: --: invalid option
printf: usage: printf [-v var] format [arguments]
$ printf "%s\n" $b
-----
$ printf -- "$b"
-----
日付の表示
直接フォーマットをかけることが出来ます。
timestamp=$(date "+%Y%m%d-%H%M%S")
画面のクリアとエスケープシーケンス
エスケープシーケンスを使うと画面クリアとか出来たり、色をつけたりすることが出来ます。エンドポイントを定期ポーリングアプリなので、Top的な表示がいいと思いました。残念ながら、Mac では、自分で新しいバージョンの、bashを入れないといけないので、今回は単純にclear
コマンドを使って、決して、再描画する方法にしました。ちなみに新しいbashのためには、brew install bash
とかで入れる必要があります。
このブログとかいい感じです。
## 標準入力で、リダイレクトの入力があるか判断する
基本的な使い方は、標準入力からのリダイレクトを想定しているのですが、リダイレクトがされていない時は、ヘルプのメッセージを表示したいと思いました。こんな感じの実装です。-s
は、サイズが0より上かを判断してくれます。ちなみに、パイプだと、-p
が使えます。詳しくは7.1.1.1. Expressions used with if
if [ -s /dev/stdin ]; then
echo ""
else
usage
exit 1
fi
コード全体
dashboard
#!/bin/bash
usage() {
cat <<END
Usage: dashboard
Report the health status of the endpoints. Read data from standard input.
The data should be separated by '\n'.
e.g.
dashboard < endpoints.txt
END
}
healthcheck() {
declare url=$1
result=$(curl -I $url 2>/dev/null | grep HTTP/1.1)
echo $result
}
lines() {
declare -i number=$1
declare delimiter=$2
result=""
for (( i=0; i<$number; ++i )); do
result="$delimiter$result"
done
printf '%b\n' $result
}
header() {
declare -i endpoint_length=$1
declare -i line_length
declare -i space_length=0
line_length="$endpoint_length+31"
title="*** DevOps OpeanHack health report ***"
line=$(lines $line_length '-')
(( space_length=$endpoint_length-8 ))
spaces=$(printf "%-${space_length}s" " ")
header="| Timestamp | Status | Endpoint ${spaces}|"
printf "%b\n" "$title\n$line\n${header}\n$line\n"
}
footer() {
declare -i endpoint_length=$1
declare -i line_length
(( line_length=$endpoint_length+31 ))
line=$(lines "${line_length}" "-")
printf "%s\n" "$line\n"
}
display() {
declare status=$1
declare endpoint=$2
declare endpoint_length=$3
declare -i space_length=0
timestamp=$(date "+%Y%m%d-%H%M%S")
(( space_length=$endpoint_length-${#endpoint} ))
spaces=$(printf "%-${space_length}s" " ")
echo "| $timestamp | $status | $endpoint ${spaces}|"
}
trimStatus() {
read response
if [ -z "$response" ]; then
echo " Dead "
else
if [[ $response =~ 'HTTP/1.1 200' ]]; then
echo " Alive "
else
echo " Dead "
fi
fi
}
if [ -s /dev/stdin ]; then
echo ""
else
usage
exit 1
fi
declare -i max_endpoint_length=0
DONE=false
declare -a endpoints
declare -i index=0
until $DONE ; do
read -r ep || DONE=true
endpoints[$index]=$ep
(( index++ ))
done
for endpoint in ${endpoints[@]} ; do
if [[ ${#endpoint} > $max_endpoint_length ]]; then
max_endpoint_length=${#endpoint}
fi
done
while [[ true ]]; do
declare -a messages
declare -i i=0
for endpoint in ${endpoints[@]} ; do
statusBuffer=$(healthcheck $endpoint)
status=$(echo $statusBuffer | trimStatus)
messages[$i]="$(display $status $endpoint $max_endpoint_length)"
(( i++ ))
done
clear
printf "$(header $max_endpoint_length)\n"
for message in "${messages[@]}" ; do
echo "$message"
done
printf -- "$(footer $max_endpoint_length)\n"
sleep 3
done
実行
リダイレクトの標準入力がなければ、usage を表示します。
$ ./dashboard
Usage: dashboard
Report the health status of the endpoints. Read data from standard input.
The data should be separated by '\n'.
e.g.
dashboard < endpoints.txt
実行すると画面がクリアされてポーリングされます。
```bash
$ dashboard < endpoints.txt
*** DevOps OpeanHack health report ***
------------------------------------------------------------------------------------------------
| Timestamp | Status | Endpoint |
------------------------------------------------------------------------------------------------
| 20190320-225245 | Alive | https://qiita.com/TsuyoshiUshio@github/items/33f4236fdb0717a783c1 |
| 20190320-225245 | Alive | https://github.com/ |
| 20190320-225246 | Alive | https://www.tldp.org/LDP/abs/html/varsubn.html |
------------------------------------------------------------------------------------------------
残課題
なぜか、最初の1行だけ最後の|
がズレています。2行目以降と全く同じアルゴリズムなので、おそらく、データが影響しているのと思いますが、特定は出来ていません。
まとめ
なんとか、シェルスクリプトを作ることが出来ました。生からリファレンスなしでゴリゴリ書いたいのははじめてですが、なんかダサさも感じます。もっと良い書き方があれば是非コメントいただければと思います。