shell script の書き方

# 概要
> このドキュメントは「シェルスクリプトは好きじゃない or 書けない」という人が居たので、なんとなく書いてみたものです。

---
# ◎ シェルスクリプトとは
> `shell` はオペレーティングシステム （OS） のユーザーのためにインタフェースを提供するソフトウェアで、カーネルのサービスへのアクセスを提供します。
> そのシェルを使用して、外部 Un*x コマンドを呼び出し処理を行います。
> 要するに、Un*x 系の OS に入っている sh を使ったプログラム、という事です。

---
# ◎ どの sh を使うか
> `/bin/sh` を使う事が多いですが、起動スクリプト等では `/sbin/sh` を使う事もあります。
> また`ksh` や高機能な `bash, zsh` を使う事もあります。
> 
> `ksh, bash, zsh` 等は `B Shell` 系と言われている種類のものです。
> `Bourne shell` からの進化系という事でそう呼ばれています。
> 対して、`C Shell` 系というものがあります。
> これは `C Shell` と `tcsh` くらいしかありません。
> ログインシェルでは `tcsh` が使われる事もあります。
> `RH 系 Linux` のログインシェルはデフォルトでは `bash` です。
> `Debian 系 Linux` の場合は `dash` が使われています。
> この辺りの話は Un*x の歴史（系譜）の話になってくるので興味のある方は調べてみると良いでしょう。
> 
> `bash` 依存で実装すると、微妙にバグに悩まされるケースがあります。
> 常に `bash` の最新版が使えれば問題は少ないですが、古い環境の場合は注意が必要です。
> 出来れば安定して枯れている `ksh` を使うのが良いでしょう。
> （この辺は好みだったり要件だったりします。）
> それと出来る限りビルトインな機能ではなく、外部コマンドを使用しましょう。
> 実行速度は圧倒的にビルトインな機能の方が速いですが、`man` に詳細が無かったりするので、使い方を誤ったり変な機能制限に悩まされる可能性があります。
> 
> C 言語と同じように記述できる `csh` は今や好きな人しか使いません。あえて使う理由は余りないです。
> そして本テキストは `csh` に対応していません。（ちょっとだけ記載していますが、、、）

---
# ◎ おまじない（シェバン 或いはシバン）
> 必ず一行目に記述します。
> `/bin/sh` が実際にないとだめです。
> `/bin/sh` を実行環境として使います。という意味です。
> 環境によっては `/usr/bin/sh` だったり `/sbin/sh` だったりします。

```sh
#!/bin/sh
```

---
# ◎ コメント記号
> 「#（シャープ）」をつける事でコメント扱いになります。

---
# ◎ その他
> ~~デバッガ等の便利なものはありません。デバッグする場合は、echo 或いは printf を使用して行います。~~
> 最近は [shellcheck](https://www.shellcheck.net/) という便利なものがあります。
> バイナリでも配されていて yum や atp でもインストール可能ですので使うと良いでしょう。
> 作成したら必ず実行権限を付けてください。じゃないと実行できません。

```shell-session
chmod u+x ./test.sh
```

---
# ◎ 構文確認
> sh のオプションを使用して構文確認をします。
> 何も表示されなければ構文 OK です。

```shell-session
sh -n test.sh
```

---
# ◎ 実行ログ出力
> sh のオプションを使用して実行ログを出力して実行します。
> 処理の詳細が出力されますので、意図しない動きをしていないか確認します。

```shell-session
sh -x test.sh
```

---
# ◎ そのほかのオプション
> 上記以外にも sh のオプションを使用して様々なチェックができます。
> 実際に実行する前、或いは UT としてこれらのオプションを使ったチェックをすると品質の良いものになると思います。

- -e
    - 実行したコマンドが0でないステータスで終了した場合、即座に終了するオプション
- -v
    - シェルスクリプト内でこれから実行されるコマンドを表示するオプション
    - 変数が使用されている場合は -x オプションとは異なり、変数名がそのまま表示される

<font color="Red">
※ 偶に、e オプションを使った例外処理をしている人がいますが、大変理解しずらくなるので辞めましょう。
</font>

---
# ◎ 環境変数
> 環境変数とはシェル環境の設定です。
> 最近の bash は高機能で、実行するユーザの環境変数を読み込んでシェルスクリプトを実行してくれます。
> よって、敢えてシェルスクリプト内で一般的な環境変数を記述しなくても動きます。
> しかし bash 以外の場合は、動かない事も多いので、汎用性を持たせる為に敢えて環境変数はシェルスクリプト内で使う環境変数は必ず設定をするようにします。

## B シェル系
```sh
export ＜環境変数名＞="ほげ"
又は
＜環境変数名＞="ほげ"; export ＜環境変数名＞
```

## C シェル系
```sh
setenv ＜環境変数名＞ "ほげ"
```

---
# ◎ シェル変数
> シェル変数とは、シェルスクリプト内で使用する変数です。
> 気を付けなくてはならないのが「=」の書き方です。
> 他の言語と違って「＝」の左右に *スペース* は入れては ***ダメ*** です。

## 変数宣言１
> そのまま記述します。ダブルクォーテーションでくくった時と扱いは同じです。

```sh
hoge=hage
```

## 変数宣言２
> ダブルクォーテーション `"` でくくります。中に変数が含まれる場合は展開されます。

```sh
hoge="hage"
```
```
例）
DIR="/you/dir/path"
hoge="${DIR}/hage"
echo ${hoge} ⇒ /you/dir/path/hage
```

## 変数宣言３
> シングルクォーテーション `'` でくくります。中身は全てクオートされて展開されません。

```sh
hoge='hage'
```
```
例）
DIR="/you/dir/path"
hoge='${DIR}/hage'
echo ${hoge} ⇒ ${DIR}/hage
```

## 変数宣言４
> バッククォーテーションでくくります。展開した値を変数に格納します。
> コマンド実行結果を格納する場合に使用します。
> 現在 ksh では非推奨になっており、将来サポートとされなくなる古い書き方です。

```sh
hoge=`hage`
```
```
例）
hoge=`date +%Y/%m/%d`
echo ${hoge} ⇒ 2014/06/15
```

## 変数宣言５
> `$()` でくくります。展開した値を変数に格納します。
> コマンド実行結果を格納する場合に使用します。
> ksh では現在こちらを推奨しており、bash 等も追随する可能性がありますので、こちらで覚えておきましょう。

```sh
hoge=$(hage)
```
```
例）
hoge=$(date +%Y/%m/%d)
echo ${hoge} ⇒ 2014/06/15
```

---
# ◎ 変数の呼び出し方
> いくつかの書き方があります。

## 変数１
> `{}` でくくります。
> くくった方が分かりやすいです。
> また旧い sh の場合はこの書き方じゃないとダメな場合もあります。

```sh
${hoge}
```

## 変数２
> 現在の sh では通常この書き方をします。が、文字列連結をした場合等でいらぬ不具合を生むのでお勧めしません。

```sh
$hoge
```

---
# ◎ ヘッダ
> 必ずヘッダを記載するようにしましょう。
> 処理の概要、引数、リターンコード、くらいは記載してください。

## シェルスクリプトのヘッダ
```sh
#!/bin/sh
#======================================================================================
#
# システム名     ： テンプレート
# ファイル名     ：
# 処理概要       ： シェル関数
# 変更履歴 NO/版 ： 1
# 
# 引数           型             説明
# -------------- -------------- -------------------------------------------------------
# なし
# 
# 戻り値         ：  0 = 正常終了
#                   >0 = 異常終了
#
#======================================================================================
```

## シェル関数のヘッダ
```sh
###------------------------------------------------------------------------------------
### 名前 ： シェル関数用ヘッダ
### 説明 ： 一つの関数につき一つのヘッダを記載する
### 引数 ： $1 第一コマンドライン引数の説明
###         $2 第二コマンドライン引数の説明
### 戻値 ：   0 = 正常終了
###          >0 = 異常終了
### 特記 ： 
###------------------------------------------------------------------------------------
```

## 各処理のヘッダ
```sh
###------------------------------------------------------------------------------------
### 処理
###------------------------------------------------------------------------------------
```

# ◎ シェル関数
> シェル関数はうまく駆使すると、かっこいいです。
> 標準ライブラリとして、よく使う処理を関数化しておくと楽です。
> 関数内で定義している変数を設定ファイルにまとめると、かっこいいです。
```
**※ 注意点**
関数の中で更に関数を定義したり他の関数を呼び出すのは、出来るだけ避けましょう。
いわゆるスパゲッティコードになる恐れがあります。
また、return コマンドを使ったリターンコードの設定をしましょう。
そうする事で二次利用が可能になります。
関数内で使用する変数名はシェルスクリプト本体では使わないようにしましょう。
可読性を考慮しましょう。
```

## 書き方
```sh
FuncHOGE
{
    _HAGE="$1"
    ＜コマンド＞ ${_HAGE}
}
```

## 呼び出し方
```sh
FuncHOGE ${FUGA}
```

## 標準関数としてよく使う処理

### リターンコード判定処理
```sh
Func_CheckArgs
{
    _ERR_CODE="1"
    _OK_CODE="0"
    _VALID_ARGS="OK_CODE"
    _NUM_ARGS="${1}"
    if [ "${_NUM_ARGS}" -ne "${_VALID_ARGS}" ]; then
        ＜ログ出力処理＞;
        _CODE=${_ERR_CODE};
    fi
    return ${_CODE};
}
```

### ログ出力処理
```sh
Func_LogWrite
{
    typeset _PGM_NAME=${1}
    typeset _IN_MSG=${2}
    typeset _RC
    _DATE=$(date "+%Y%m%d %H:%M:%S")
    _ERR_CODE="1"
    _OK_CODE="0"
    echo "${_IN_MSG}" | while read _MSG_STRING
    do
        if [ ${_VERBOSE} != "TRUE" ]; then
            echo "${_DATE} [${_VERBOSE}]" "${_MSG_STRING}" \
                 >> ${_LOG_FILE}
        else
            echo "${_DATE} [${_VERBOSE}]" "${_MSG_STRING}" 2>&1 | \
                tee -a ${_LOG_FILE}
        fi
    done
    RC="${?}"
    if [ "${_RC}" -ne "0" ]; then
        echo "funcLogWrite failed. [${_LINENO}](${_RC})"
        CODE=${_ERR_CODE};
    fi
    return ${_CODE}
}
```

### ログ出力（その２）
```sh
Func_FLG()
{
    _MSG1="${1}"
    _MSG2="${2}"
    _MSG3="${3}"
    _C=$(echo -n ${_MSG1} | wc -c)
    if [ "${_C}" -le "13" ]; then
        TAB="\t\t"
    else
        TAB="\t"
    fi
    if [ "${_FLG}" -eq "0" ]; then
        printf " [OK]\t: ${_MSG1}${_TAB}: ${_MSG2}\t: ${_MSG3}\n";
    else
        printf "*[NG]\t: ${_MSG1}${_TAB}: ${_MSG2}\t: ${_MSG3}\n";
    fi
}
```

### 引数確認処理
```sh
Func_CheckArg()
{
    _ERR_CODE="1"
    _OK_CODE="0"
    if [ $# -eq '0' ]; then
      printf "Usage: $0 [option]\n";
        _CODE=${_ERR_CODE};
    fi
    return ${_CODE};
}
```

### 実行ユーザ確認処理
```sh
Func_CheckUser
{
    _ERR_CODE="1"
    _OK_CODE="0"
    _EXEC_USER="testuser"
    _USER="${1}"
    if [ "${_EXEC_USER}" != "${_USER}" ]; then
        _CODE=${_ERR_CODE};
    fi
    return ${_CODE};
}
```

### 二重起動確認処理
```bash
Func_Check_MultiExec()
{
    _SCRIPT=$(cat /proc/$$/cmdline | xargs --null)
    _RC='0'
    if [[ $$ -ne "$(pgrep -fo "${_SCRIPT}")" ]]; then
        _RC='1'
    fi
    echo "${_RC}";
    return "${_RC}";
}
```

### リトライ処理
```sh
```

## かっこいい関数の利用法
> 関数は return コマンドでリターンコードを設定して、変数に格納をして後続の処理に渡すという形にすると、よりかっこいいです。

```sh
# 関数は return コマンドでリターンコードを設定しておく
# 設定したリターンコードを ${TEST} に格納をして後続の処理に渡す
TEST=$(FuncTEST)
```

---
# ◎ 配列
ksh、bash や zsh の場合は、配列を使用可能です。
素の sh では使用できません。
※ ksh の場合は `set -A` を使用してください。
※ 最近は通常 sh というと ksh で Linux の場合は bash なので余り意識しなくても大丈夫です。

## 配列への格納方法
```sh
ARRAY[0]=test0
ARRAY[1]=test1
ARRAY[2]=test2
ARRAY[3]=test3
ARRAY[4]=test4
ARRAY[5]=test5
or
ARRAY=(test0 test1 test2 test3 test4 test5)
```
```sh
set -A ARRAY test0 test1 test2 test3 test4 test5
```

## 配列参照方法
```sh
#-- ARRAY[5]を参照
${ARRAY[5]}
```
```sh
#-- ARRAYの配列値を全部参照 
{$ARRAY[@]}
```

---
# ◎ 引数の設定
> 必ず想定する引数の数をチェックします。
> 引数 0 だったり、想定する数よりも多い場合はメッセージ表示の上で異常終了する、等の処理をします。
> オプションは、以下のルールで定義します。

1. 引数を与える場合は **「-」＋ 「文字」** とします
2. 引数を与えない場合は **「--」＋「文字列」** とします

```shell-session
test.sh -p /you/dir/path --disp
```
上記、例の場合は「-p /you/dir/path」が 1 で、--disp が 2 です。

> オプション設定をする場合、getopt, getopts, 自力で解析、の 3 つの方式があります。
> それぞれの使い方やメリットデメリットについては [こちら](http://qiita.com/b4b4r07/items/dcd6be0bb9c9185475bb) を参照してください。
> 汎用性を考慮し、ここでは「自力で解析」を選択します。
> 以下のコードであれば概ねどの環境でも動作します。
> ※ どの環境：BSD 系 Unix/ Solaris/ RH 系 Linux/ Debian 系 Linux

### コマンドライン引数の数をチェック
```sh
if [ $# -eq '0' ]
then
    printf "Usage: $0 [option]\n";
    exit 1;
fi
```

### オプションを設定
```sh
for OPT in "$@"
do
    case "${OPT}" in
        '-bp'|'-BP' )
            if [[ -z "$2" ]] || [[ "$2" =~ ^-+ ]]
            then
                printf "${MSG_USG}\n";
                exit 1;
            fi
            OPT_1=$2
            ;;
        '-lp'|'-LP' )
            if [[ -z "$2" ]] || [[ "$2" =~ ^-+ ]]
            then
                printf "${MSG_USG}\n";
                exit 1;
            fi
            OPT_2=$2
            ;;
        '--disp' )
            if [[ -z "$1" ]] || [[ "$1" =~ ^+ ]]
            then
                printf "${MSG_USG}\n";
                exit 1;
            fi
            MODE='0'
            ;;
        -* | --* ) printf "${MSG_USG}\n"; exit 1; ;;
    esac
    shift
done
```


---
# ◎ 処理の流れ
> 基本的に以下の形に沿ってコーディングします。
> これで文句を言われる事は殆どないと思います。
> （現場やシェルスクリプトの性質によっては、あまり別ファイルを作らないでほしい場合もありますが）

## 「#!/bin/sh」を宣言
> 場合によっては「#!/bin/ksh」「#!/bin/bash」だったりしますが、とにかく宣言をします。
> ちなみに「#!/you/dir/php」「#!/you/dir/perl」として、php や perl も動かせます。

## 変数の宣言
> 別ファイルにしておくと、設定ファイルとして使えます。
> 設定ファイルの変更であれば、本番環境への適用もハードルが下がるので便利です。
> ファイル名は「.conf」「.env」が分かりやすいです。

- /you/dir/path/hoge.conf
- /you/dir/path/hoge.env

> 変数を最初にまとめるか、処理の直前に記述するか、については、好みです。
> 「恒久的に変更がない変数はまとめて、コロコロ変わる場合は処理の直前で記述する」というのが、通常です。

## シェル関数の定義
> シェル関数は出来るだけ別ファイルにしてプログラムからは、読み込みをする形にします。

```shell-session
. /you/dir/path/hoge.func
```

## 事前確認
> 以下の項目をチェックします。
> 各項目のチェックで NG の場合は「exit 9」で異常終了させます。

- 実行ユーザ確認
- 引数確認
- 二重起動確認
- ディレクトリ確認
- その他プログラム固有の確認

## 開始宣言
> 主にデバッグ用ですが、出来るだけ処理開始の宣言をログへ出力する様にします。

## 主処理
> メイン処理です。
> 出来るだけ、リトライ処理を入れます。
> また、必ず実行判定をする為にリターンコードを取得します。（RC="$?"）

## 終了処理
> 主処理で取得したリターンコードを使って、正常異常の判定を行います。
> 異常の場合はリターンコードの番号で規定したメッセージを出力する様にします。
> 正常の場合でも異常の場合でも最終的にはリターンコードを終了コードに設定して終了させます。
> 
> 通常リターンコード（戻り値）は、正常終了（0）、異常終了（0 以外）で 1 を設定する事が多いです。
> が、要件によっては 9 をで設定する場合があります。
> 要件に従って異常値を設定してください。
> 異常値の認識が誤ると、監視設定で引っかからなかったり、色々重要な落とし穴に引っかかるケースがありますのでご注意ください。
> 
> よくあるのが、正常終了（0）、異常終了（1：処理に失敗）、異常終了（9：処理前チェックで NG）という感じです。
> この辺は現場やプロジェクトによりけりです。


-