シェルスクリプト入門から実践まで
シェルスクリプトは、コマンドをファイルに書き並べて自動化する仕組みです。ここでは、最初の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が実行後も残る |
#!/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 | cmd2 | cmd1の出力を、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 file | fi・done・esac・閉じ括弧の書き忘れ | 開始と終了の対応を確認する。bash -nで調べる |
unbound variable | set -uで未定義の変数を参照した | 変数名のタイプミスを直す。または${var:-}でデフォルト値を指定する |
実際のエラーメッセージの表示例は、エラーメッセージ辞典でも調べられます。
練習用ターミナルで書いて動かす
読むだけでは身につきにくいため、練習用ターミナルの課題モードに、「シェルスクリプト」の課題を8問用意しています。viでスクリプトを書き、実行権限を付けて、実行結果を確認する流れを体験できます。書き方は自由で、「実行した結果が正しいか」で判定します。
- 最初のシェルスクリプトを書いて実行する
- 引数を受け取るスクリプトを書く
- if でファイルの有無を判定する
- for ループでファイルを順に処理する
- while read でファイルを1行ずつ処理する
- 終了コードで成否を伝える
- バックアップスクリプトを作る
- 関数を使ってログ出力をまとめる
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には挙動に注意が必要な場面もあるため、重要な処理では終了コードを明示的に確認するのも有効です。