はじめに
シェルスクリプトを学習していると、次のようなコードを見かけます。
#!/usr/bin/env bash
set -euo pipefail
while [[ $# -gt 0 ]]; do
case "$1" in
--help)
echo "ヘルプを表示します"
shift
;;
*)
echo "不明な引数: $1"
exit 1
;;
esac
done
初めて見ると、$#、$1、shift、esac など、何をしているのか分からない構文が多いと思います。
しかし、一つずつ理解していけば、それほど複雑な処理ではありません。
この記事では、シェルスクリプトを読む・書くために最初に覚えたいBashの基本構文を、サンプルコードと実行結果を交えて解説します。
※この記事はBashを対象としています。POSIX shや他のシェルでは動作が異なる場合があります。
1. シェルスクリプトとは
シェルスクリプトとは、シェルで実行するコマンドをファイルにまとめたものです。
例えば、Linuxで次のコマンドを実行するとします。
pwd
ls
echo "処理が完了しました"
これらを毎回手動で入力する代わりに、sample.sh というファイルにまとめます。
#!/usr/bin/env bash
pwd
ls
echo "処理が完了しました"
次のコマンドで実行できます。
bash sample.sh
これにより、複数のコマンドをまとめて実行できます。
シェルスクリプトは、ファイル操作、バックアップ、アプリケーションの起動、AWSなどのクラウド環境の操作を自動化するときにも使用されます。
2. #!/usr/bin/env bash とは
シェルスクリプトの先頭によく書かれているのが、次の記述です。
#!/usr/bin/env bash
これは Shebang(シバン) と呼ばれます。
スクリプトを直接実行するときに、どのインタープリターを使用するか指定するものです。
/usr/bin/env bash は、env を通じて PATH から bash を探して起動します。
例えば、次のスクリプトを作成します。
#!/usr/bin/env bash
echo "Hello Bash"
実行権限を付与します。
chmod +x sample.sh
直接実行します。
./sample.sh
実行結果:
Hello Bash
なお、bash sample.sh と実行した場合は、明示的にBashを指定しているため、Shebangはインタープリターの選択には使われません。
3. 変数
Bashでは、変数を使用して値を保持できます。
NAME="Taro"
echo "$NAME"
実行結果:
Taro
変数を定義するときは、= の前後にスペースを入れないようにします。
# 正しい
NAME="Taro"
# 間違い
NAME = "Taro"
変数を参照するときは $ を使用します。
また、次のように波括弧を使用することもできます。
NAME="Taro"
echo "${NAME}"
実行結果:
Taro
${NAME} は変数名の範囲を明確にしたい場合にも便利です。
NAME="Taro"
echo "${NAME}_backup"
実行結果:
Taro_backup
変数を参照するときは、意図しない単語分割やファイル名展開を避けるため、基本的に "$NAME" のようにダブルクォートで囲むことをおすすめします。
4. $1 / $2:位置パラメータ
Bashでは、スクリプトに渡された引数を $1、$2、$3 などで取得できます。
例えば、次のスクリプトを作成します。
#!/usr/bin/env bash
echo "1番目: $1"
echo "2番目: $2"
echo "3番目: $3"
実行:
bash sample.sh apple banana orange
実行結果:
1番目: apple
2番目: banana
3番目: orange
それぞれの対応は次のとおりです。
| 記述 | 取得する値 |
|---|---|
$1 |
apple |
$2 |
banana |
$3 |
orange |
これらを位置パラメータと呼びます。
なお、$0 は通常、スクリプトの起動に使用された名前やパスを表します。
10番目以降の引数を取得する場合は、${10}、${11} のように記述します。
5. $#:引数の個数
$# は、渡された引数の個数を取得します。
#!/usr/bin/env bash
echo "引数の個数: $#"
実行:
bash sample.sh apple banana orange
実行結果:
引数の個数: 3
引数が3つあるため、3 が表示されます。
引数が存在するかどうかの判定にも使用できます。
if [[ $# -eq 0 ]]; then
echo "引数がありません"
fi
ここで使用している -eq は、数値が等しいかを比較する演算子です。
6. $@:すべての引数
$@ は、渡されたすべての位置パラメータを表します。
#!/usr/bin/env bash
echo "$@"
実行:
bash sample.sh apple banana orange
実行結果:
apple banana orange
特に重要なのが "$@" という書き方です。
ダブルクォートで囲まれた "$@" は、各引数を独立した要素として保持します。
例えば、次のコードを見てみましょう。
#!/usr/bin/env bash
for arg in "$@"; do
echo "引数: $arg"
done
実行:
bash sample.sh apple "red banana" orange
実行結果:
引数: apple
引数: red banana
引数: orange
"red banana" は、スペースを含んでいても1つの引数として扱われます。
引数を順番に処理する場合は、基本的に "$@" を使用すると安全です。
7. shift:引数を左にずらす
shift は、位置パラメータを左にずらすためのBashの組み込みコマンドです。
例えば、次のように引数を渡したとします。
bash sample.sh apple banana orange
最初の状態:
$1 = apple
$2 = banana
$3 = orange
$# = 3
ここで shift を実行すると、次のようになります。
$1 = banana
$2 = orange
$# = 2
最初の引数 apple が位置パラメータから取り除かれ、残りの引数が1つずつ左に移動します。
実際に確認してみましょう。
#!/usr/bin/env bash
echo "shift前: $1"
shift
echo "shift後: $1"
実行:
bash sample.sh apple banana orange
実行結果:
shift前: apple
shift後: banana
また、shift 2 と記述すると2つ分ずらせます。
shift は、複数の引数を順番に処理するときによく使用されます。
8. ${VAR:-default}:デフォルト値
Bashでは、変数が未設定、または空文字列の場合にデフォルト値を使用できます。
NAME="${1:-Guest}"
echo "$NAME"
引数なしで実行:
bash sample.sh
実行結果:
Guest
引数ありで実行:
bash sample.sh Taro
実行結果:
Taro
${1:-Guest} は、次のような意味です。
-
$1に空でない値がある場合は、その値を使用する -
$1が未設定または空文字列の場合は、Guestを使用する
なお、${VAR:-default} は変数そのものにデフォルト値を代入するわけではありません。
値を代入したい場合は、別の構文である ${VAR:=default} を使用できます。
また、${VAR-default} のようにコロンを省略すると、未設定の場合だけデフォルト値が使用され、空文字列はそのまま扱われます。
9. if / case:条件分岐
if文
if は、条件に応じて処理を分岐させるための構文です。
#!/usr/bin/env bash
AGE=20
if [[ $AGE -ge 18 ]]; then
echo "18歳以上です"
else
echo "18歳未満です"
fi
実行結果:
18歳以上です
ここで使用している -ge は、左側の数値が右側以上であるかを比較する演算子です。
よく使用する数値比較演算子は次のとおりです。
| 演算子 | 意味 |
|---|---|
-eq |
等しい |
-ne |
等しくない |
-gt |
より大きい |
-ge |
以上 |
-lt |
より小さい |
-le |
以下 |
case文
case は、値に応じて複数の処理を分岐させる構文です。
#!/usr/bin/env bash
case "${1:-}" in
start)
echo "開始します"
;;
stop)
echo "停止します"
;;
*)
echo "不明なコマンドです"
;;
esac
実行:
bash sample.sh start
実行結果:
開始します
case では、値がどのパターンに一致するかによって処理が決まります。
*) は、それまでのどのパターンにも一致しなかった場合に実行されます。
また、;; は、その分岐の処理を終了するための記述です。
最後の esac は、case 文の終わりを表します。
10. while:繰り返し処理
while は、条件が成立している間、処理を繰り返す構文です。
#!/usr/bin/env bash
COUNT=1
while [[ $COUNT -le 3 ]]; do
echo "$COUNT"
COUNT=$((COUNT + 1))
done
実行結果:
1
2
3
$((COUNT + 1)) は、Bashの算術展開です。
変数 COUNT に1を加算しています。
whileとshiftを組み合わせる
次のようなコードは、シェルスクリプトでよく使用されます。
#!/usr/bin/env bash
while [[ $# -gt 0 ]]; do
echo "現在の引数: $1"
shift
done
実行:
bash sample.sh apple banana orange
実行結果:
現在の引数: apple
現在の引数: banana
現在の引数: orange
while [[ $# -gt 0 ]] は、引数の個数が0より大きい間、処理を繰り返すという意味です。
shift によって引数を1つずつ取り除き、最後に引数が0個になるとループが終了します。
11. 関数
Bashでは、複数の処理を関数としてまとめることができます。
#!/usr/bin/env bash
greet() {
echo "Hello $1"
}
greet "Taro"
greet "Hanako"
実行結果:
Hello Taro
Hello Hanako
greet() が関数の定義です。
greet "Taro" のように記述すると、関数を呼び出せます。
関数の内部では、$1 は関数に渡された1番目の引数を表します。
なお、Bashの関数では return によって終了ステータスを返します。一般的な文字列などのデータを返したい場合は、echo などの標準出力を利用する方法があります。
12. set -euo pipefail:エラーを検出しやすくする
シェルスクリプトの先頭では、次の記述を見かけることがあります。
set -euo pipefail
これは、スクリプトのエラーを検出しやすくするための設定です。
それぞれ分解して説明します。
set -e
コマンドが失敗した場合に、一定の条件下でスクリプトを終了します。
#!/usr/bin/env bash
set -e
false
echo "この行は実行されません"
false は終了ステータス1を返すコマンドです。
この例では、false が失敗するため、後続の echo は実行されません。
ただし、if の条件判定や &&、|| など、一部の文脈では -e による終了が適用されません。
set -u
未定義の変数を参照した場合に、エラーとして扱います。
#!/usr/bin/env bash
set -u
echo "$NAME"
NAME が定義されていなければ、エラーになります。
未定義の変数にデフォルト値を設定したい場合は、${NAME:-Guest} のように記述できます。
set -o pipefail
パイプラインの途中でコマンドが失敗した場合にも、失敗を検出しやすくします。
#!/usr/bin/env bash
set -o pipefail
false | cat
echo "終了ステータス: $?"
実行結果:
終了ステータス: 1
通常、パイプラインの終了ステータスは最後のコマンドによって決まります。
pipefail を有効にすると、失敗したコマンドが存在する場合、最も右側にある失敗コマンドの終了ステータスがパイプライン全体の終了ステータスになります。
なお、set -euo pipefail を指定しただけですべてのエラーを防げるわけではありません。
想定される失敗については、if などを使用して明示的に処理することも重要です。
13. BASH_SOURCE[0] と dirname
シェルスクリプトでは、スクリプト自身が配置されているディレクトリを取得したい場合があります。
例えば、次のようなディレクトリ構成を考えます。
project/
├── scripts/
│ └── sample.sh
└── config/
└── app.conf
sample.sh から config/app.conf を参照したい場合、スクリプトの場所を基準にパスを作ると便利です。
BASH_SOURCE[0]
BASH_SOURCE は、Bashが提供する配列変数です。
通常、スクリプトのトップレベルでは ${BASH_SOURCE[0]} に、そのスクリプトのパスが格納されています。
echo "${BASH_SOURCE[0]}"
例えば、次のように表示されます。
./scripts/sample.sh
※実際の表示は起動方法によって異なります。
dirname
dirname は、指定されたパスのディレクトリ部分を取得するコマンドです。
dirname "/home/user/project/scripts/sample.sh"
実行結果:
/home/user/project/scripts
組み合わせて使用する
次のコードを見てみましょう。
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "$SCRIPT_DIR"
この処理は、次のような流れです。
-
${BASH_SOURCE[0]}でスクリプトのパスを取得する -
dirnameでディレクトリ部分を取得する -
cdでそのディレクトリに移動する -
pwdで絶対パスを取得する -
$(...)でコマンドの出力を取得し、SCRIPT_DIRに代入する
例えば、次のような結果になります。
/home/user/project/scripts
さらに、プロジェクトのルートディレクトリを取得したい場合は、次のように記述できます。
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
.. は親ディレクトリを表します。
これにより、スクリプトをどの作業ディレクトリから実行しても、スクリプトの配置場所を基準にファイルを参照しやすくなります。
ただし、この方法はシンボリックリンクの実体パスを自動的に解決するものではありません。
14. bash -n:構文チェック
Bashでは、スクリプトを実行せずに構文を確認できます。
bash -n sample.sh
-n は、コマンドを実行せずに構文を読み込んでチェックするオプションです。
例えば、次のようなスクリプトがあったとします。
#!/usr/bin/env bash
if [[ 1 -eq 1 ]]; then
echo "Hello"
このコードでは fi が不足しています。
bash -n sample.sh を実行すると、構文エラーが検出されます。
なお、bash -n は構文の確認が目的であり、変数の値や実行時のエラー、処理の正しさまで保証するものではありません。
15. shfmt:コードのフォーマット
shfmt は、シェルスクリプトのコードを整形するためのツールです。
例えば、次のようなコードがあるとします。
if [[ $# -gt 0 ]];then
echo "$1"
fi
shfmt を使用すると、インデントや空白などを統一できます。
shfmt -w sample.sh
整形後:
if [[ $# -gt 0 ]]; then
echo "$1"
fi
よく使用するオプションは次のとおりです。
| オプション | 意味 |
|---|---|
-w |
ファイルを直接書き換える |
-d |
整形前後の差分を表示する |
-l |
整形が必要なファイルを表示する |
-i 2 |
インデントをスペース2つに設定する |
例えば、次のコマンドでスペース2つのインデントに統一できます。
shfmt -i 2 -w sample.sh
※shfmt はBashの標準コマンドではないため、別途インストールが必要です。
また、shfmt は主にコード整形を行うツールです。潜在的なバグや危険な書き方の検出には、ShellCheck という静的解析ツールも役立ちます。
16. まとめ
今回は、シェルスクリプト初心者が最初に覚えたいBashの基本構文を紹介しました。
| 構文・コマンド | 役割 |
|---|---|
#!/usr/bin/env bash |
Bashで実行するためのShebang |
NAME="value" |
変数を定義する |
$1 / $2
|
引数を個別に取得する |
$# |
引数の個数を取得する |
"$@" |
すべての引数を個別に保持する |
shift |
位置パラメータを左にずらす |
${VAR:-default} |
未設定または空文字列の場合のデフォルト値 |
if |
条件分岐 |
case |
パターンによる条件分岐 |
while |
条件が成立する間繰り返す |
function_name() |
関数を定義する |
set -euo pipefail |
エラーを検出しやすくする |
BASH_SOURCE[0] |
スクリプトのパスを取得する |
dirname |
パスのディレクトリ部分を取得する |
bash -n |
構文をチェックする |
shfmt |
コードを整形する |
最初からすべての構文を暗記する必要はありません。
実際にシェルスクリプトを書いたり、既存のコードを読んだりしながら、少しずつ覚えていくのがおすすめです。
特に、while、case、shift の組み合わせや、set -euo pipefail、BASH_SOURCE[0] などは、運用・自動化スクリプトを読む際にも役立ちます。
まずは小さなスクリプトを作成して、各構文がどのように動作するのか確認してみてください。