スタートアップCTOによるITエンジニアのためのブログ

Bashのパラメータ展開(変数展開)をマスターしよう

シェルスクリプトを書いていると、文字列の切り出しや置換のために sedcut を多用しがちですが、実はBashの標準機能であるパラメータ展開(変数展開とも呼ばれる)だけで完結できることが多々あります。

本記事では、${}で表現されるパラメータ展開の仕組みや様々な機能を、実用的なコード例とともに解説します。

目次

  • 概要
  • パラメータと変数の関係
  • メリット
  • 基本:変数代入とクォートのルール
  • 単純参照
  • パラメータの状態に応じたデフォルト値の設定
  • 文字列の切り出し(スライス)
  • 変数一覧
  • 配列キーの一覧
  • 文字列の長さの取得
  • 置換
  • 大文字・小文字変換
  • パラメータ変換
  • 入れ子(ネスト)変数の間接展開
  • 展開の優先順位
  • ベストプラクティス
  • まとめ

概要

Bashにおけるパラメータ展開(Parameter Expansion)とは、変数や特殊パラメータに格納された値を参照する際、その値を操作・加工して取り出す仕組みのことです。外部コマンドを呼び出さずにシェル内部(ビルドイン)で処理を完結できるため、非常に高速です。

パラメータと変数の関係

パラメータ展開は「変数展開(Variable Expansion)」と呼ばれることもありますが、Bashの定義では以下のような階層構造になっています。

  • パラメータ (Parameter): 値を保持するエンティティの総称。
    • 変数 (Variable): 名前(英数字やアンダースコア)で識別されるパラメータ。
    • 特殊パラメータ: $ , ? , * など、ユーザーが直接代入できない記号。
    • 位置パラメータ: $1, $2 など、引数を表す数字。

つまり、「変数はパラメータの一種」であり、${} を使った操作はすべて「パラメータ展開」と呼ぶのが正確です。

メリット

なぜ外部コマンドではなく、パラメータ展開を使うべきなのでしょうか?

  • パフォーマンス: sedcut は「サブプロセスの生成」を伴いますが、パラメータ展開はシェル内部で処理されるため圧倒的に高速です。
  • 依存性の低さ: 外部コマンドのパスやバージョンの違いに左右されません。
  • 簡潔さ: パイプで繋ぐ必要がなく、一行で記述できます。
処理内容パラメータ展開 (高速)外部コマンド (低速/オーバーヘッドあり)
文字列の長さ${#param}echo -n "${text}" | wc -m
部分抽出${param:0:5}echo "${param}" | cut -c 1-6
置換${param/old/new}echo "${param}" | sed 's/old/new/'
大文字化${param^^}echo "${param}" | tr '[:lower:]' '[:upper:]'

基本:変数代入とクォートのルール

変数代入のルール

  • 使用可能文字: 英数字およびアンダースコア (_)。
  • 先頭文字: 数字から始めることはできません。
  • 代入時の注意: = の前後にスペースを入れないでください。
# Bash
# 正解
my_var="Hello"

# 不正解
my-var="Hello"  # ハイフン不可
2var="Hello"    # 数字スタート不可
var = "Hello"   # スペース不可

クォートの使い分け

クォーテーションの種類の違いにより、パラメータ展開されるかどうかが決定されます。

  • シングルクォート (' '): 中身をすべて「ただの文字列」として扱います。展開は行われません。
  • ダブルクォート (" "): $`\ などの特殊記号を解釈し、展開を実行します。
# Bash
name="Bash"
echo 'Hello $name' # 結果: Hello $name
echo "Hello $name" # 結果: Hello Bash

単純参照

パラメータに格納されている値を加工なしで参照する、最も基本的な形です。$parameter と書くこともできますが、パラメータの直後に文字が続く場合は ${parameter} のようにブレース(波括弧)で囲む必要があります。

# Bash
unit="kilo"
echo "${unit}gram" # 結果: kilogram
# echo "$unitgram" だと変数 $unitgram を探してしまい失敗する

パラメータの状態に応じたデフォルト値の設定

パラメータが「未設定(unset parameter)」または「空文字(parameter=parameter="")」の場合の挙動を制御できます。

コロン(:)を付けると「パラメータが未設定 または 空文字」かを判定します。コロンを抜くと「未設定」かどうかを判定します。

構文未設定時の挙動空文字時の挙動設定済みの時の挙動
${var:-word}wordを返すwordを返すvarの値を返す
${var-word}wordを返す空文字を返すvarの値を返す
${var:=word}wordを代入して返すwordを代入して返すvarの値を返す
${var=word}wordを代入して返す空文字を返すvarの値を返す
${var:?word}エラー終了エラー終了varの値を返す
${var?word}エラー終了空文字を返すvarの値を返す
${var:+word}何もしない何もしないwordを返す
${var+word}何もしないwordを返すwordを返す
# Bash
unset user_name
echo "${user_name:-Guest}" # 結果: Guest

# sedなどを使う場合と比較
echo "$user_name" | sed 's/^$/Guest/'

文字列の切り出し(スライス)

${parameter:offset:length} の形式で、offsetで指定した位置からlength分の文字を抽出します。lengthを指定しない場合は最後まで抽出します。

# Bash
str="BashScripting"
echo "${str:4}"   
# 結果: Scripting (4文字目以降すべて)

echo "${str:4:6}"
# 結果: Script    (4文字目から6文字分)

# マイナスoffsetはデフォルト値:-と区別するため()が必要
echo "${str:(-9)}"
# 結果: Scripting (後ろから9文字目から最後まで)

# cutコマンドを使う場合
echo "BashScripting" | cut -c 5-10

変数一覧

特定のプレフィックスを持つ変数を一覧表示します。

  • ${!prefix*}: 結果が1つの引数になります。
  • ${!prefix@}: 結果が複数の引数になります。
# Bash
CONF_DB="mydb"
CONF_PORT="5432"
printf "%s\n" "${!CONF_*}"
# 結果
# CONF_DB CONF_PORT

printf "%s\n" "${!CONF_@}"
# 結果
# CONF_DB
# CONF_PORT

配列キーの一覧

配列キーの一覧を取得します。

  • ${!name[*]}: キーの一覧を1つの引数として結合します。
  • ${!name[@]}: キーの一覧を複数の引数として扱います。
# Bash
declare -A colors=([red]="#ff0000" [green]="#00ff00")
printf "%s\n" "${!colors[*]}"
# 結果
# red green

printf "%s\n" "${!colors[@]}"
# 結果
# red
# green

文字列の長さの取得

${#parameter} で文字列の長さ(文字数)を取得できます。

# Bash
text="Hello"
echo "${#text}" # 結果: 5

# wcを使う場合 (遅い)
echo -n "$text" | wc -m

置換(Pattern Substitution)

${parameter/pattern/string} を使用します。

  • / : 最初の1つだけ置換
  • // : すべて置換
  • /# : 前方一致
  • /% : 後方一致

# Bash
path="/usr/bin/python"
echo "${path/python/ruby}"   # /usr/bin/ruby
echo "${path//\//_}"         # _usr_bin_python (全ての / を _ に)

# sedを使う場合との比較
echo "$path" | sed 's/python/ruby/'

ここで使われるパターンは正規表現ではなく、ファイルパス展開で使われるGlobbingです。

大文字・小文字変換

Bash 4.0以降で利用可能です。

  • ${parameter^} : 最初の1文字を大文字に
  • ${parameter^^} : すべて大文字に
  • ${parameter,} : 最初の1文字を小文字に
  • ${parameter,,} : すべて小文字に
# Bash
title="bash tips"
echo "${title^^}" # BASH TIPS

# trを使う場合よりも簡潔です
echo "$title" | tr '[:lower:]' '[:upper:]'

パラメータ変換(Bash 4.4以降)

${parameter@operator} の形式で特殊な変換を行えます。

  • Q: 値をクォートした形式に変換(再利用時に安全)。
  • P: プロンプト文字列として展開。
# Bash
input="It's a pen"
echo "${input@Q}" # 'It'\''s a pen'

入れ子(ネスト)変数の間接展開(indirect Expansion)

${!var} を使うと、変数の値を変数名として参照できます。

# Bash
target="real_value"
pointer="target"
echo "${!pointer}" # 結果: real_value

eval を使っても間接参照は可能ですが、任意のコードが実行されてしまう脆弱性(コードインジェクション)のリスクがあるため、極力避け、${!var} を使いましょう。

展開の優先順位

Bashは以下の順序で展開を行います。チルダからプロセス置換までは一つのフェーズとして、左から右へ出現順に処理されます。

  1. ブレース展開 {a,b}
  2. 一括スキャン(左から右へ順次実行):
  3. 単語分割 (Word Splitting)
  4. パス名展開(ワイルドカード) *

ベストプラクティス

  1. ブレース {} は常に付ける: 前述の${var}gram のように境界を明確にする癖をつけると、変数名の誤認ミスを防げます。
  2. ダブルクォートで囲む: スペースを含む値が分割されるのを防ぐため、原則 "${var}" と書くのが鉄則です。
# Bash
file="my photo.jpg"
# NG: スペースで分割され、ls: 'my' と 'photo.jpg' を探してしまう
ls -l ${file} 
# OK: 1つの引数として正しく渡される
ls -l "${file}"

まとめ

パラメータ展開をマスターすれば、スクリプトはより高速に、そして簡潔になります。まずは、普段書いているスクリプトの sedcut を、パラメータ展開に置き換えるところから始めてみてください。