こんにちは、かつコーチです。
制御構文編(if・for・while・case)はここまでで一通り押さえてきました。
スクリプトが長くなってくると、同じような処理を何度も書き写している自分に気づくはずです。
今回は、そうした重複をなくすシェル関数の定義と使い方を解説します。
シェル関数とは?
処理をまとめて名前を付ける仕組み
シェル関数とは、複数のコマンドをひとまとまりにして、名前を付けて呼び出せるようにする仕組みです。
会社の業務マニュアルに近いイメージを持つとわかりやすいです。
毎回「まずAをして、次にBをして、最後にCを確認する」と口頭で説明する代わりに、その手順に「開店準備」という名前を付けておけば、次からは「開店準備をやっておいて」の一言で済みます。
シェル関数もこれと同じで、複数行の処理に名前を付けておくことで、スクリプトのどこからでも短い呼び出しだけで実行できるようになります。
なぜ関数が必要なのか
関数を使わずに同じ処理をコピー&ペーストで増やしていくと、次のような問題が起きます。
- 修正が必要になったとき、コピーした箇所すべてを直す必要がある
- 一部だけ直し忘れて、処理内容がバラバラになる
- スクリプト全体が長くなり、どこで何をしているか読みにくくなる
関数化しておけば、修正は関数の中身1箇所だけで済みますし、呼び出し側は「何をしているか」が関数名から一目で分かるようになります。
基本の書き方
関数定義の基本構文
シェル関数の定義方法は主に2種類あります。
#!/bin/bash
# 書き方1: function キーワードを使う
function greet() {
echo "こんにちは、$1さん"
}
# 書き方2: function キーワードを省略する(POSIX互換)
greet2() {
echo "こんばんは、$1さん"
}
greet "かつコーチ"
greet2 "かつコーチ"
Bashでは両方使えますが、POSIX準拠のシェル(dashなど)を意識するならfunctionキーワードを省略した書き方のほうが移植性が高くなります。
Ubuntu 26.04では#!/bin/bashを指定していれば、どちらの書き方でも問題なく動作します。
引数の受け取り方
関数の中では、$1・$2・$@・$#をスクリプト全体の引数と同じ感覚で使えます。
ただし、これらは「スクリプトに渡された引数」ではなく「その関数を呼び出したときに渡した引数」を指す点に注意してください。
#!/bin/bash
backup_file() {
local target="$1"
local dest_dir="$2"
echo "${target}を${dest_dir}にコピーします"
cp "$target" "$dest_dir"
}
backup_file "report.txt" "/tmp/backup"
$1はreport.txt、$2は/tmp/backupを指し、スクリプト起動時に渡した引数(あれば)とは別物として扱われます。
戻り値とreturnの制約
関数の実行結果を呼び出し元に伝える方法は2つあります。
1つ目はreturn文で終了ステータス(0〜255の整数)を返す方法です。
#!/bin/bash
is_even() {
local num="$1"
if (( num % 2 == 0 )); then
return 0 # 成功(真)
else
return 1 # 失敗(偽)
fi
}
if is_even 4; then
echo "4は偶数です"
fi
2つ目は、文字列など数値以外の値を返したい場合の方法で、echoで出力してコマンド置換$()で受け取ります。
#!/bin/bash
get_today() {
echo "$(date +%Y-%m-%d)"
}
today="$(get_today)"
echo "今日は${today}です"
よくあるつまずきポイント・エラー対処
local忘れによるグローバル変数の汚染
シェル関数の変数は、デフォルトではグローバル変数(スクリプト全体で共有される変数)になります。
これに気づかず、関数内でうっかり変数名を使い回すと、意図しない上書きが起きます。
❌ Before:localを付けずに関数内で変数を使う
#!/bin/bash
count=10
increment() {
count=$((count + 1)) # グローバルのcountを直接書き換えてしまう
}
increment
increment
echo "$count" # 期待は10のままのつもりが12になっている
実際に私がこれで詰まったのは、複数の関数で同じ変数名(iやresultなど)を使い回していたときです。
片方の関数の呼び出し後にもう片方の関数の結果がおかしくなり、原因を探すのに30分ほどかかりました。
✅ After:関数内で使う変数には必ずlocalを付ける
#!/bin/bash
count=10
increment() {
local count=$((count + 1)) # 関数内だけで有効なローカル変数
echo "関数内: $count"
}
increment
echo "関数外: $count" # 10のまま
localを付けることで、その変数は関数の中だけで有効になり、関数の外の同名変数には影響しません。
関数を書くときは「変数には基本的にlocalを付ける」を習慣にしておくと、この種の事故を防げます。
return に文字列を渡そうとして失敗する
returnは終了ステータス(0〜255の整数のみ)を返す仕組みなので、文字列や256以上の数値を渡すと想定外の値になります。
#!/bin/bash
get_status() {
return "処理OK" # エラーになる
}
これを実行すると、numeric argument required(数値の引数が必要)というエラーになります。
文字列を返したい場合は、前述のecho + コマンド置換のパターンを使うのが正しい書き方です。
応用・一歩先の使い方
関数を別ファイルに分けてsourceで読み込む
関数の数が増えてきたら、1つのファイルにlog_infoやbackup_fileのような共通関数だけをまとめておき、sourceコマンド(.コマンドでも可)で読み込む構成にすると管理しやすくなります。
# lib/functions.sh
log_info() {
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $1"
}
#!/bin/bash
# main.sh
source "$(dirname "$0")/lib/functions.sh"
log_info "処理を開始します"
この構成にしておくと、複数のスクリプトから同じ関数群を使い回せるようになり、次回以降の記事(cronでの定期実行やバックアップスクリプトの実践編)でもこの考え方を土台にしていきます。
まとめ
この記事のポイント
- シェル関数は複数のコマンドに名前を付けて再利用できるようにする仕組み(業務マニュアル化のようなもの)
- 引数は
$1・$2・$@で受け取り、関数呼び出し時の引数を指す - 戻り値は
return(0〜255の整数)か、echo+ コマンド置換のどちらかで返す - 関数内の変数には必ず
localを付けてグローバル変数の汚染を防ぐ
次に読むべき記事
- sed・awkと連携した文字列操作の基本
- cronで定期実行するスクリプトを組む
タグ: Shell, 中級者向け, 関数