cv.cm

コーディング · プログラミング言語

Bashスクリプト入門:初心者向けチュートリアル

約11分で読めます

結論

Bashは、Linuxサーバー、macOSのターミナル、CIパイプラインで使われる、Unixのコマンドラインシェル兼スクリプト言語です。Bashスクリプトは、シェルコマンドを書いたテキストファイルで、変数、条件分岐、ループ、関数、パイプと組み合わせて使います。このページでは、スクリプトの書き方と実行方法、変数の安全なクォート、制御構文、配列、出力のリダイレクト、set -euo pipefailによるエラー処理を紹介します。

chmod 計算を開く

Bashとは何か、何に使われるのか

Bash(Bourne Again SHell)は、GNUプロジェクトのコマンドラインシェル兼スクリプト言語です。入力したコマンドを読み取ってプログラムを実行し、パイプやリダイレクトでそれらを接続します。Bashスクリプトはそれと同じコマンドを書いたテキストファイルなので、ターミナルで手作業で行うことは何でも自動化できます。

Bashは、ほとんどのLinuxディストリビューションで標準の対話型シェルです。macOSでは標準シェルがzshになりましたが、Bashも引き続き同梱されています。Windowsでは、WSLやGit Bashで使えます。開発者、システム管理者、DevOpsエンジニアは、ビルドスクリプト、デプロイ、CIジョブ、サーバーの保守、grep、sed、awk、sort などによる手早いデータ処理に使っています。

Hello, worldを書いて実行する

スクリプトは、使うインタプリタをシステムに伝えるshebang行で始まります。chmod でファイルを実行可能にしてから、実行します。

#!/usr/bin/env bash
# hello.sh
echo "Hello, world!"
chmod +x hello.sh
./hello.sh
# Hello, world!
bash hello.sh       # also works without chmod
bash --version      # check which version you have

#!/usr/bin/env bash は PATH からBashを探すので、/bin/bash を直接書くよりも移植性が高くなります。スクリプトがPOSIXの機能だけを使うなら #!/bin/sh も使えますが、その場合は [[ ]] や配列のようなBash固有の構文は使えません。

変数、クォート、展開

代入は name=value、参照は $name または ${name} で行います。すべての変数は文字列で、Bashが整数演算を行うのは $(( )) の中だけです。展開は二重引用符で囲むと、空白や特殊文字がそのまま保たれます。

name="Ada"                 # no spaces around =
count=3
echo "Hello, $name"        # Hello, Ada
echo "Next: $((count + 1))"  # Next: 4  (integer arithmetic)
today=$(date +%Y-%m-%d)    # command substitution
echo "Today is $today"
file="my report.txt"
echo "${file%.txt}.pdf"    # my report.pdf  (strip suffix)
echo "${#name}"            # 3  (string length)
echo "${missing:-default}" # default  (fallback value)
構文意味
"$var"変数の値。1つの単語として扱われる
'text'リテラルのテキスト。展開は一切されない
$(command)コマンドの出力
$((a + b))整数演算
$1, $2, $@, $#スクリプトや関数の引数、すべての引数、引数の数
$?直前のコマンドの終了ステータス(0は成功)

条件分岐とcase

if はコマンドを実行して、その終了ステータスを調べます。テストコマンド [[ ]] は文字列の比較やファイルの検査を行い、(( )) は数値を比較します。case は、値をglobパターンと照合します。

#!/usr/bin/env bash
file="${1:-notes.txt}"

if [[ -f "$file" ]]; then
  echo "$file exists"
elif [[ -d "$file" ]]; then
  echo "$file is a directory"
else
  echo "$file not found"
fi

n=7
if (( n % 2 == 1 )); then echo "$n is odd"; fi

case "$file" in
  *.txt) echo "text file" ;;
  *.sh)  echo "shell script" ;;
  *)     echo "something else" ;;
esac
テスト真になる条件
[[ -f path ]]パスが通常のファイルである
[[ -d path ]]パスがディレクトリである
[[ -z "$s" ]]文字列が空である
[[ "$a" == "$b" ]]文字列が等しい
(( a > b ))数値aがbより大きい
[[ $a -lt $b ]]数値aがbより小さい(古い書き方)

ループ

Bashには、単語のリストを回す for、C言語風の for (( ))、while、until があります。ファイルを1行ずつ読むには while read を使います。

for fruit in apple banana cherry; do
  echo "fruit: $fruit"
done

for (( i = 1; i <= 3; i++ )); do
  echo "i = $i"
done

for f in *.log; do
  [[ -e "$f" ]] || continue   # skip if no .log files match
  echo "log file: $f"
done

while IFS= read -r line; do
  echo "line: $line"
done < input.txt

IFS= read -r は、各行を書かれたとおりに読み取ります。IFS= は行頭の空白を保ち、-r はバックスラッシュを保ちます。

関数

関数は name() { ... } で定義し、コマンドのように呼び出します。引数は $1、$2 のように渡されます。関数は return で終了ステータス(0〜255)を返します。データを返すには、それを出力し、$( ) で出力を取り込みます。

greet() {
  local name="${1:-world}"   # local variable, default value
  echo "Hello, $name!"
}

is_even() {
  (( $1 % 2 == 0 ))          # exit status 0 means true
}

greet                         # Hello, world!
greet "Ann"                   # Hello, Ann!
msg=$(greet "Bo")             # capture output
echo "Got: $msg"              # Got: Hello, Bo!

if is_even 4; then echo "4 is even"; fi

配列と連想配列

インデックス配列は文字列のリストを保持します。連想配列は文字列のキーを値に対応付けます。これにはBash 4以降が必要で、macOSに付属する古いBash 3.2では使えません。

langs=("bash" "python" "go")
langs+=("rust")
echo "${langs[0]}"           # bash
echo "${#langs[@]}"          # 4
for l in "${langs[@]}"; do echo "- $l"; done

# associative array (Bash 4 or later)
declare -A port
port[http]=80
port[https]=443
echo "${port[https]}"        # 443
for key in "${!port[@]}"; do
  echo "$key -> ${port[$key]}"
done

要素を回すときは、必ず "${array[@]}" と引用符付きで書いてください。要素に空白が含まれていても、各要素が1つの単語として保たれます。

パイプとリダイレクト

Bashを代表する特長は、小さなプログラムを組み合わせられることです。パイプ | は、あるコマンドの出力を次のコマンドの入力に渡します。リダイレクトは、出力をファイルに送ったり、入力をファイルから読み込んだりします。ファイルディスクリプタの1は標準出力、2は標準エラー出力です。

ls -l > files.txt            # write stdout to a file (overwrite)
echo "more" >> files.txt     # append
grep error app.log 2> /dev/null   # discard error messages
make > build.log 2>&1        # stdout and stderr to the same file

# count the 3 most common words in a file
tr -cs '[:alpha:]' '\n' < book.txt | tr '[:upper:]' '[:lower:]' |
  sort | uniq -c | sort -rn | head -n 3

エラー処理と安全なスクリプト

デフォルトでは、Bashはコマンドが失敗しても処理を続けます。多くのスクリプトは set -euo pipefail で始まります。-e は未処理のエラーで終了し、-u は未設定の変数をエラーとして扱い、-o pipefail はパイプライン中のどれかのコマンドが失敗したらパイプライン全体を失敗にします。trap は、スクリプトの終了時にクリーンアップ用のコードを実行します。

#!/usr/bin/env bash
set -euo pipefail            # stop on errors, unset vars, pipe failures

tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT     # clean up when the script exits

if ! grep -q "ready" status.txt; then
  echo "status is not ready" >&2   # errors go to stderr
  exit 1
fi

cp config.txt "$tmp" || { echo "copy failed" >&2; exit 2; }
echo "ok"

|| で1行の中で失敗を処理し、&& で直前のコマンドが成功した場合にだけコマンドを実行します。set -e には例外があり(たとえば if の条件内のコマンドでは作動しません)、重要なコマンドは明示的に確認してください。

小さな完成スクリプト:日付付きバックアップ

このスクリプトは、ディレクトリをタイムスタンプ付きの .tar.gz ファイルにアーカイブし、最新の5つだけを残します。引数、デフォルト値、テスト、コマンド置換、パイプを使っています。

#!/usr/bin/env bash
# backup.sh: copy a directory into a dated .tar.gz archive
set -euo pipefail

usage() { echo "usage: $0 SOURCE_DIR [DEST_DIR]" >&2; exit 1; }

[[ $# -ge 1 ]] || usage
src="$1"
dest="${2:-$HOME/backups}"
[[ -d "$src" ]] || { echo "not a directory: $src" >&2; exit 1; }

mkdir -p "$dest"
stamp=$(date +%Y%m%d-%H%M%S)
archive="$dest/$(basename "$src")-$stamp.tar.gz"

tar -czf "$archive" -C "$(dirname "$src")" "$(basename "$src")"
size=$(du -h "$archive" | cut -f1)
echo "Created $archive ($size)"

# keep only the 5 newest archives for this source
ls -1t "$dest"/"$(basename "$src")"-*.tar.gz | tail -n +6 | while read -r old; do
  rm -f -- "$old"
  echo "Removed $old"
done
./backup.sh ~/projects/site
# Created /home/ann/backups/site-20261031-093000.tar.gz (2.1M)

次のステップ

  • 公式ドキュメントの「GNU Bash Reference Manual」。
  • ターミナルでの man bash と help(例:help read)。
  • スクリプトのよくあるバグを説明してくれる静的解析ツール、ShellCheck。
  • sh、dash、BusyBoxでも動くスクリプトが必要な場合は、POSIXシェルの仕様。

よくある質問

Bashとshの違いは何ですか?

sh はPOSIXシェルの標準で、dashなどのプログラムが実装しています。BashはPOSIX shと互換性があり、[[ ]]、配列、(( ))、ブレース展開といった機能を追加しています。

Bashとzshの違いは何ですか?

日常的なコマンドのほとんどは、どちらでも互換性があります。zshは対話的な機能が充実しており、macOSの標準シェルです。bash のshebangで書かれたスクリプトは、引き続きBashで実行されます。

スクリプトを実行すると permission denied と表示されるのはなぜですか?

ファイルが実行可能になっていません。chmod +x script.sh を実行するか、bash script.sh で起動してください。

Bashスクリプトに引数を渡すにはどうしますか?

./script.sh one two のように、スクリプト名の後ろに書きます。スクリプト内では $1 と $2 で参照でき、$@ はすべての引数、$# は引数の数を保持します。

大きなプログラムをBashで書くべきですか?

Bashは、コマンドをつなぎ合わせる用途に最適です。スクリプトが複雑になり、データ構造や慎重なエラー処理が必要になったら、通常はPythonのような言語のほうが保守しやすくなります。

コーディング講座の一覧