0Pricing
DevOps Bootcamp · レッスン

再利用可能なBashライブラリの構築と読み込み

共有ヘルパーを、インクルードガードと名前空間付き関数プレフィックスを備えたsource可能な.shライブラリファイルに整理します。

「再利用可能なBashライブラリの構築と読み込み」はCoddyKit上の無料DevOps Bootcampレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはDevOps Bootcamp学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 DevOps Bootcampコースには全4レッスンが含まれています。

Bash ライブラリとは

ソフトウェア工学において、ライブラリとは、複数のプログラムで共有できる再利用可能な関数の集合です。Bash では、source 可能なシェルスクリプトによって同じ概念を実現できます。

ヘルパー関数をすべてのスクリプトにコピー&ペーストする代わりに、専用の .sh ファイルに配置し、source コマンド(または短縮形の .)で読み込みます。そのファイルで定義された関数、変数、エイリアスは、呼び出し元スクリプトの現在のシェルセッションで利用できるようになります。

  • DRY 原則(Don't Repeat Yourself)を促進します
  • バグ修正を一元化できます。一度修正すれば、すべての呼び出し元に反映されます
  • 個々のスクリプトが短くなり、読みやすくなります
  • ログ、エラー処理、ユーティリティロジックをチーム全体で統一できます

適切に構成された Bash プロジェクトでは通常、高水準言語の慣習にならい、これらの共有ファイルを格納する lib/ ディレクトリを用意します。

source コマンドとドット演算子

ライブラリファイルを現在のシェル環境に読み込む方法は、同等のものが 2 つあります。

  • source /path/to/lib.sh — 明示的で読みやすい形式
  • . /path/to/lib.sh — POSIX 互換の短縮形

どちらも現在のシェルプロセス内でファイルを実行するため、ファイル内で定義されたすべての関数と変数が、呼び出し直後からスクリプトの環境の一部になります。

よく使われるパターンとして、$BASH_SOURCE を使って呼び出し元スクリプトからの相対位置でライブラリを特定します。これにより、インストール先に関係なくプロジェクトを移植できます。

#!/usr/bin/env bash
# main.sh — load a library relative to this script's own location

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/utils.sh"

echo "Library loaded. Calling greet..."
greet "World"

最初のライブラリファイルの作成

ライブラリファイルは、関数定義(場合によっては定数)だけを含む通常の .sh ファイルです。トップレベルで副作用のあるコードを実行してはいけません。直接実行するのではなく、source されることを意図したファイルだからです。

主な規約:

  • ライブラリの目的を説明する shebang コメントから始めます
  • 関数だけを定義し、トップレベルに main のロジックを置きません
  • 関数内では return を使います(呼び出し元を終了させる exit は決して使いません)
  • プロジェクトの lib/ サブディレクトリにファイルを置きます
#!/usr/bin/env bash
# lib/utils.sh — General-purpose utility functions

# Print a greeting message
greet() {
    local name="${1:-stranger}"
    echo "Hello, ${name}!"
}

# Print a timestamped log line to stderr
log_info() {
    echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" >&2
}

# Print an error message and return a failure code
log_error() {
    echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" >&2
    return 1
}

インクルードガード: 二重読み込みの防止

複数のスクリプトが同じライブラリを source したり、ライブラリがメインスクリプトからも読み込まれる別のライブラリを source したりすると、関数が複数回定義されることがあります。これは時間を無駄にするだけでなく、実行途中に関数の本体が置き換わることで、見つけにくいバグの原因にもなります。

解決策はインクルードガードです。これはフラグとして機能する変数です。初回の読み込み時は変数が未設定なので、ファイルの処理が続行されます。その後の読み込みではガードがすでに設定されているため、ファイルはすぐに戻ります。

これは、C/C++ の #pragma once や、他の言語における if not already imported パターンに相当する Bash の仕組みです。

#!/usr/bin/env bash
# lib/utils.sh — with include guard

# Guard: if already sourced, do nothing
[[ -n "${_LIB_UTILS_LOADED:-}" ]] && return 0
_LIB_UTILS_LOADED=1

greet() {
    local name="${1:-stranger}"
    echo "Hello, ${name}!"
}

log_info() {
    echo "[INFO]  $(date '+%Y-%m-%d %H:%M:%S')  $*" >&2
}

log_error() {
    echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S')  $*" >&2
    return 1
}

名前空間付き関数プレフィックス

Bash の関数には、単一のグローバル名前空間しかありません。2 つのライブラリがそれぞれ log や init という関数を定義すると、後の定義が前の定義を気付かないうちに上書きします。

標準的な対策は名前空間プレフィックスです。ライブラリ内のすべての関数に、ライブラリの短い名前と 2 つのコロン(::)またはアンダースコアを付けます。たとえば、文字列ユーティリティライブラリには str::、ファイルライブラリには file:: を使います。

  • 衝突が起きる可能性を大幅に下げられます
  • 呼び出し側のコードが自己説明的になります。str::trim を見れば、関数の場所がすぐに分かります
  • Grep で検索しやすくなります。grep 'str::' main.sh を実行すれば、文字列ライブラリの呼び出しをすべてすぐに確認できます
#!/usr/bin/env bash
# lib/str.sh — String utility library (namespaced)

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

# Trim leading and trailing whitespace
str::trim() {
    local s="$1"
    s="${s#"${s%%[![:space:]]*}"}"
    s="${s%"${s##*[![:space:]]}"}"  
    echo "$s"
}

# Convert string to uppercase
str::upper() {
    echo "${1^^}"
}

# Convert string to lowercase
str::lower() {
    echo "${1,,}"
}

# Check if a string contains a substring
str::contains() {
    [[ "$1" == *"$2"* ]]
}

lib/ ディレクトリの整理

プロジェクトが大きくなると、1 つの utils.sh では扱いにくくなります。役割ごとに分けたライブラリファイルを lib/ ディレクトリ内に配置します。

  • lib/log.sh — ログ出力ヘルパー(log::info、log::warn、log::error)
  • lib/str.sh — 文字列操作(str::trim、str::upper)
  • lib/fs.sh — ファイルシステムヘルパー(fs::require_dir、fs::safe_rm)
  • lib/net.sh — ネットワーク確認(net::wait_for_port、net::is_online)

1 つのブートストラップローダーファイル(lib/bootstrap.sh)ですべてを正しい順序で source できるため、各スクリプトでは 1 回 source を呼び出すだけで済みます。

#!/usr/bin/env bash
# lib/bootstrap.sh — Load all project libraries in dependency order

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

_BOOTSTRAP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

source "${_BOOTSTRAP_DIR}/log.sh"
source "${_BOOTSTRAP_DIR}/str.sh"
source "${_BOOTSTRAP_DIR}/fs.sh"
source "${_BOOTSTRAP_DIR}/net.sh"

log::info "All libraries loaded."

完全なログ出力ライブラリ

ログ出力は、スクリプト間で最も共通して必要となる処理です。専用の lib/log.sh に、出力形式、ログレベル、カラーコードを集約します。

このようなライブラリを用意すると、プロジェクト内のすべてのスクリプトで、形式が統一されたタイムスタンプ付きの色分けメッセージを出力でき、書式設定のコードを重複させずに済みます。

#!/usr/bin/env bash
# lib/log.sh — Coloured, levelled logging library

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

# Colour codes (disabled when not writing to a terminal)
_LOG_RED=''; _LOG_YEL=''; _LOG_GRN=''; _LOG_RST=''
if [[ -t 2 ]]; then
    _LOG_RED='\033[0;31m'
    _LOG_YEL='\033[0;33m'
    _LOG_GRN='\033[0;32m'
    _LOG_RST='\033[0m'
fi

_log::_print() {
    local level="$1" colour="$2"; shift 2
    printf "%b[%s]%b %s  %s\n" \
        "$colour" "$level" "$_LOG_RST" \
        "$(date '+%H:%M:%S')" "$*" >&2
}

log::info()  { _log::_print 'INFO ' "$_LOG_GRN" "$@"; }
log::warn()  { _log::_print 'WARN ' "$_LOG_YEL" "$@"; }
log::error() { _log::_print 'ERROR' "$_LOG_RED" "$@"; return 1; }
log::fatal() { _log::_print 'FATAL' "$_LOG_RED" "$@"; exit 1; }

ファイルシステムヘルパーライブラリ

ファイルやディレクトリを操作するスクリプトでは、同じ防御的なチェックを繰り返し行うことがよくあります。そのディレクトリは存在するか、そのパスは書き込み可能か、重要なものを削除しようとしていないか、といったチェックです。

これらのチェックを lib/fs.sh に集約すると、それを利用するすべてのスクリプトの安全性と可読性が向上します。各関数が失敗時に exit ではなく return 1 を使っている点に注目してください。これにより、呼び出し元がエラーを適切に処理できます。

#!/usr/bin/env bash
# lib/fs.sh — Filesystem helper library

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

# Ensure a directory exists; create it if not
fs::require_dir() {
    local dir="$1"
    if [[ ! -d "$dir" ]]; then
        mkdir -p "$dir" || { echo "[fs] Cannot create directory: $dir" >&2; return 1; }
    fi
}

# Remove a file only if it exists (no error on missing)
fs::safe_rm() {
    local target="$1"
    [[ -e "$target" ]] && rm -rf -- "$target"
    return 0
}

# Assert that a file exists and is readable
fs::require_file() {
    local file="$1"
    [[ -f "$file" && -r "$file" ]] || {
        echo "[fs] Required file missing or unreadable: $file" >&2
        return 1
    }
}

定数によるライブラリのバージョン管理

ライブラリを複数のプロジェクトで共有したり、チームに配布したりする場合、実行時にどのバージョンのライブラリが読み込まれているかを把握することが重要になります。簡単な慣例として、各ライブラリからバージョン定数をエクスポートします。

これにより、呼び出し元は起動時に最低限必要なバージョンを確認でき、後になって原因不明の問題を調査する前に、不一致を早期に検出できます。ガード変数をバージョン文字列としても使うことで、1つの変数に2つの役割を持たせています。

#!/usr/bin/env bash
# lib/str.sh — versioned example

# Guard doubles as the version identifier
[[ -n "${_LIB_STR_LOADED:-}" ]] && return 0
readonly _LIB_STR_LOADED='1.3.0'

# Caller can validate the version
str::version() { echo "$_LIB_STR_LOADED"; }

# ---- Utility functions ----
str::trim() {
    local s="$1"
    s="${s#"${s%%[![:space:]]*}"}"
    s="${s%"${s##*[![:space:]]}"}"  
    echo "$s"
}

str::repeat() {
    local str="$1" count="$2" result=''
    for (( i=0; i<count; i++ )); do result+="$str"; done
    echo "$result"
}

自己完結型デモ:複数のライブラリを使う

この場面では、2つのライブラリを読み込み、それぞれの関数を使用する現実的なスクリプトを示します。メインスクリプトがすっきりしている点に注目してください。メインスクリプトは意図を表現し、実装の詳細はすべてライブラリに置かれています。

このスクリプトは、一時ファイルに書き出すヒアドキュメントを使ってライブラリをインラインで定義しているため、単独のファイルとして実行できます。実際のプロジェクトでは、各ライブラリを lib/ 配下の独立したファイルに配置します。

#!/usr/bin/env bash
# Standalone demo: inline libs written to /tmp, then sourced
set -euo pipefail

# --- Create a temporary lib/log.sh ---
TMPDIR_LIBS="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_LIBS"' EXIT

cat > "${TMPDIR_LIBS}/log.sh" <<'LIBEOF'
[[ -n "${_LIB_LOG_LOADED:-}" ]] && return 0
_LIB_LOG_LOADED=1
log::info()  { echo "[INFO]  $*"; }
log::error() { echo "[ERROR] $*" >&2; return 1; }
LIBEOF

cat > "${TMPDIR_LIBS}/str.sh" <<'LIBEOF'
[[ -n "${_LIB_STR_LOADED:-}" ]] && return 0
_LIB_STR_LOADED=1
str::upper() { echo "${1^^}"; }
str::trim()  { local s="$1"; s="${s#"${s%%[![:space:]]*}"}";
               s="${s%"${s##*[![:space:]]}"}"  ; echo "$s"; }
LIBEOF

# --- Source both libraries ---
source "${TMPDIR_LIBS}/log.sh"
source "${TMPDIR_LIBS}/str.sh"

# --- Main logic ---
log::info "Libraries loaded successfully."
raw_input="   hello from bash libraries   "
trimmed="$(str::trim "$raw_input")"
log::info "Trimmed: '${trimmed}'"
log::info "Uppercased: '$(str::upper "$trimmed")'"

ベストプラクティスとよくある落とし穴

チームで使うライブラリを公開する前に、次のチェックリストを確認してください。

  • インクルードガード — すべてのライブラリに必ず用意し、ガード変数名を先頭に記載してください
  • トップレベルで副作用を起こさない — 関数本体の外で cd、echo を実行したり、グローバル状態を変更したりしないでください
  • 関数内のすべての変数に local を使う — local がないと、すべての代入が呼び出し元のスコープに漏れ出します
  • 終了には return を使い、exit は使わない — source されたファイル内の exit は、呼び出し元のシェル全体を終了させます
  • 入力を検証する — 必須の引数を確認し、指定されていない場合は意味のあるエラーコードを返してください
  • コメントでドキュメント化する — 各関数の処理内容、パラメーター、戻り値を説明してください
  • ライブラリファイル内で set -e を避ける — 呼び出し元が独自のエラー処理方針を持っている可能性があるため、判断は呼び出し元に委ねてください

理解度チェック:インクルードガード

main.sh が lib/bootstrap.sh と lib/log.sh の両方を source し、さらに lib/bootstrap.sh も内部で lib/log.sh を source しているプロジェクトについて考えてみましょう。このとき、lib/log.sh のインクルードガードの主な目的は何でしょうか。

レッスンのまとめ:正しい Bash ライブラリ

再利用可能な Bash ライブラリを構築し、利用するための基本的なテクニックをすべて学びました。重要なポイントをまとめます。

  • source または . で読み込む — ファイルを現在のシェルに読み込み、その関数をすぐに利用できるようにします
  • $BASH_SOURCE を使う — 呼び出し元のスクリプトを基準にライブラリのパスを解決し、プロジェクトの移植性を保ちます
  • インクルードガード([[ -n "${_GUARD:-}" ]] && return 0)により、複数のファイルが同じライブラリを source した場合の定義の重複を防ぎます
  • 名前空間プレフィックス(log::、str::、fs::)により、ライブラリ間の関数名の衝突を防ぎます
  • lib/ ディレクトリに、単一責任に集中したファイルを配置すると、大規模なプロジェクトを保守しやすくなります
  • ブートストラップローダー(lib/bootstrap.sh)により、すべてのスクリプトから1回の source 呼び出しでエコシステム全体を読み込めます
  • ライブラリファイルでは exit やトップレベルの副作用を決して使わない — そこに置くのは関数定義と定数だけにします

これらのパターンを一貫して適用すれば、シェルスクリプトも高級言語で書かれたコードと同じように、モジュール性と保守性に優れたものになります。

よくある質問

「再利用可能なBashライブラリの構築と読み込み」レッスンは無料ですか?

はい。「再利用可能なBashライブラリの構築と読み込み」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、DevOps Bootcampコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 DevOps Bootcampコースには全4レッスンが含まれています。

「再利用可能なBashライブラリの構築と読み込み」で何を学びますか?

共有ヘルパーを、インクルードガードと名前空間付き関数プレフィックスを備えたsource可能な.shライブラリファイルに整理します。 ブラウザで直接実行するハンズオンコードでDevOps Bootcampを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

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

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

「再利用可能なBashライブラリの構築と読み込み」レッスンにはどのくらい時間がかかりますか?

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

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

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

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

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