ネット上に散在している Bash スクリプトのベストプラクティスについて、僕なりにまとめたメモです。
VS Code での開発を前提に記載しています。
VS Code Extensions
VS Code を利用するのであれば、以下の Extensions を利用できます。
- language server
- linter
- formatter
インデント
「タブ派」と「スペース 2 個派」に分かれるようですが、ヒアドキュメントを書く際にはタブが必須となるため、タブを利用しようと思います。
シェルスクリプトのインデントにはスペースとタブのどちらがよいか?
settings.json
{ "[shellscript]": { "editor.tabSize": 4, "editor.insertSpaces": false, "editor.defaultFormatter": "foxundermoon.shell-format", "editor.formatOnSave": true } }
命名規則
- ファイル名
- kebab-case
- 定数
- UPPER_SNAKE_CASE
- readonly 宣言して使う
- 変数
- lower_snake_case
- 関数内では local 宣言して使う
- 関数
- lower_snake_case
シバン行
PATH を見て実行させます。
#!/usr/bin/env bash
#!/usr/bin/env
searchesPATH
forbash
, andbash
is not always in/bin
, particularly on non-Linux systems. For example, on my OpenBSD system, it's in/usr/local/bin
, since it was installed as an optional package.If you are absolutely sure
bash
is in/bin
and will always be, there's no harm in putting it directly in your shebang—but I'd recommend against it because scripts and programs all have lives beyond what we initially believe they will have.
Why is #!/usr/bin/env bash superior to #!/bin/bash?
set コマンドのオプション
set -Eeuo pipefail
option | description |
---|---|
-E | ERR trap を関数内にも継承させる |
-e | コマンドがゼロ以外で終了した場合、直ちに終了する |
-u | 未設定の変数をエラーとして扱う |
-o pipefail | パイプライン内のコマンドがゼロ以外で終了した場合、その失敗コマンドの終了ステータスがパイプライン全体の終了ステータスになる |
スクリプトファイルのフルパスを取得して、そこを基点に操作する
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) readonly SCRIPT_DIR echo "$SCRIPT_DIR" ls "$SCRIPT_DIR" cd "$SCRIPT_DIR/../" && pwd # output: # /full/path/to/script/directory # sample.sh # /full/path/to/script
usage 関数を作成する
#!/bin/bash set -Eeuo pipefail function usage() { cat <<-EOF Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-p param_value] arg1 [arg2] description. Options: -h, --help Print this help -v, --verbose Print script debug info -p, --param Some param description Arguments: arg1 aaaaa arg2 bbbbb EOF } function parse_args() { while :; do case ${1:-default} in -h | --help) usage && exit ;; -v | --verbose) set -x ;; -p | --param) local param="${2}" shift ;; -?*) printf "\033[31m%s\033[m\n\n" "ERROR: unknown option" >&2 usage && exit 1 ;; *) break ;; esac shift done if [[ $# -eq 0 ]]; then printf "\033[31m%s\033[m\n\n" "ERROR: missing arguments" >&2 usage && exit 1 fi readonly PARAM=${param:-} readonly ARG1=${1} readonly ARG2=${2:-} } parse_args "$@" echo "$ARG1 $ARG2 $PARAM"
実行例1。ヒントの表示。
$ ./sample.sh -h Usage: sample.sh [-h] [-v] [-p] arg1 arg2 description. Options: -h, --help Print this help -v, --verbose Print script debug info -p, --param Some param description Arguments: arg1 aaaaa arg2 bbbbb
実行例2。引数の不足。
$ ./sample.sh ERROR: missing arguments Usage: sample.sh [-h] [-v] [-p param_value] arg1 [arg2] description. Options: -h, --help Print this help -v, --verbose Print script debug info -p, --param Some param description Arguments: arg1 aaaaa arg2 bbbbb
実行例3。デバッグモードでの出力。
$ ./sample.sh -v --param '!!' Hello World + shift + : + case ${1:-default} in + local 'param=!!' + shift + shift + : + case ${1:-default} in + break + [[ 2 -eq 0 ]] + readonly 'PARAM=!!' + PARAM='!!' + readonly ARG1=Hello + ARG1=Hello + readonly ARG2=World + ARG2=World + echo 'Hello World !!' Hello World !!
trap コマンドを活用する
以下は trap コマンドを利用して、必ず finally の終了処理を呼ぶ例です。
#!/usr/bin/env bash set -Eeuox pipefail trap finally SIGINT SIGTERM ERR EXIT function finally() { trap - SIGINT SIGTERM ERR EXIT # trap の設定をリセットする. リセットしないと、非 EXIT シグナルが呼ばれた時に、この関数が 2 回実行されてしまう echo "finally done" } function force_error() { not_found_command } force_error
実行例。
$ ./sample.sh + trap finally SIGINT SIGTERM ERR EXIT + force_error + not_found_command ./sample.sh: line 11: not_found_command: command not found ++ finally ++ trap - SIGINT SIGTERM ERR EXIT ++ echo 'finally done' finally done $ $ echo $? 127
スクリプトの終了コードは、trap される前の値を利用してくれる、という話。
main 関数を作成する
function main() { parse_args "$@" echo "$ARG1 $ARG2 $PARAM" } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi
if 文の部分は、source コマンドで実行された時には実行させないための工夫。
sample.sh
#!/bin/bash echo "1: ${BASH_SOURCE[0]}" echo "2: ${0}"
実行結果。
$ ./sample.sh
1: ./sample.sh
2: ./sample.sh
$
$ source ./sample.sh
1: ./sample.sh
2: /bin/bash
まとめ
gist7481b760492267c449c25277fda9045c