11
16

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-10-19

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

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 
11
16
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
11
16