Edited at

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

More than 1 year has passed since last update.


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


test.sh

#!/bin/sh


echo $1

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

実行方法と結果

sh test.sh a 123

#=> a

sh test.sh abc 123

#=> abc
#=> 123


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


test1.txt

abc 111

bcd 222
efg 333
abc 444
abcd 555
ab 666
1abc 777
1ab 888


test2.sh

#!/bin/bash


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

実行方法と結果

sh test2.sh test1.txt

#=> abc 111
#=> bcd 222
#=> efg 333
#=> abc 444
#=> abcd 555
#=> ab 666
#=> 1abc 777
#=> 1ab 888


シェルスクリプトでの引数で渡したファイルの読み取り(スペース区切りで配列的な読み込み)


test3.sh

#!/bin/bash


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

実行方法と結果

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


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


test.inc

val1="abc"

val2="def"
val3=123;


test4.sh

#!/bin/sh


. $1

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

実行方法と結果

sh test4.sh test.inc

#=> val1:abc
#=> val2:def
#=> val3:123


シェルスクリプトでのエラー出力する


test5.sh

#!/bin/sh


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

if [ $1 == "stderr" ]; then
echo "error" >&2
exit 1;
fi


実行方法と結果

## 標準出力(正常終了)

sh test5.sh stdout

#=> OK

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

#=> 0

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

#=> error

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

#=> 1


シェルスクリプトでリストでループする


test6.sh

#!/bin/bash


for STR in "aa" "bb" "cc" "dd"
do
echo "VAL: ${STR}"
done

実行方法と結果

sh test6.sh 

VAL: aa
VAL: bb
VAL: cc
VAL: dd


シェルスクリプトでリスト(配列)でループする


test7.sh

#!/bin/bash


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

for STR in ${ary[@]}
do
echo "VAL: ${STR}"
done


実行方法と結果

sh test7.sh 

VAL: ab
VAL: cd
VAL: ef
VAL: gh


バッチ処理のシェルスクリプトでのエラーが発生した場合にメール送信する

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


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

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


batch_filedelete.sh

#!/bin/sh


file1=/tmp/testfile.txt

ls ${file1}
rm ${file1}



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

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


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


実行方法と結果

## 正常終了時テスト

# 削除するファイルを作成する
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" が出ているかをチェック可能)

  • ただし、ログの集約単位以上の跨ぎに関しては、考慮しない。つまり、チェック不可能
    (バッチ処理のシェルスクリプトでのエラーが発生した場合にメール送信するのログの集約単位が月単位の為、月を跨ぐ日跨ぎに関してはチェック不可能)


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


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分に実行するように設定


crontab

55 * * * * /bin/sh batch_check.sh 1>/dev/null 2>&1



itamae実行用のスクリプト

itamaeをCLIで手動コマンド実行してしまうと、Infrastructure as Codeが台無しになってしまうので、レシピ実行をスクリプトにまとめてやる

実行するレシピはここ

また、個人的な環境ではRVMでrubyをセットアップしているので、RVM用のスクリプトになっている


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