シェルスクリプトしたい(bash)

$ bash スクリプトファイル.sh
$ コマンド実行 && echo "成功" || echo "失敗"

bashは、Bourne Shell(sh)を拡張したシェルです。 対話シェルとしても、シェルスクリプトを実行するインタプリタとしても使われます。

注釈

bashは、長らくLinuxやmacOSの標準シェルとして使われてきました。 しかし、Ubuntuでは2006年以降、シェルスクリプトが参照する/bin/shがbashではなくdashに変更されました。 2019年リリースのmacOS Catalina以降、新規ユーザーのデフォルトログインシェルがbashからzshに変更されました。

インストールしたい(bash)

$ brew install bash
$ bash --version

bashはHomebrewでインストールできます。

$ /bin/bash --version
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)
Copyright (C) 2007 Free Software Foundation, Inc.

macOSに標準でインストールされているbashは、バージョン3系で古いです。

Hello Worldしたい(echo)

1#!/usr/bin/env bash
2
3echo "Hello World"

シェバングしたい(#!/usr/bin/env bash)

1#!/usr/bin/env bash

#!ではじまる行をシェバング(shebang)と呼びます。 シェルスクリプトを実行する際には、どのシェルで実行するかを指定する必要があります。 /usr/bin/envで環境変数PATHからbashを探して実行するように指定するのが一般的です。

1#!/bin/bash

#!/bin/bashのように、シェルの絶対パスを指定することもできます。 しかし、この方法では、実行環境によってはエラーになる場合があります。 #!/usr/bin/env bashの方が、より移植性の高い書き方です。

おまじないしたい(set -euo pipefail)

1#!/usr/bin/env bash
2
3set -euo pipefail

set -euo pipefailは、シェルスクリプトの実行時にエラーが発生した場合に、即時にスクリプトを終了させるための設定です。

-eは、コマンドが失敗した場合にスクリプトを終了させます。 -uは、未定義の変数を参照した場合にスクリプトを終了させます。 -o pipefailは、パイプライン内のいずれかのコマンドが失敗した場合にスクリプトを終了させます。

「おまじない」と思って、シェルスクリプトの冒頭に書いておくとよいです。

テストしたい([ ... ] / test)

1if test 条件式; then
2    条件式が True の場合の処理
3fi

testは、条件式を評価するためのコマンドです。 if文と組み合わせて、条件分岐を作成できます。

1if [ 条件式 ]; then
2    条件式が True の場合の処理
3fi

[ ... ]は、testの別名でより一般的に使われます。

1if [ ! 条件式 ]; then
2    条件式が False の場合の処理
3fi

!を使うことで、条件式の否定を評価できます。

ファイルを確認したい(test -f / test -d)

1if [ ! -f ファイルパス ]; then
2    echo "Path is not a regular file"
3    exit 1
4fi

test -fで、指定したパスが通常のファイルであるかを確認できます。

1if [ ! -d ディレクトリパス ]; then
2    echo "Path is not a directory"
3    exit 1
4fi

test -dで、指定したパスがディレクトリであるかを確認できます。

1if [ ! -e パス ]; then
2    echo "File or directory does not exist"
3    exit 1
4fi

test -eで、指定したパスが存在するかを確認できます。

コマンドの存在を確認したい(command -v)

1if ! command -v コマンド名 >/dev/null 2>&1; then
2    echo "Error: コマンド名 is missing"
3    exit 1
4fi

command -v コマンド名を使ってコマンドの存在を確認できます。 上記のスクリプトは、コマンドが存在しなかった場合、エラーメッセージを表示して終了させています。

引数を確認したい($#)

1if [ $# -lt 2 ]; then
2    echo "引数の数が不足しています"
3    exit 1
4fi

$#で引数の数を取得できます。 テスト文と組み合わせて、引数のバリデーションに使えます。

オプション解析したい(getopts)

 1while getopts "n:h" opt; do
 2    case "${opt}" in
 3        n)
 4            arg1=${OPTARG}
 5            ;;
 6        h)
 7            show_help
 8            ;;
 9        *)
10            echo "Invalid option: ${opt}" >&2
11            exit 1
12            ;;
13    esac
14done

-hのように、フラグとしてのオプション変数は"h"、 -n 行数のように、引数が必要な場合は"n:"と書きます。

ディスク容量を監視したい(if)

1#!/usr/bin/env bash
2
3usage=$(df / | awk 'NR==2 {gsub("%","",$5); print $5}')
4
5if [ "${usage}" -ge 90 ]; then
6    echo "Warning: disk usage is ${usage}%"
7    exit 1
8fi

ifで条件分岐できます。 dfでルートパーティションの使用率を取得し、90%以上ならエラーメッセージを出して終了します。 cronで定期実行すれば、簡易的なディスク容量監視スクリプトになります。

1#!/usr/bin/env bash
2
3if [ -f ".env" ]; then
4    source .env
5else
6    echo "Error: .env file not found" >&2
7    exit 1
8fi

elseを使うと、条件に一致しなかった場合の処理も書けます。 .envファイルがあれば読み込み、なければエラーで終了する、よくある設定読み込みパターンです。

 1#!/usr/bin/env bash
 2
 3branch=$(git branch --show-current)
 4
 5if [ "${branch}" = "main" ]; then
 6    echo "Error: cannot push directly to main" >&2
 7    exit 1
 8elif [ "${branch}" = "" ]; then
 9    echo "Error: not on any branch (detached HEAD?)" >&2
10    exit 1
11else
12    git push origin "${branch}"
13fi

elifで条件を追加できます。 現在のブランチがmainなら拒否し、ブランチ名が取得できなければ(detached HEADなど)エラーにし、それ以外ならpushする、という3分岐のガードスクリプトです。

サブコマンドを実装したい(case)

 1#!/usr/bin/env bash
 2
 3subcommand="${1:-help}"
 4
 5case "${subcommand}" in
 6    start)
 7        echo "starting..."
 8        ;;
 9    stop)
10        echo "stopping..."
11        ;;
12    status)
13        echo "checking status..."
14        ;;
15    help|*)
16        echo "usage: $0 {start|stop|status}"
17        ;;
18esac

caseで、変数の値がどのパターンに一致するかで処理を分岐できます。 start・stop・statusのようなサブコマンドを受け取って処理を切り替える自作CLIによくある構造です。 help|*のように|で複数パターンをまとめられ、*は「どれにも一致しなかった場合」を拾う受け皿になります。

複数ファイルをまとめて処理したい(for)

1#!/usr/bin/env bash
2
3for file in *.jpg; do
4    convert "${file}" -resize 50% "resized_${file}"
5done

forで、値のリストを順に変数へ入れながら処理を繰り返せます。 カレントディレクトリの.jpgファイルすべてをリサイズして、別名で保存する例です。 グロブ(*.jpg)はそのままinの後に書くだけで展開されます。

1#!/usr/bin/env bash
2
3for host in web01 web02 web03; do
4    echo "=== ${host} ==="
5    ssh "${host}" uptime
6done

サーバーのリストを直接書いて、sshで同じコマンドを順番に実行する例です。 台数が多い場合はhosts.txtからwhile readで読み込む方法(下記)に切り替えるとよいです。

サーバーが起動するまで待ちたい(while / until)

1#!/usr/bin/env bash
2
3until curl -sf http://localhost:8080/health >/dev/null; do
4    echo "waiting for server..."
5    sleep 2
6done
7
8echo "server is up"

untilは条件式が真になるまで処理を繰り返します。 ヘルスチェック用エンドポイントが応答するまで2秒おきにリトライし、起動を待つデプロイスクリプトなどでよく使うパターンです。

1#!/usr/bin/env bash
2
3while read -r line; do
4    echo "processing: ${line}"
5done < hosts.txt

whileは条件式が真の間、処理を繰り返します。 hosts.txtを1行ずつ読み込んで処理する、ファイル入力の定番パターンです。 for line in $(cat hosts.txt)と書くと、空白を含む行やスペース区切りで意図せず分割されてしまうため、while readのほうが安全です。

メニューから選ばせたい(select)

 1#!/usr/bin/env bash
 2
 3PS3="deploy先を選んでください: "
 4select env in staging production quit; do
 5    case "${env}" in
 6        staging)
 7            echo "deploying to staging..."
 8            break
 9            ;;
10        production)
11            echo "deploying to production..."
12            break
13            ;;
14        quit)
15            exit 0
16            ;;
17        *)
18            echo "invalid option"
19            ;;
20    esac
21done

selectで、値のリストを番号付きで表示し、ユーザーに選択させられます。 PS3でプロンプト文字列を指定でき、caseと組み合わせて選択結果ごとに処理を分けるのが基本形です。 対話的にデプロイ先を選ばせたいスクリプトなどに向いています。

関数を定義したい

1function 関数名
2{
3    実行内容
4}
1関数名()
2{
3    実行内容
4}

どちらの書き方でも関数を定義できます。 functionキーワードはbash独自の拡張で、後者の書き方はPOSIX準拠のシェルでも使えます。