簡単なシェルスクリプトの備忘録

### シェルスクリプトでの引数の取り方と文字列比較

```shell-session:test.sh
#!/bin/sh

echo $1

if [ $1 == "abc" ]; then
    echo $2
fi
```

実行方法と結果

```shell-session
sh test.sh a 123

#=> a

sh test.sh abc 123

#=> abc
#=> 123

```

### シェルスクリプトでの引数で渡したファイルの読み取り

```text:test1.txt
abc 111
bcd 222
efg 333
abc 444
abcd 555
ab  666
1abc  777
1ab 888
```
```shell-session:test2.sh
#!/bin/bash

cat $1 | while read line
do
    echo "${line}"
done
````

実行方法と結果

```shell-session
sh test2.sh test1.txt

#=> abc 111
#=> bcd 222
#=> efg 333
#=> abc 444
#=> abcd 555
#=> ab  666
#=> 1abc  777
#=> 1ab 888
```
### シェルスクリプトでの引数で渡したファイルの読み取り(スペース区切りで配列的な読み込み)

```shell-session:test3.sh
#!/bin/bash

cat $1 | while read col1 col2
do
    echo "1:${col1}|2:${col2}"
done
```

実行方法と結果

```shell-session
sh test3.sh test1.txt
 
#=> 1:abc|2:111
#=> 1:bcd|2:222
#=> 1:efg|2:333
#=> 1:abc|2:444
#=> 1:abcd|2:555
#=> 1:ab|2:666
#=> 1:1abc|2:777
#=> 1:1ab|2:888
```

### シェルスクリプトでの外部パラメータファイルの読み込み

```text:test.inc
val1="abc"
val2="def"
val3=123;
```

```shell-session:test4.sh
#!/bin/sh

. $1 

echo "val1:${val1}"
echo "val2:${val2}"
echo "val3:${val3}"
```

実行方法と結果

```shell-session
sh test4.sh test.inc

#=> val1:abc
#=> val2:def
#=> val3:123
```
### シェルスクリプトでのエラー出力する

```shell-session:test5.sh
#!/bin/sh

if [ $1 == "stdout" ]; then
  echo "OK"
fi

if [ $1 == "stderr" ]; then
  echo "error" >&2
  exit 1;
fi
````
実行方法と結果

```shell-session
## 標準出力(正常終了)
sh test5.sh stdout

#=> OK

## 終了ステータス
echo $?

#=> 0

## エラー出力(エラー終了)
sh test5.sh stderr 1>/dev/null

#=> error

## 終了ステータス
echo $?

#=> 1

```
### シェルスクリプトでリストでループする
```shell-session:test6.sh
#!/bin/bash

for STR in "aa" "bb" "cc" "dd"
do
    echo "VAL: ${STR}"
done
```
実行方法と結果

```shell-session
sh test6.sh 
VAL: aa
VAL: bb
VAL: cc
VAL: dd
```
### シェルスクリプトでリスト(配列)でループする
```shell-session:test7.sh
#!/bin/bash

ary=("ab" "cd" "ef" "gh")

for STR in ${ary[@]}
do
    echo "VAL: ${STR}"
done
```
実行方法と結果

```shell-session
sh test7.sh 
VAL: ab
VAL: cd
VAL: ef
VAL: gh
```
### バッチ処理のシェルスクリプトでのエラーが発生した場合にメール送信する

単一シェルでエラー判別してメール送信しても良いが、シェルを単純化したいので、メイン処理は子スクリプトで処理し、エラー判別は、OSの標準エラー出力機能を使用した親スクリプトで行い、エラー時には親スクリプトでメール送信する

* メイン処理用(子スクリプト)サンプル

単純にfile1で指定したファイルを削除するだけのスクリプト

```shell-session:batch_filedelete.sh 
#!/bin/sh

file1=/tmp/testfile.txt

ls ${file1}
rm ${file1}
```

* 実行用親スクリプトサンプル

子スクリプトを実行するスクリプトで 実行可否ログ、実行詳細ログ、エラーログを出力し、エラー時にはメールを指定したアドレスにメール送信する

```shell-session:batch_filedelete_run.sh 
#!/bin/sh

export LANG=C

run_date=`date +"%Y%m"`
hostname=`hostname`
batch="batch_filedelete"
batchname="${hostname}.${batch}"
run_script="${batch}.sh"
# log
log1="/var/log/batch/${run_date}.${batchname}.log"
log2="/var/log/batch/${run_date}.${batchname}.status.log"
log3="/var/log/batch/${run_date}.${batchname}.error.log"
# mail
mail_title="[batch]${batchname}:NG"
mail_to="xxxx@localhost"
mail_from="batch@localhost"

# start batch script
stime=`date '+%Y/%m/%d %X'`
echo "${stime} | ${batchname} | start" >> ${log1}
echo "${stime} | ${batchname} | start" >> ${log2}

# run script file
/bin/sh ${run_script} 1>>${log2} 2>${log3}

# end batch script (NG end/OK end)
etime=`date '+%Y/%m/%d %X'`
if [ -s "${log3}" ]; then
    # NG end
    cat "${log3}" | mail -s "${mail_title}" -r "${mail_from}" "${mail_to}"
    echo "${etime} | ${batchname} | end | NG" >> ${log1}
    echo "${etime} | ${batchname} | end | NG" >> ${log2}
else
    # OK end
    echo "${etime} | ${batchname} | end | OK" >> ${log1}
    echo "${etime} | ${batchname} | end | OK" >> ${log2}
    rm -f "${log3}"
fi
```

実行方法と結果

```shell-session
## 正常終了時テスト
# 削除するファイルを作成する
touch /tmp/textfile.txt

sh batch_filedelete_run.sh 

## エラー終了時テスト
# そのまま実行する、削除するファイルがないのでエラーになる
sh batch_filedelete_run.sh 
```

### バッチ処理のlogをチェックするスクリプト

バッチでerrorが発生した場合にメール送信するようにしておくだけでは、根本的にバッチが動かないような事象(未実行等)の際に発見することはできない。しかし、正常終了の際にメール送信して、正常終了メールが来ない場合をエラー(未実行等)としてキャッチするとして、仮に毎時2本、1日1回2本のバッチを2台のサーバーで実行させると、1日に100通のメールを受け取ることになる、さらにバッチ数やサーバー台数が増えていくと、メールフィルターを駆使したとしても、未到達によるエラー(未実行等)を発見するのはとても困難となる。そのために、実行ログを集積サーバに集めて、その実行ログに期待するログが含まれているかを1本のバッチでチェックすれば、100のログでも1本のメールに 集約することが可能となるため、そのようなスクリプトを考えてみる

* 集めるログのフォーマットは[バッチ処理のシェルスクリプトでのエラーが発生した場合にメール送信する](#バッチ処理のシェルスクリプトでのエラーが発生した場合にメール送信する)のログ形式
* ログの集積部分はこのスクリプトでは考えない(バッチ側の最後にscpやrsyncや別のミドルウェアなどで実施するとする)
* cronで毎時実行すると、本スクリプトで1日1回〜毎時実行までの、バッチのログのチェックが可能
    (例えば、55 * * * * /bin/sh batch_check.sh 1>/dev/null 2>&1 設定し、10時台のチェックが走ったとすると、ログの最終レコードが10:00〜10:54までに "end | OK" が出ているかをチェックする)    
* スクリプト自体を単純化するために、目的時単位でのリストファイルを作成することにする
   (00時台のチェックは00.list、01時台のチェックは01.list)   
*  実行タイミングや実行時間の関係で、ログの時刻が時台を跨ぐ場合には、offsetに1を設定すると1時間までは対応する
   (例えば10時台のチェックで ログの最終レコードが9:00〜10:54までに "end | OK" が出ているかをチェックが可能になる)
*  日跨ぎに関しては考慮する
   (00時台のチェックで ログの最終レコードが前日の23:00〜当日の00:54までに "end | OK" が出ているかをチェック可能)
* ただし、ログの集約単位以上の跨ぎに関しては、考慮しない。つまり、チェック不可能
  ([バッチ処理のシェルスクリプトでのエラーが発生した場合にメール送信する](#バッチ処理のシェルスクリプトでのエラーが発生した場合にメール送信する)のログの集約単位が月単位の為、月を跨ぐ日跨ぎに関してはチェック不可能)

```shell-session:00.list
# batch_name hostname log_path log_name offset
batch_1 srv1 /home/batch/log/srv1 .srv1.batch_1.log 0
batch_2 srv1 /home/batch/log/srv1 .srv1.batch_2.log 0 
batch_3 srv1 /home/batch/log/srv1 .srv1.batch_3.log 1
batch_1 srv2 /home/batch/log/srv2 .srv2.batch_1.log 0
batch_2 srv2 /home/batch/log/srv2 .srv2.batch_2.log 0 
batch_3 srv2 /home/batch/log/srv2 .srv2.batch_3.log 1
```

```shell-session:batch_check.sh
#!/bin/sh

# mail ini
mail_to="xxxx@localhost"
mail_from="batch@localhost"
mail_subject="Batch Log Check Hourly Report"

# set check date
dt=`date +"%Y%m"`
tday=`date +"%d"`
hour=`date +"%H"`

###################
### subroutine

# record_check
# $1:check hour | $2:check date | $3:offset | $4:log_time | $5:batchname | $6:runhost | $7:start/end | $8:OK/NG 
function record_check()
{
    log_hour=`date -d "${4}" "+%H"`
    log_day=`date -d "${4}" "+%d"`

    # record check
    if [ ${7} == "end" ] && [ ${8} == "OK" ] && [ ${3} -ne 0 ] ; then

        offset_hour=`date -d "${4} ${3} hour" "+%H"`
        offset_day=`date -d "${4} ${3} hour" "+%d"`

        if [ ${2} == ${log_day} ] && [ ${1} == ${log_hour} ] ; then
            echo "OK | ${6}-${5}" >> /tmp/batch-log-check-body
        elif [ ${2} == ${offset_day} ] && [ ${1} == ${offset_hour} ] ; then
            echo "OK | ${6}-${5}" >> /tmp/batch-log-check-body
        else
            echo "NG | ${6}-${5}" >> /tmp/batch-log-check-body
        fi

    elif [ ${7} == "end" ] && [ ${8} == "OK" ] && [ ${2} == ${log_day} ] && [ ${1} == ${log_hour} ] ; then
        echo "OK | ${6}-${5}" >> /tmp/batch-log-check-body
    else
        echo "NG | ${6}-${5}" >> /tmp/batch-log-check-body
    fi
}

# mail_send
# $1:subject | $2:from | $3:to
function mail_send()
{
    cat /tmp/batch-log-check-body | mail -s "${1}" -r "${2}" "${3}"
    rm -f /tmp/batch-log-check-body
} 

#################
## main

# output date
echo "check date:${dt}${tday} hour:${hour}" > /tmp/batch-log-check-body

# check ini file
if [ ! -e "${hour}.list" ]; then
    echo "Not exist ${hour}.list" >> /tmp/batch-log-check-body
    mail_send "${mail_subject}" "${mail_from}" "${mail_to}"
    exit 1
fi

# read ini file
cat "./${hour}.list" |grep -v "^#"| while read batchname runhost path log offset
do
    # check log file
    if [ ! -e "${path}/${dt}${log}" ]; then
        echo "Not exist ${path}/${dt}${log}" >> /tmp/batch-log-check-body
        continue
    fi
        # read log file 
        tail -n1 "${path}/${dt}${log}" | (IFS="|" read log_time log_name log_status1 log_status2; record_check "${hour}" "${tday}" "${offset}" "${log_time}" "${batchname}" "${runhost}" "${log_status1}" "${log_status2}")
done

mail_send "${mail_subject}" "${mail_from}" "${mail_to}"
```
cronに毎時55分に実行するように設定

```shell-session:crontab
55 * * * * /bin/sh batch_check.sh 1>/dev/null 2>&1
```

### itamae実行用のスクリプト

itamaeをCLIで手動コマンド実行してしまうと、Infrastructure as Codeが台無しになってしまうので、レシピ実行をスクリプトにまとめてやる
実行するレシピは[ここ](https://github.com/hirotaka-tajiri/itamae_cookbooks)
また、個人的な環境ではRVMでrubyをセットアップしているので、RVM用のスクリプトになっている

```shell-session:itamae_centos7-web.sh
#!/bin/bash --login

# RVM gemset 
rvm use ruby-2.3.3@itamae

# target host
target_host="192.168.56.102"
run_user="xxxx"
p_key="/root/ssh_keys/id_ed25519_xxx"

# itamae recipe
list_repo=("epel.rb" "remi.rb")
list_recipe=("centos7_default.rb" "centos7_chrony.rb" "selinux.rb" "ldap_client.rb" "sshd.rb" "php71.rb")

# execute or dry-run
if [ $# == 1 ]; then
    if [ $1 == "--dry-run" ]; then
        DRY_RUN="--dry-run"
    fi
fi

###
# yum repository
cd /root/itamae_cookbooks/repos

for str in ${list_repo[@]}
do
    itamae ssh -h ${target_host} -u ${run_user} -i ${p_key} ${str} ${DRY_RUN}
done 

###
# os
cd /root/itamae_cookbooks/os_package

# with json
    itamae ssh -h ${target_host} -u ${run_user} -i ${p_key} --node-json files/centos7-web.json hostname.rb ${DRY_RUN}

# without json
for str in ${list_recipe[@]}
do
    itamae ssh -h ${target_host} -u ${run_user} -i ${p_key} ${str} ${DRY_RUN}
done 
```