0Pricing
DevOps Bootcamp · レッスン

ローカルスコープと終了コードを使った関数設計

壊れやすいグローバル変数ではなく、ローカル変数、終了ステータス、printfベースの戻り値を使う関数を記述します。

「ローカルスコープと終了コードを使った関数設計」はCoddyKit上の無料DevOps Bootcampレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはDevOps Bootcamp学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 DevOps Bootcampコースには全4レッスンが含まれています。

関数のスコープが重要な理由

Bashでは、変数はデフォルトでグローバルです。関数内で設定した変数は、localを明示的に宣言しない限り、呼び出し元のスコープに漏れ出します。これはシェルスクリプトで微妙なバグを引き起こす、よくある原因です。

  • localを使わない関数は、呼び出し元の変数を気付かないうちに上書きする可能性があります。
  • local変数は、関数が戻ると破棄されます。
  • スコープの境界を明確にすると、関数を再利用しやすく、単体でテストしやすくなります。

このレッスンでは、自己完結した関数の書き方を学びます。local変数を使い、終了コードとprintfで結果を伝え、暗黙的なグローバル状態に依存しない関数を作成します。

グローバル変数の漏洩問題

グローバル変数が漏洩する具体例を見てみましょう。関数 set_name は result という変数を設定しますが、その際に呼び出し元が持つ result 変数を気付かないうちに上書きしてしまいます。

このスクリプトを実行して、予期しない出力を確認してください。関数呼び出しの後、呼び出し元の result が消えています。

#!/usr/bin/env bash

set_name() {
    result="Alice"   # No 'local' — this is GLOBAL
}

result="important data"
echo "Before: $result"

set_name

echo "After:  $result"   # Prints 'Alice', not 'important data'

'local' によるローカル変数の宣言

local 組み込みコマンドを使うと、変数のスコープを、それを囲む関数と、その関数が呼び出す関数に限定できます。関数の外では、その変数は未設定になるか、以前の値を保持します。

  • local varname — 代入せずに宣言します。
  • local varname="value" — 宣言と代入を一度に行います。
  • local -i count=0 — 整数型のローカル変数を宣言します。
  • local -r PI=3.14159 — 読み取り専用のローカル定数を宣言します。

ベストプラクティス: グローバルにする必要が意図的にある場合を除き、関数内のすべての変数を local として宣言してください。

#!/usr/bin/env bash

greet() {
    local name="$1"          # local — safe
    local greeting="Hello, ${name}!"
    echo "$greeting"
}  # 'name' and 'greeting' vanish here

name="global value"
greet "Bob"
echo "name is still: $name"   # Prints 'global value'

戻り値としての終了コード

Bash の関数は文字列を return できません。return が設定できるのは、整数の終了ステータス(0〜255)だけです。慣例として、次のように使います。

  • return 0 — 成功
  • return 1(または 0 以外の値) — 失敗

呼び出し元では、呼び出し直後に $? を使って終了ステータスを取得するか、if 条件内で関数を直接使用します。終了コードは、関数から成功または失敗を通知する慣用的な方法です。

#!/usr/bin/env bash

is_even() {
    local -i n="$1"
    (( n % 2 == 0 ))   # arithmetic command: exits 0 if true, 1 if false
}

for num in 2 3 7 10; do
    if is_even "$num"; then
        echo "$num is even"
    else
        echo "$num is odd"
    fi
done

printf による文字列結果の伝達

関数から文字列の結果を返す必要がある場合、標準的なパターンは標準出力に出力し、コマンド置換 $() で取得することです。echo よりも printf が推奨される理由は次のとおりです。

  • printf は、デフォルトでは末尾に改行を付けません(\n を含めた場合を除きます)。
  • printf の動作は一貫しており、POSIX で定義されています。一方、echo の動作はシェルによって異なります。
  • コマンド置換は末尾の改行を取り除くため、printf '%s' "$value" は正確に値を扱えます。
#!/usr/bin/env bash

to_uppercase() {
    local input="$1"
    printf '%s' "${input^^}"   # Bash 4+ parameter expansion
}

word="hello"
upper=$(to_uppercase "$word")
echo "Original: $word"
echo "Upper:    $upper"

終了コードと stdout 出力の組み合わせ

適切に設計された関数は、結果を出力する(成功時)と同時に、失敗を通知する(終了コードによって)ことができます。呼び出し元は、出力を信頼する前に終了コードを確認して、処理を決定します。

次のパターンは、実際の Bash ライブラリで広く使われています。

  • 成功時: printf で結果を出力し、return 0 を実行します。
  • 失敗時: 診断メッセージを stderr(stdout ではありません)に書き込み、return 1 を実行します。
  • エラーを stderr に書き込むことで、パイプに渡す stdout をクリーンに保てます。
#!/usr/bin/env bash

divide() {
    local -i numerator="$1"
    local -i denominator="$2"

    if (( denominator == 0 )); then
        printf 'Error: division by zero\n' >&2
        return 1
    fi

    printf '%d' $(( numerator / denominator ))
    return 0
}

if result=$(divide 20 4); then
    echo "20 / 4 = $result"
else
    echo "Division failed."
fi

if result=$(divide 10 0); then
    echo "10 / 0 = $result"
else
    echo "Division failed (caught the error)."
fi

'local' による再帰関数の保護

再帰は、local が不可欠な理由を最も明確に示す例の一つです。再帰呼び出しが行われるたびに、呼び出しスタック上で各 local 変数の独立したコピーが作成されます。local がなければ、各呼び出しが同じグローバル変数を上書きし、誤った結果を生み出します。

次の階乗関数が安全に動作するのは、n と sub が各スタックフレームのローカル変数になっているためです。

#!/usr/bin/env bash

factorial() {
    local -i n="$1"
    local -i sub

    if (( n <= 1 )); then
        printf '1'
        return 0
    fi

    sub=$(factorial $(( n - 1 )))
    printf '%d' $(( n * sub ))
}

for i in 1 2 3 4 5 6; do
    echo "${i}! = $(factorial $i)"
done

local -n(Nameref)によるサブシェルの落とし穴の回避

コマンド置換 $() はサブシェルで実行されます。その中で行った変数への代入は、親シェルから見えません。関数から呼び出し元が用意した変数にサブシェルを使わず書き込みたい場合は、Bash 4.3 以降で利用できるnameref(local -n)を使います。

  • local -n ref="$1" により、ref は $1 に格納された変数の名前への別名になります。
  • 関数内で ref に代入すると、呼び出し元の変数が直接変更されます。
  • 実装の詳細をローカルに保ちながら、サブシェルを回避できます。
#!/usr/bin/env bash

# Fills caller's array by reference — no subshell needed
read_csv_line() {
    local -n _out="$1"    # nameref to caller's variable
    local line="$2"
    local IFS=','
    read -ra _out <<< "$line"
}

declare -a fields
read_csv_line fields "alice,30,engineer"

echo "Name:  ${fields[0]}"
echo "Age:   ${fields[1]}"
echo "Role:  ${fields[2]}"

小さな関数ライブラリの構築

実際の Bash プロジェクトでは、再利用可能な関数をライブラリファイルに分割し、スクリプトから source(またはドット演算子 .)で読み込みます。適切なライブラリ設計のルールは次のとおりです。

  • ライブラリ関数内のすべての変数を local にします。
  • ライブラリ関数では決して exit せず、return を使って呼び出し元を実行し続けられるようにします。
  • 名前の衝突を避けるため、一貫した名前空間プレフィックス(例: str_、log_)を使います。
  • 番兵変数を使って二重読み込みを防ぎます。

以下は、これらの規約に従った最小限の文字列ユーティリティライブラリです。

#!/usr/bin/env bash
# lib/str.sh  — string utility library

[[ -n "${_LIB_STR_LOADED:-}" ]] && return 0
_LIB_STR_LOADED=1

str_trim() {
    local str="$1"
    str="${str#"${str%%[![:space:]]*}"}"
    str="${str%"${str##*[![:space:]]}"}" 
    printf '%s' "$str"
}

str_repeat() {
    local -i times="$2"
    local char="$1"
    local -i i
    for (( i = 0; i < times; i++ )); do
        printf '%s' "$char"
    done
}

str_contains() {
    local haystack="$1"
    local needle="$2"
    [[ "$haystack" == *"$needle"* ]]
}

# --- self-test when executed directly ---
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
    trimmed=$(str_trim "   hello world   ")
    echo "Trimmed: '${trimmed}'"
    str_repeat '-' 20; echo
    if str_contains "bash scripting" "script"; then
        echo "Contains: yes"
    fi
fi

関数内での引数の検証

引数を受け取る関数では、早い段階で引数を検証し、入力が不正な場合は特定の終了コードを返す必要があります。これはガード節パターンと呼ばれます。つまり、早期に、明確に失敗させる方法です。

  • $# で引数の個数を確認します。
  • 処理を始める前に、型や形式を検証します。
  • 診断メッセージはstderr のみに出力し、stdout には決して出力しません。
  • 0 以外の戻りコードを用途ごとに分けます(例: 1 = 引数が不正、2 = ファイルが見つからない)。こうすると、呼び出し元で失敗の種類に応じて異なる対応ができます。
#!/usr/bin/env bash

file_line_count() {
    if (( $# != 1 )); then
        printf 'Usage: file_line_count <file>\n' >&2
        return 1
    fi

    local file="$1"

    if [[ ! -f "$file" ]]; then
        printf 'Error: not a file: %s\n' "$file" >&2
        return 2
    fi

    if [[ ! -r "$file" ]]; then
        printf 'Error: cannot read: %s\n' "$file" >&2
        return 3
    fi

    local -i count
    count=$(wc -l < "$file")
    printf '%d' "$count"
    return 0
}

# Test with /etc/hosts (exists on every Linux/macOS system)
if lines=$(file_line_count /etc/hosts); then
    echo "/etc/hosts has $lines lines"
else
    echo "Failed with exit code: $?"
fi

すべてを組み合わせる: 実践的な例

ここでは、このレッスンのすべての概念を組み合わせた、完全に自己完結したスクリプトを示します。

  • すべての関数で local 変数を使っています。
  • 成功または失敗の通知に終了コードを使っています。
  • 文字列結果の伝達に printf を使っています。
  • エラーは stderr に、結果は stdout に書き込んでいます。
  • 引数の検証にガード節を使っています。

処理の流れを確認してください。parse_version がデータを抽出し、version_ge が比較を行い、main が両方を適切に利用しています。

#!/usr/bin/env bash

# Parse a semver string into components via nameref
parse_version() {
    local -n _major="$2" _minor="$3" _patch="$4"
    local version="$1"
    local IFS='.'
    local -a parts
    read -ra parts <<< "$version"
    _major="${parts[0]:-0}"
    _minor="${parts[1]:-0}"
    _patch="${parts[2]:-0}"
}

# Return 0 if version $1 >= version $2
version_ge() {
    local -i maj_a min_a pat_a
    local -i maj_b min_b pat_b
    parse_version "$1" maj_a min_a pat_a
    parse_version "$2" maj_b min_b pat_b

    if   (( maj_a != maj_b )); then (( maj_a > maj_b ))
    elif (( min_a != min_b )); then (( min_a > min_b ))
    else                             (( pat_a >= pat_b ))
    fi
}

require_bash_version() {
    local required="$1"
    local actual="${BASH_VERSION%%(*}"
    if version_ge "$actual" "$required"; then
        printf 'Bash %s satisfies >= %s\n' "$actual" "$required"
        return 0
    else
        printf 'Error: need Bash >= %s, got %s\n' "$required" "$actual" >&2
        return 1
    fi
}

main() {
    require_bash_version "4.3" || return 1
    require_bash_version "99.0" || true   # demonstrates failure path
}

main

理解度チェック: ローカル変数と戻り値

次の Bash 関数について考えてみてください。呼び出し元で文字列結果を取得する正しい方法は何でしょうか。また、変数 tmp について正しい説明はどれでしょうか。

transform() {
    local tmp="${1,,}"   # lowercase
    printf '%s' "$tmp"
    return 0
}

まとめ: ローカルスコープと戻りコードを持つ関数

このレッスンでは、整理され、組み合わせて使いやすく、安全な Bash 関数の書き方を学びました。

  • 関数内の変数には常に local を使うことで、呼び出し元のスコープを汚染しないようにします。
  • 終了コード(return 0/1/N)を使うことで成功または失敗を通知します。終了コードは if、&&、|| と自然に組み合わせられます。
  • 文字列結果の伝達にはstdout への printf を使い、呼び出し元で $() によって取得します。
  • エラーは stderr(>&2)に書き込み、データの受け渡しやパイプ処理に使う stdout をクリーンに保ちます。
  • サブシェルのオーバーヘッドなしに呼び出し元が用意した変数へ書き込む必要がある場合は、local -n(nameref)を使います。
  • ガード節(早い段階で引数を検証し、不正な入力があればすぐに戻る)によって、関数の堅牢性と自己説明性が高まります。
  • ライブラリファイルは source で読み込み、名前空間プレフィックスを使い、exit を呼び出さず、二重読み込みを防ぐ必要があります。

これらのパターンを身につけることが、壊れやすい一度きりのスクリプトと、プロフェッショナルで保守しやすい Bash コードベースとの違いにつながります。

よくある質問

「ローカルスコープと終了コードを使った関数設計」レッスンは無料ですか?

はい。「ローカルスコープと終了コードを使った関数設計」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、DevOps Bootcampコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 DevOps Bootcampコースには全4レッスンが含まれています。

「ローカルスコープと終了コードを使った関数設計」で何を学びますか?

壊れやすいグローバル変数ではなく、ローカル変数、終了ステータス、printfベースの戻り値を使う関数を記述します。 ブラウザで直接実行するハンズオンコードでDevOps Bootcampを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

DevOps Bootcampを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのDevOps Bootcampは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。

「ローカルスコープと終了コードを使った関数設計」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このDevOps Bootcampレッスンでコードを書いて実行できますか?

はい。すべてのDevOps Bootcampレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. ローカルスコープと終了コードを使った関数設計
  2. 再利用可能なBashライブラリの構築と読み込み
  3. getoptsによるフラグと引数の解析
  4. 関数間での配列と連想マップの受け渡し
← DevOps Bootcampに戻る