Bashを使いこなす上で避けて通れない、かつ最も強力な機能の一つが「コマンド置換(Command Substitution)」です。
コマンド置換はコード上では $(command) と表現され、「コマンドの結果をそのまま変数に入れたい」「別のコマンドの引数として使い回したい」といった自動化の要望をスマートに解決してくれます。
本記事では、Bash初級者から中級者へステップアップするために必要な、コマンド置換の仕組みと落とし穴を徹底解説します。
目次
- 概要
- メリット
- 2種類の構文
- 主な活用シーン
- 変数への代入
- コマンドの引数として利用
- 計算や文字列操作
- サブシェルでの環境変数の扱い
- 改行の扱いとダブルクォート
- 標準エラーの扱いと拾い方
- 展開の優先順位
- $(cat file)と$(< file)の違い
- ヌルバイト警告への対応
- まとめ
概要
コマンド置換とは、「コマンドの実行結果(標準出力)を、そのコマンドが書かれていた場所に置き換える」仕組みです。
例えば、echo "本日の日付は$(date)です" というコードの場合、シェルはまず内側の date コマンドを実行します。その出力(例:2026年 2月 5日 ...)が元の $(date) の位置にスッポリとはまるイメージです。
メリット
- 自動化の促進: 手入力していた日付やファイル名を自動取得できる。
- コードの簡略化: 一時ファイルを作成せずに、出力をそのまま次の処理へ渡せる。
- 柔軟性: 実行環境(OSの種類やパスなど)に応じて挙動を変えるスクリプトが書ける。
2種類の構文
コマンド置換には $() と ` `(バッククォート)を使った2つの書き方がありますが、結論から言うと $() を使うべきです。その最大の理由は、入れ子(ネスト)にした時の可読性にあります。
1. $() を使った入れ子(推奨)
$() は括弧の始まりと終わりが明確なため、そのまま内側に別の $() を書くだけで動作します。
# Bash
file_type=$(file $(which python3))
2. バッククォート ` ` を使った入れ子(非推奨)
バッククォートの場合、内側のバッククォートをバックスラッシュ(\)でエスケープする必要があります。
# Bash
file_type=`file \`which python3\``
3重、4重と深くなるにつれ、エスケープの数が倍々で増えていくため、人間が管理するのは不可能に近くなります。
| 特徴 | $() (括弧形式) | ` ` (バッククォート) |
| 可読性 | 非常に高い | 低い(記号が混ざる) |
| 入れ子の書き方 | そのまま書ける | エスケープが必要 |
| モダンさ | 現在の標準(POSIX準拠) | 互換性のための旧式 |
主な活用シーン
変数への代入
# Bash
current_dir=$(pwd)
today=$(date +%Y-%m-%d)
コマンドの引数として利用
あるコマンドの出力を、別のコマンドのターゲットにします。
# Bash
# プロセスIDを特定して詳細を表示
ps -p $(pgrep -u $USER bash)
計算や文字列操作(レガシーな手法)
expr コマンドと組み合わせることで計算が可能です。
# Bash
echo "合計は $(expr 10 + 20) です"
# 出力: 合計は 30 です
# Bash
str="Hello Bash"
echo "長さは $(expr length "$str") です"
# 出力: 長さは 10 です
現代のBashでは $((10 + 20)) という「算術式展開」や、${#str}という「パラメータ展開」を使うのが一般的ですが、古いシェルスクリプトや互換性を重視する場面では今でも $(expr ...) が見られます。
サブシェルでの環境変数の扱い
コマンド置換はサブシェル(子プロセス)で実行されます。 そのため、「コマンド置換の中で定義した変数や変更した環境変数は、親シェルには反映されない」という点に注意してください。
# Bash
# これは失敗する
$(VAR=hello)
echo $VAR # 何も表示されない(VARはサブシェルと共に消滅したため)
改行の扱いとダブルクォート
ここが最大のハマりポイントです。
- 末尾の改行: コマンド置換は、出力の末尾にある改行をすべて削除します。
- 途中の改行: ダブルクォートで囲まないと、途中の改行がスペースに変換されてしまいます(単語分割の影響)。
比較実験:seq 1 3(1, 2, 3を改行区切りで出力)
- クォートなし:
echo $(seq 1 3)→1 2 3(1つのスペース区切りに変換される) - クォートあり:
echo "$(seq 1 3)"→ 改行が維持される
# Bash
# 1, 2, 3という3つのディレクトリを作成したい場合
mkdir $(seq 1 3)
# '1\n2\n3' という名前の1つのディレクトリを作ってしまう(事故の元!)
mkdir "$(seq 1 3)"
実践的な比較表
| 書き方 | シェルの解釈 | 主な用途 |
$(cmd) | 出力をスペース等で分割し、複数の引数として展開する | 複数のファイルリストをループで回すときなど |
"$(cmd)" | 出力を一つの塊(文字列)として保持する | ファイルの内容を変数に入れたり、改行を維持して表示したいとき |
標準エラーの扱いと拾い方
デフォルトでは、コマンド置換は標準出力(stdout)のみをキャプチャし、標準エラー(stderr)はそのまま画面にスルーします。
# Bash
# 存在しないファイルを ls する
# 画面にはエラーが出るが、変数 result は空になる
result=$(ls non_existent_file)
# ls: 'non_existent_file' にアクセスできません: そのようなファイルやディレクトリはありません
echo "変数の中身: [$result]"
# 出力
# 変数の中身: []
パターンA:標準出力と標準エラーを両方拾う
2>&1 を使って、エラーを標準出力に合流させます。
# Bash
# 変数にエラーメッセージが格納される
result=$(ls non_existent_file 2>&1)
echo "変数の中身: [$result]"
# 出力
# 変数の中身: [ls: 'non_existent_file' にアクセスできません: そのようなファイルやディレクトリはありません]
パターンB:標準エラー「だけ」を拾う
リダイレクトの順序が重要です。
- まず
2>&1で、「エラー(2)の行き先を、現在の出力(1=画面)と同じにする」と設定します。 - 次に
>/dev/nullで、「出力(1)の行き先をゴミ箱にする」と設定します。 - このとき、エラー(2)の行き先は「1番(画面)」に固定されたまま残るため、結果としてエラーメッセージだけがコマンド置換のキャプチャ対象(標準出力扱い)として残ります。
# Bash
result=$(ls non_existent_file 2>&1 >/dev/null)
echo "変数の中身: [$result]"
# 出力
# 変数の中身: [ls: 'non_existent_file' にアクセスできません: そのようなファイルやディレクトリはありません]
展開の優先順位
Bashは以下の順序で展開を行いますが、この順序を理解していないと、思わぬバグが発生する可能性がありますので気をつけましょう。
- ブレース展開
{a,b} - 同列順位(左から右へ順次実行):
- チルダ展開
~ - パラメータと変数展開
${var} - 算術展開
$(( )) - コマンド置換
$( ) - プロセス置換
<()
- チルダ展開
- 単語分割 (Word Splitting)
- パス名展開(ワイルドカード)
*
ブレース展開はコマンド置換よりも先に実行されるため、以下のような書き方は意図通りに動きません。
# Bash
# ファイルが3つある場合、{1..3} になってほしいが...
echo {1..$(ls | wc -l)}
# 実際の出力: {1..3}(文字列としてそのまま出力される)
$(cat file) と $(< file) の違い
ファイルの中身を読み込む際、以下の2つは同じ結果になりますが、パフォーマンスが違います。
$(cat file):catコマンド(外部プロセス)を起動する。$(< file): Bashの組み込み機能で読み込む。高速。
スクリプトの高速化を狙うなら、後者の書き方がスマートです。
# Bash
content=$(< settings.conf)
ヌルバイト警告への対応
バイナリデータ(\0を含む)をコマンド置換に入れようとすると、Bashは警告を出してヌルバイトを破棄します。
# Bash
output_with_nulls=$(cmd)
# -bash: 警告: command substitution: ignored null byte in input
テキストとして扱いたい場合は、事前に tr -d '\0' で削除しておくのが定石です。
# Bash
output_without_nulls=$(cmd | tr -d '\0')
まとめ
コマンド置換をマスターすれば、Bashスクリプトは単なる命令の羅列から、状況に応じて判断を変える「動的なプログラム」へと進化します。
$( )を優先して使う- 改行を守りたいなら必ず
" "で囲む - サブシェル内での変更は外に届かないことを忘れない
この3点を意識して、より堅牢なシェルスクリプトを書いていきましょう!
