シェルスクリプト入門から実践まで

シェルスクリプトは、コマンドをファイルに書き並べて自動化する仕組みです。ここでは、最初の1本を動かすところから、引数・条件分岐・繰り返し・関数、安全に書くための決まりごと、エラーの調べ方まで、手を動かしながら順に整理します。試験対策の要点は中級の第13章で扱っているため、このページでは「書けるようになること」を重視しています。

シェルスクリプトでできること・向いていること

シェルスクリプトは、ターミナルで手入力しているコマンドを、ファイルにまとめて自動で実行できるようにしたものです。バックアップ、ログの集計、ディスク使用率のチェック、サービスの起動確認など、「同じ手順を繰り返す作業」と相性が良く、cronと組み合わせれば定期的に自動実行もできます。

一方、複雑なデータ構造の処理や、数百行を超える大きなプログラムには向いていません。そうした場合は、PythonなどのLinuxに入っている別の言語を検討するのが一般的です。目安として、「コマンドをつなげて使う程度の自動化」であればシェルスクリプトが手軽です。

最初のスクリプトを書いて実行する

ファイルを作成し、1行目にシェバン(#!で始まる、実行するプログラムの指定)を書きます。続けて実行したいコマンドを書きます。

$ vi hello.sh
# 中身(viで入力して :wq で保存)
#!/bin/bash
echo "Hello, Linux"

保存しただけでは実行できません。実行権限を付けてから、パスを付けて実行します。

$ ./hello.sh
-bash: ./hello.sh: Permission denied     # 実行権限がないので失敗
$ chmod +x hello.sh                    # 実行権限を追加
$ ./hello.sh
Hello, Linux

カレントディレクトリのスクリプトを、先頭の./なしでhello.shとだけ入力すると、command not foundになります。コマンドの検索先であるPATHに、カレントディレクトリが含まれていないためです。これは安全のための仕様なので、./を付けて実行する癖をつけましょう。

実行方法の違い

書き方実行権限特徴
./hello.sh必要シェバンで指定したプログラムで実行する。通常の実行方法
bash hello.sh不要bashで直接読み込んで実行する。権限の切り分けにも使える
source hello.sh(. hello.sh)不要今のシェルの中で実行する。変数やcdが実行後も残る
シェバンは2種類あります:
#!/bin/bashはbashの場所を直接指定する書き方で、#!/usr/bin/env bashはPATHから探す書き方です。
多くの環境では前者で問題なく動きます。

変数とクォートのルール

変数への代入は名前=値の形で、イコールの前後にスペースを入れません。値を使うときは$名前と書きます。

#!/bin/bash
name="Alice"
echo "Hello, $name"
echo "Hello, ${name}san"    # 変数名の区切りを明示したいときは ${ } で囲む

文字列を囲む引用符は、種類によって動きが違います。ここがシェルスクリプトで最もつまずきやすい点です。

書き方変数の展開使いどころ
"$name"(二重引用符)される変数を含む文字列。基本はこちらを使う
'$name'(一重引用符)されない記号をそのまま文字として扱いたいとき
$name(引用符なし)される(空白で分割)原則として避ける

引用符なしで変数を使うと、値に空白が含まれたときに、複数の単語に分割されてしまいます。たとえばmy file.txtというファイル名をrm $fileと書くと、myとfile.txtの2つを削除しようとします。変数は原則として"$変数"のように二重引用符で囲むのが、安全に書くための基本です。

デフォルト値と環境変数

#!/bin/bash
dir=${1:-/tmp}              # 引数がなければ /tmp を使う
echo "対象: $dir"
export LANG=C               # export すると、このスクリプトが起動する子プロセスにも引き継がれる
$ ./d.sh
対象: /tmp
$ ./d.sh /var
対象: /var

引数を受け取る(位置パラメータ)

スクリプトに渡した引数は、$1・$2…で受け取れます。主な特殊変数は次のとおりです。

変数意味
$0スクリプト自身の名前
$1, $2…1番目、2番目…の引数
$#引数の個数
"$@"すべての引数を、1つずつ独立した単語として展開する
$?直前のコマンドの終了コード

引数をすべて処理するときは、"$@"を二重引用符で囲んで使います。スペースを含む引数も、1つの値として正しく扱えます。

#!/bin/bash
for a in "$@"; do
  echo "[$a]"
done
$ ./args.sh "my file.txt" b
[my file.txt]
[b]

引数が足りないときの使い方表示

#!/bin/bash
if [ $# -lt 1 ]; then
  echo "使い方: $0 ファイル名" >&2     # >&2 で標準エラー出力へ
  exit 2
fi
echo "処理: $1"
$ ./usage.sh; echo $?
使い方: ./usage.sh ファイル名
2
$ ./usage.sh x.txt; echo $?
処理: x.txt
0

条件分岐(if・case)

ifの条件には、[ ](testコマンドの別の書き方)を使います。[と]の内側には、必ずスペースが必要です。これを忘れるとcommand not foundのエラーになります。

#!/bin/bash
if [ -f "$1" ]; then
  echo "$1 はファイルです"
elif [ -d "$1" ]; then
  echo "$1 はディレクトリです"
else
  echo "$1 は存在しません"
fi

よく使う判定

種類書き方意味
ファイル-e / -f / -d存在する / 通常ファイル / ディレクトリ
ファイル-r / -w / -x読める / 書ける / 実行できる
文字列-z "$s" / -n "$s"空である / 空でない
文字列"$a" = "$b" / !=等しい / 等しくない
数値-eq -ne -lt -le -gt -ge等しい・異なる・未満・以下・より大きい・以上

文字列の比較には=、数値の比較には-eqなどを使います。[ 10 -lt 9 ]は数値としての比較で偽ですが、[ "10" \< "9" ]のように文字列として比べると、辞書順で「10」が先になり、結果が逆になります。

case文

1つの値で何通りにも分岐するときは、caseのほうが見やすくなります。サービスの起動スクリプトでよく使われる書き方です。

#!/bin/bash
case "$1" in
  start) echo "起動します" ;;
  stop)  echo "停止します" ;;
  *)     echo "使い方: $0 {start|stop}" ;;
esac
$ ./case.sh start
起動します
$ ./case.sh foo
使い方: ./case.sh {start|stop}

繰り返し(for・while)

for:一覧の要素を順に処理する

#!/bin/bash
for f in *.log; do
  echo "found: $f"
done

for i in 1 2 3; do     # 数字の一覧。{1..3} とも書ける(bashの機能)
  echo "$i 回目"
done
注意:
for f in $(ls)のようにlsの出力を使うと、ファイル名にスペースが含まれたときに、単語ごとに分割されて正しく処理できません。
ファイルを順に処理するときは、for f in *.logのようにワイルドカードを直接使うのが安全です。

while read:ファイルを1行ずつ処理する

#!/bin/bash
while read -r name; do
  echo "user: $name"
done < users.txt
$ cat users.txt
alice
bob
$ ./users.sh
user: alice
user: bob

done < ファイル名のように、ループの終わりにファイルを渡すと、1行ずつ読み込めます。read -rの-rは、バックスラッシュを特別な文字として扱わない安全な指定です。

while:条件が成り立つあいだ繰り返す

#!/bin/bash
count=1
while [ $count -le 3 ]; do
  echo "$count 回目"
  count=$((count + 1))     # $(( )) で算術計算
done

終了コードと && ・ ||

コマンドは終了時に終了コードを返します。0が成功、それ以外が失敗です。直前の終了コードは$?で確認できます。スクリプトの途中や末尾でexit 数字と書くと、そのコードを呼び出し元に返せます。cronや別のスクリプトは、この値で成功か失敗かを判断します。

#!/bin/bash
if grep -q ERROR app.log; then      # -q: 出力せず、見つかったかどうかだけ返す
  echo "ERROR found"
  exit 1
fi
echo OK
$ ./scan.sh; echo $?
ERROR found
1

&&は「前のコマンドが成功したら次を実行」、||は「失敗したら次を実行」です。短い条件分岐を、ifを使わずに書けます。

$ mkdir backup && echo "作成成功"
作成成功
$ cd /nonexistent || echo "移動に失敗しました"
-bash: cd: /nonexistent: No such file or directory
移動に失敗しました

関数にまとめる

同じ処理を何度も書くときは、関数にまとめます。関数の中の引数も$1で受け取ります。関数の中だけで使う変数は、localを付けて宣言すると、外の変数を上書きせずに済みます。

#!/bin/bash
log() {
  echo "[INFO] $1"
}
add() {
  local sum=$(( $1 + $2 ))
  echo "$sum"
}

log start
result=$(add 3 4)          # 関数の出力は $( ) で受け取る
log "結果: $result"
$ ./func.sh
[INFO] start
[INFO] 結果: 7

他の言語と違い、シェルの関数がreturnで返せるのは終了コード(0〜255の数値)だけです。文字列や計算結果を返したいときは、echoで出力し、$( )で受け取ります。関数は、呼び出す前に定義されている必要があります。

入出力:リダイレクト・パイプ・ヒアドキュメント

書き方意味
cmd > file標準出力をファイルへ書き込む(上書き)
cmd >> file標準出力をファイルの末尾へ追記する
cmd 2> err.log標準エラー出力だけをファイルへ
cmd > out.log 2>&1標準出力と標準エラー出力を、同じファイルへまとめる
cmd > /dev/null 2>&1出力をすべて捨てる(cronで出力を抑えるときなど)
cmd1 | cmd2cmd1の出力を、cmd2の入力につなぐ

ヒアドキュメントを使うと、複数行のテキストをスクリプトの中にそのまま書いて、コマンドに渡せます。区切りの語(ここではEOF)を一重引用符で囲むと、中の$が展開されません。

#!/bin/bash
cat <<'EOF' > report.txt
定期レポート
$HOME はそのまま表示されます
EOF

安全に書くための決まりごと

スクリプトの先頭に、次の1行を入れておく書き方が広く使われています。

#!/bin/bash
set -euo pipefail
オプション効果
-eコマンドが失敗した時点でスクリプトを停止する
-u未定義の変数を使うとエラーにする(変数名のタイプミスに気づける)
-o pipefailパイプの途中のコマンドが失敗した場合も、全体の失敗として扱う
#!/bin/bash
set -euo pipefail
echo start
false                      # 失敗する(終了コード1)
echo "ここには来ない"
$ ./safe.sh; echo "exit=$?"
start
exit=1
注意:
set -eは万能ではありません。
ifの条件部分や、&&・||の左側で失敗したコマンドでは、スクリプトは停止しません。
重要な処理では、終了コードを自分で確認しましょう。

一時ファイルの後始末(trap)

一時ファイルはmktempで作り、trapで「スクリプトが終了するときに必ず削除する」と登録しておくと、途中で失敗してもごみが残りません。

#!/bin/bash
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT     # 終了時(成功でも失敗でも)に実行される
echo data > "$tmp"
cat "$tmp"

破壊的な操作の前に

rm -rf "$dir/"のような処理では、$dirが空だと意図しない場所を消すおそれがあります。set -uを付けるか、[ -n "$dir" ]で空でないことを確かめてから実行しましょう。削除対象は、事前にechoで表示して確認する習慣も有効です。

デバッグの方法

思いどおりに動かないときは、実行の流れを表示させるのが近道です。

$ bash -x x.sh              # 実行される各行を、先頭に + を付けて表示
+ name=World
+ echo 'Hello, World'
Hello, World
$ bash -n x.sh              # 実行せず、文法のエラーだけ調べる

スクリプトの一部だけ追跡したいときは、その範囲をset -xとset +xで囲みます。文法の誤りやよくある書き間違いを自動で指摘してくれるShellCheckというツールもあり、実務ではよく使われています。

実用例

例1:ディスク使用率を調べて、閾値を超えたら警告する

#!/bin/bash
usage=$(df -P / | awk 'NR==2 {gsub("%","",$5); print $5}')
echo "使用率: ${usage}%"
if [ "$usage" -ge 80 ]; then
  echo "警告: 80%以上です"
fi

df -Pは表示形式を固定するオプションで、awkで2行目の5列目(使用率)を取り出し、%を除いて数値として比較しています。閾値は、運用ルールに合わせて変更してください。

例2:ログから ERROR の件数を数える

#!/bin/bash
count=$(grep -c ERROR app.log)
echo "ERROR: ${count}件"
$ ./cnt.sh
ERROR: 2件

例3:バックアップを取り、古いものを削除する

#!/bin/bash
set -euo pipefail
src=$1
dest=/tmp/backup
mkdir -p "$dest"
cp -p "$src" "$dest/$(basename "$src").$(date +%Y%m%d)"
find "$dest" -name "*.bak" -mtime +7 -delete     # 7日より古い .bak を削除
echo "完了: $src"

ファイル名に日付を付けると、バックアップを世代ごとに残せます。find ... -deleteは強力なので、実際に使う前に-deleteを外して、削除対象が想定どおりかを確認してください。定期実行は、cronに登録して行います。

cronで動かすときの注意

手で実行すると動くのに、cronから実行すると動かない、というトラブルがよくあります。主な原因は、cronの環境変数PATHが最小限であることです。コマンドは絶対パス(/usr/bin/awsなど)で書くか、スクリプトの先頭でPATHを設定します。また、出力は>> /var/log/myjob.log 2>&1のようにログファイルへ残しておくと、失敗の原因を調べられます。詳しくは、cronジョブが実行されない場合の調べ方も参考にしてください。

よくあるエラーと原因

表示されるエラー主な原因対処
Permission denied実行権限がないchmod +xを実行する。またはbash script.shで実行する
command not found(スクリプト名)./を付けていない./script.shと実行する
bad interpreter: No such file or directory改行コードがCRLF。またはシェバンの指すパスが存在しない改行コードをLFに変換する。シェバンのパスを確認する
[: =: unary operator expected引用符なしの変数が空になっている[ "$x" = "a" ]のように、二重引用符で囲む
[1: command not found[の内側にスペースがない[ $a -eq 1 ]のように、前後にスペースを入れる
syntax error: unexpected end of filefi・done・esac・閉じ括弧の書き忘れ開始と終了の対応を確認する。bash -nで調べる
unbound variableset -uで未定義の変数を参照した変数名のタイプミスを直す。または${var:-}でデフォルト値を指定する

実際のエラーメッセージの表示例は、エラーメッセージ辞典でも調べられます。

練習用ターミナルで書いて動かす

読むだけでは身につきにくいため、練習用ターミナルの課題モードに、「シェルスクリプト」の課題を8問用意しています。viでスクリプトを書き、実行権限を付けて、実行結果を確認する流れを体験できます。書き方は自由で、「実行した結果が正しいか」で判定します。

  1. 最初のシェルスクリプトを書いて実行する
  2. 引数を受け取るスクリプトを書く
  3. if でファイルの有無を判定する
  4. for ループでファイルを順に処理する
  5. while read でファイルを1行ずつ処理する
  6. 終了コードで成否を伝える
  7. バックアップスクリプトを作る
  8. 関数を使ってログ出力をまとめる

viの操作に不安がある場合は、先にviエディタの基本操作を確認してください。

シェルスクリプトに関するよくある質問

シェルスクリプトのファイル名は .sh でなければいけませんか?

いいえ、拡張子に決まりはありません。.shを付けるのは「シェルスクリプトだと分かりやすくするための慣習」です。実行できるかどうかを決めるのは、拡張子ではなく実行権限(chmod +x)と、1行目のシェバンです。

./script.shとbash script.shの違いは何ですか?

./script.shは、実行権限とシェバンにもとづいて、指定されたプログラムで実行します。bash script.shは、実行権限がなくてもbashで読み込んで実行します。権限がなくてPermission deniedになったときの切り分けにも使えます。

source script.shと./script.shの違いは?

./script.shは別のプロセス(子プロセス)で実行するため、スクリプト内で変えた変数やcdは、実行後の元のシェルには残りません。sourceは今のシェルの中で実行するため、変数やcdの結果がそのまま残ります。設定ファイルの読み込みにsource ~/.bashrcを使うのはこのためです。

shとbashは同じですか?

別物です。shはPOSIXで定められた最小限のシェルで、bashはそれを拡張したシェルです。[[ ]]や配列などはbash独自の機能のため、#!/bin/shのスクリプトでは使わないのが無難です。Debian系・Ubuntuではshがdashを指していることが多く、同じ書き方でも動きが変わる場合があります。

Windowsで書いたスクリプトがLinuxで動かないのはなぜですか?

改行コードがCRLFになっていることが多い原因です。1行目のシェバンに余分な^Mが付いて、bad interpreterというエラーになります。エディタの設定で改行コードをLFに変えるか、sed -i 's/\r$//' script.shで変換できます。

スクリプトの途中でエラーが出たのに、最後まで実行されてしまうのはなぜですか?

bashは、既定ではコマンドが失敗しても次の行へ進みます。失敗した時点で止めたいときは、スクリプトの先頭にset -euo pipefailを書きます。ただし、-eには挙動に注意が必要な場面もあるため、重要な処理では終了コードを明示的に確認するのも有効です。

関連する章:
試験対策を含む入門は中級「シェル環境のカスタマイズとシェルスクリプト入門」、viの操作は「viエディタの基本操作」、定期実行は「ユーザー管理とジョブスケジューリング」で扱っています。

← Linuxコマンド一覧へ戻る