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

Bashのコマンド置換(Command Substitution)徹底解説

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はサブシェルと共に消滅したため)

改行の扱いとダブルクォート

ここが最大のハマりポイントです。

  1. 末尾の改行: コマンド置換は、出力の末尾にある改行をすべて削除します。
  2. 途中の改行: ダブルクォートで囲まないと、途中の改行がスペースに変換されてしまいます(単語分割の影響)。

比較実験: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:標準エラー「だけ」を拾う

リダイレクトの順序が重要です。

  1. まず 2>&1 で、「エラー(2)の行き先を、現在の出力(1=画面)と同じにする」と設定します。
  2. 次に >/dev/null で、「出力(1)の行き先をゴミ箱にする」と設定します。
  3. このとき、エラー(2)の行き先は「1番(画面)」に固定されたまま残るため、結果としてエラーメッセージだけがコマンド置換のキャプチャ対象(標準出力扱い)として残ります。
# Bash
result=$(ls non_existent_file 2>&1 >/dev/null)
echo "変数の中身: [$result]"
# 出力
# 変数の中身: [ls: 'non_existent_file' にアクセスできません: そのようなファイルやディレクトリはありません]

展開の優先順位

Bashは以下の順序で展開を行いますが、この順序を理解していないと、思わぬバグが発生する可能性がありますので気をつけましょう。

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

ブレース展開はコマンド置換よりも先に実行されるため、以下のような書き方は意図通りに動きません。

# 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スクリプトは単なる命令の羅列から、状況に応じて判断を変える「動的なプログラム」へと進化します。

  1. $( ) を優先して使う
  2. 改行を守りたいなら必ず " " で囲む
  3. サブシェル内での変更は外に届かないことを忘れない

この3点を意識して、より堅牢なシェルスクリプトを書いていきましょう!