いろいろな考え方があり、それぞれのプロジェクトで最適解が異なります。
規約のたたき台として、まずは作成しました。
参考の一助になれば幸いです。
ここでは、自分が使いやすい規約にしてみました。
臨機応変に変更してください。
Google Shell guide といったものも存在します。
https://google.github.io/styleguide/shellguide.html
プロジェクトにおけるシェルコマンド言語を利用したスクリプトは、視認性やメンテナンス性の向上を目的として、次の規約に則り作成すること。
UNIX として共通の仕様である POSIX に規定された範囲でのコーディングとし、独自拡張は極力利用しないこと。
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
また、必要があって利用する場合は、ヘッダに注意事項として記載すること。
[Tips]
たとえば、Linux の for 文で C 言語のような ( i=0 ; i < x ; i++ ) という形式を利用できます。これは便利なのですが bash の独自実装なので汎用性が下がります。
スクリプトは、次の要件を満たすこと。
ファイル名は、英数字 + 拡張子とする。
拡張子は、シェルスクリプト単体で実行するものを .sh とする。
また、変数や関数を別途まとめたファイルについては .src とする。
[Tips]
csh / tcsh / fish / bash / dash といった、シェルを複数利用するようなプロジェクトでは、拡張子をそれぞれに合わせた方が良いと思います。
bash しか利用しないことが分かっている環境で、シバンにも bash 指定のみの場合は .sh でも不都合はないと思います。
.src は、内容として bash なのですが source コマンド (ビルトインでは . コマンド) で呼ぶことを意図しています。
実行権限は、システムとして特に指定のない限り 610 とする。
[Tips]
権限を 755 や 700 で運用することは多いのですが、セキュリティを考慮すると、権限は最小限にすべきでしょう。
ここでは本番環境を想定していて、作成するユーザと、実行するユーザが異なることを考慮しています。
所有者は、更新できるが実行できない。前提条件を満たさない、不用意な実行を阻止できる。
グループ(実行者)は、更新できないが実行できる。JP1 のような自動実行ユーザのスクリプト改変を阻止できる。
スクリプトは、次の構成を基本とする。
段落 | 内容 | 備考 |
ヘッダ | シバンやスクリプトの目的などを記載する。 | |
変数宣言 | スクリプトが利用する変数を宣言もしくは指定する。 | source (.) での指定も可 |
関数宣言 | スクリプトが利用する関数を宣言する。 | source (.) での指定も可 |
メイン処理 | スクリプトが目的とする処理を行う。 | |
終了処理 | 一時利用のために展開していたテンポラリファイルの削除などを行う。 | |
文字コードは UTF-8 とする。
また、BOM は付与しないこと。
日本語はコメントに限定する。
[Tips]
コメントに日本語を使わない場合は ASCII を指定してもよいです。
むしろ ASCII として認識されるでしょう。
改行コードは LF とする。
[Tips]
Windows でスクリプトを書くと、改行コードは考慮から漏れがちです。
Linux や UNIX 上で書くと気にしなくても良いのですが、プロジェクトの進行状況や管理方法次第で、どうしても Windows 端末での開発を行わなければならない状況もあります。
致命的ではない文字コードやインデントも指定するのですから、致命的エラーとなる改行コードは指定しておきましょう。
インデントは、半角スペース2つとする。
[Tips]
タブ (半角スペース4つもしくは8つ) だと、横に広がりすぎて視認性が悪くなることがあります。
シバンは、以下の通り bash とする。
#!/bin/bash
[Tips]
シバンとヘッダは、ひとまとめにしてしまっても良いかもしれません。
もしくは、拡張子の指定でシバンも固定することになるでしょう。
スクリプトの冒頭には、スクリプトの概要を記載する。
項目 | 内容 | 備考 |
スクリプト名 | Script name | ファイル名称を記載する。 | |
変更 | Modify | 変更日・変更者・変更内容を記録する。 | |
以下は、定義する際のサンプル。
項目 | 内容 | 備考 |
スクリプト名 | Script name | ファイル名称を記載する。 | |
目的 | Purpose | スクリプトの目的を記載する。 | |
引数 | Option | 指定可能なオプションを記載する。 | |
変更 | Modify | 作成日を記録する。 | 日付と担当者をひとまとめにしてもよい。 |
作成者 | Auther | 作成者を記載する。 | |
更新日 | Update | 更新日を記載する。 | 更新内容も併記するとなおよい。 |
権限 | Permission | 実行者や権限を記載する。 | |
サンプル 1
# Script name: example.sh
サンプル 2
# スクリプト名: example.sh
# 目的: スクリプトサンプル
適宜コメントを記載すること。
コメント内は日本語で記述する。
サンプル
# システム
変数名は、別途定めるプレフィックス/サフィックスを含め、キャメルケースで指定する。
一部の予約された変数名以外は、単語の組み合わせで指定すること。
[Tips]
私がスネークケースよりキャメルケースのほうが読みやすいと感じるタイプなので、キャメルケースを指定しています。
メリット・デメリットを考慮して、規定するのが良いです。
変数名はスクリプト上部にまとめて宣言しておくので、エディタの画面分割機能でコピペすることが前提としてあります。
画面分割は vim でも可能です。
変数名が短くても、何回も手入力していればタイポする可能性は高いです。
プレフィックスは、変数の用途に応じて次の文字を使用する。
文字 | 用途 | 備考 |
p | 汎用パラメータ | Parameter を意図 |
n | 数値用パラメータ | Number を意図 |
f | 0 を真としたフラグ情報 | Flag を意図 |
t | ループで頻繁に書き換える使い捨てパラメータ | Temporary を意図 |
i | ループ用に予約 | Integer を意図 (while/until での数値ループに使用) |
s | ループ用に予約 | String を意図(for での文字列ループに使用) |
サフィックスは、特定の用途で次の文字列を使用する。
文字 | 用途 | 備考 |
Path | 絶対パスでファイル情報を指定する場合に利用 | |
Dir | ディレクトリ情報の場合に利用 | 文字列の最後は / で終わること。 |
Name | ファイル情報の場合に利用 | ディレクトリは含めないこと。 |
Enable | フラグ管理など、0 で有効とする場合に利用 | |
サフィックスとしての Disable は、Enable と併用すると混乱を招くことがあるので禁止する。
変数を利用する場合は、ブラケットで括ること。
終了ステータスは、スクリプトの目的が満たされることを正常とし、正常終了した場合に 0 とする。
関数は、
関数名は、別途定めるプレフィックスを含め、キャメルケースで指定する。
一部の予約された関数名以外は、単語の組み合わせで指定すること。
プレフィックスは、変数の用途に応じて次の文字を使用する。
文字 | 用途 | 備考 |
fn | 関数 (ファンクション) であることを示す | |
特定のログファイルに、毎回リダイレクトを記述するのは可視性としても悪くなり、ミスも誘発しやすい。
引数としてメッセージをログファイルへ書き込む関数を用意した。
スクリプトの進捗状況を記述する目的で利用すること。
サンプル
fnLog "Sample message"
開発時に利用したデバッグ用メッセージを、メンテナンス時にも再利用できるよう、フラグに応じて出力指定もできる関数を用意した。
デバッグ情報を記述する目的で利用すること。
サンプル
fnDebug "Debug message"
シェルスクリプトが標準エラー出力を行うための関数を用意した。
エラー出力を記述する目的で利用すること。
サンプル
fnErr "Standard Error message"
存在しなければ即時で終了するための関数を用意した。
引数として、エラー番号と、対象のファイルまたはディレクトリなどを指定すること。
サンプル
[ -d "/etc" ] || fnErrEnd 14 "/etc"
[ -f "/etc/hosts" ] || fnErrEnd 15 "/etc/hosts"
[ -x "/bin/ps" ] || fnErrEnd 13 "/bin/ps"
getopts を利用した解析を行う。
[Tips]
- help といったロングオプションを使いたい場合は、getopts ではなく、引数そのものを case で解析するほうがよいと思います。
サンプル
#!/bin/bash
############################################################
# Script name: example.sh
# Modify : yyyy.mm.dd 名前 初版作成
############################################################
### Parameter and Variable #################################
pCommon=./src/common.src
if [ -f "${pCommon}" ]; then
. ${pCommon}
else
echo "${pCommon} not exist."
exit 1
fi
### Function ###############################################
fnEnd() {
# スクリプト内で異常終了する際の Exit Status と、メッセージ内容の定義
nExitStatus=$1
case "${nExitStatus}" in
0 ) fnLog "Script ${0##*/} end.";;
* ) fnErr "Unknown error ($2)"; nExitStatus=1;;
esac
exit ${nExitStatus}
}
### Main ###################################################
fnLog "Script ${0##*/} start."
## オプション解析
while getopts h OPT; do
case ${OPT} in
h )
fnDebug "Option h used."
echo "Usage ${0##*/} [-h]"
echo " [-h] help messages."
fnEnd 0
;;
* )
fnDebug "Unknown option used."
fnErrEnd 10 "${OPT}"
;;
esac
done
# xxx 処理
### End ####################################################
fnEnd 0
############################################################
サンプル
# 共通ファイル (common.src)
### Parameter and Variable #################################
## ディレクトリ
pLogDir="/var/log/script/"
## ファイル
pLogName=$(basename ${0%.*}.log)
## パス
# ログファイル
pLogPath="${pLogDir}${pLogName}"
# ヌルクリア
i=""
s=""
# Exit Status
nExitStatus=0
## フラグ
fDebugEnable=0
### Function ###############################################
# 目的:ログ出力
fnLog() {
echo "$(date +'%Y/%m/%d %H:%M:%S') $*" >> ${pLogPath}
}
# 目的:デバッグ出力
fnLog() {
[ "${fDebugEnable}" -eq "0" ] && fnLog "[Debug] $*"
}
# 目的:エラー出力
fnErr() {
# ログ出力
fnLog "$*"
# 標準エラー出力
echo "$(date +'%Y/%m/%d %H:%M:%S') $*" >&2
}
# 目的: エラー終了
fnErrEnd() {
nExitStatus=${1}
# 引数確認
if [ -z "${nExitStatus}" ]; then
fnErr "Not argument."
exit 1
fi
case "${nExitStatus}" in
0 ) fnErr "Function (fnErrEnd) argument error."; nExitStatus=1;;
10 ) fnErr "Argument error ($2)";;
11 ) fnErr "Read permission not exist ($2)";;
12 ) fnErr "Write permission not exist ($2)";;
13 ) fnErr "Exec permission not exist ($2)";;
14 ) fnErr "Directory not exist ($2)";;
15 ) fnErr "File not exist ($2)";;
* ) fnErr "Unknown error ($2)";;
esac
exit ${nExitStatus}
}
||