シェルスクリプトを書いていると、文字列の切り出しや置換のために sed や cut を多用しがちですが、実はBashの標準機能であるパラメータ展開(変数展開とも呼ばれる)だけで完結できることが多々あります。
本記事では、${}で表現されるパラメータ展開の仕組みや様々な機能を、実用的なコード例とともに解説します。
目次
- 概要
- パラメータと変数の関係
- メリット
- 基本:変数代入とクォートのルール
- 単純参照
- パラメータの状態に応じたデフォルト値の設定
- 文字列の切り出し(スライス)
- 変数一覧
- 配列キーの一覧
- 文字列の長さの取得
- 置換
- 大文字・小文字変換
- パラメータ変換
- 入れ子(ネスト)変数の間接展開
- 展開の優先順位
- ベストプラクティス
- まとめ
概要
Bashにおけるパラメータ展開(Parameter Expansion)とは、変数や特殊パラメータに格納された値を参照する際、その値を操作・加工して取り出す仕組みのことです。外部コマンドを呼び出さずにシェル内部(ビルドイン)で処理を完結できるため、非常に高速です。
パラメータと変数の関係
パラメータ展開は「変数展開(Variable Expansion)」と呼ばれることもありますが、Bashの定義では以下のような階層構造になっています。
- パラメータ (Parameter): 値を保持するエンティティの総称。
- 変数 (Variable): 名前(英数字やアンダースコア)で識別されるパラメータ。
- 特殊パラメータ:
$,?,*など、ユーザーが直接代入できない記号。 - 位置パラメータ:
$1,$2など、引数を表す数字。
つまり、「変数はパラメータの一種」であり、${} を使った操作はすべて「パラメータ展開」と呼ぶのが正確です。
メリット
なぜ外部コマンドではなく、パラメータ展開を使うべきなのでしょうか?
- パフォーマンス:
sedやcutは「サブプロセスの生成」を伴いますが、パラメータ展開はシェル内部で処理されるため圧倒的に高速です。 - 依存性の低さ: 外部コマンドのパスやバージョンの違いに左右されません。
- 簡潔さ: パイプで繋ぐ必要がなく、一行で記述できます。
| 処理内容 | パラメータ展開 (高速) | 外部コマンド (低速/オーバーヘッドあり) |
| 文字列の長さ | ${#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は以下の順序で展開を行います。チルダからプロセス置換までは一つのフェーズとして、左から右へ出現順に処理されます。
- ブレース展開
{a,b} - 一括スキャン(左から右へ順次実行):
- 単語分割 (Word Splitting)
- パス名展開(ワイルドカード)
*
ベストプラクティス
- ブレース
{}は常に付ける: 前述の${var}gramのように境界を明確にする癖をつけると、変数名の誤認ミスを防げます。 - ダブルクォートで囲む: スペースを含む値が分割されるのを防ぐため、原則
"${var}"と書くのが鉄則です。
# Bash
file="my photo.jpg"
# NG: スペースで分割され、ls: 'my' と 'photo.jpg' を探してしまう
ls -l ${file}
# OK: 1つの引数として正しく渡される
ls -l "${file}"
まとめ
パラメータ展開をマスターすれば、スクリプトはより高速に、そして簡潔になります。まずは、普段書いているスクリプトの sed や cut を、パラメータ展開に置き換えるところから始めてみてください。
