LoginSignup
2
0

More than 5 years have passed since last update.

Shell Script で苦労したところを書いてみる

Posted at

最近はシェルプログラムの勉強と実践が多いけど、今日はとても簡単なシェルスクリプトを書いたけど、結構ハマったので、忘れないように記録しておきたい。

プログラムの仕様

とても簡単なもので、複数のエンドポイントの、死活情報をCUIの画面として表示するといった内容。出来たのはこんな感じ。

Screen Shot 2019-03-20 at 10.54.49 PM.png

何故か最初の一行だけズレているのは理由がわかりませんが、継続調査。まずは、動くものを提供するのが重要と考えました。

こんな簡単なモノなのに、色々ハマりました。シェルプログラムおそるべし。まぁ、ラーニングコースで学び終わった程度なので最初はそんなモノかも。わからなかった部分を整理していきたいと思います。

ハマりポイント

ハマったポイントを解説付きで整理していきます。

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
```

実行すると画面がクリアされてポーリングされます。

$ 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行目以降と全く同じアルゴリズムなので、おそらく、データが影響しているのと思いますが、特定は出来ていません。

まとめ

なんとか、シェルスクリプトを作ることが出来ました。生からリファレンスなしでゴリゴリ書いたいのははじめてですが、なんかダサさも感じます。もっと良い書き方があれば是非コメントいただければと思います。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0