TL;DR
macOS には GNU sed ではなく BSD sed が同梱されています。BSD sed では -i の後に明示的なバックアップ拡張子が必要です。バックアップを作成しない場合は空文字列を渡してください:
# macOS の修正: -i の後に '' を追加
sed -i '' 's/foo/bar/g' filename.txt
または Homebrew 経由で GNU sed をインストールし、すでに慣れている Linux の構文で gsed を使用してください。
エラーの全文
sed: 1: "filename": extra characters at the end of L command
Linux では完璧に動作する sed -i コマンドを Mac で実行すると失敗します。エラーには「L コマンド」が言及されていますが、実際には使用していません。わかりにくいメッセージですが、sed に互換性のない2つの系統が存在することを知れば、根本原因は理解できます。
根本原因
GNU sed と BSD sed では -i フラグの扱いが異なります:
- GNU sed(Linux、多くの CI サーバー):
-iはオプションのサフィックスを受け付けます。sed -i 's/…/…/' fileと書くと、GNU sed はサフィックスが省略されたと判断し、バックアップなしでインプレース編集を行います。 - BSD sed(macOS 標準):
-iには常にサフィックスが必要です。sed -i 's/…/…/' fileと書くと、BSD sed は置換パターンをバックアップ拡張子として解釈します。そしてターゲットファイルを開き、その内容を sed スクリプトとして実行しようとします。ファイルの先頭の文字が sed コマンドとして誤読され、しばしば BSD sed が後ろにゴミのあるl(小文字のL)コマンドと解釈するものに行き着きます。
あの奇妙な「extra characters at the end of L command」というメッセージの意味は:ファイルを sed スクリプトとして開いたが、解析できなかったということです。Apple はすべての macOS バージョンに BSD sed を同梱しており、GNU sed をデフォルトで含めたことは一度もありません。
使用しているバージョンを確認するには:
sed --version 2>&1 | head -1
# GNU: "sed (GNU sed) 4.x" と表示される
# macOS BSD: "sed: illegal option -- -" と表示されるか、非ゼロで終了する
修正方法 1 — -i の後に空文字列を追加する(macOS ネイティブ)
BSD sed にはサフィックス引数が必要です。空文字列 '' を渡すことで、バックアップなしのインプレース編集を指定できます:
# 修正前(macOS では失敗する)
sed -i 's/old/new/g' config.txt
# 修正後(macOS で動作する)
sed -i '' 's/old/new/g' config.txt
空文字列は独立したトークンである必要があります。-i にクォートをくっつけると、シェルによって予測不能な結果になります:
# 誤り — シェルによって動作が異なる
sed -i'' 's/old/new/g' config.txt
# 正しい — '' の前に明示的なスペースを入れる
sed -i '' 's/old/new/g' config.txt
編集前にバックアップを残したい場合は、空文字列の代わりに拡張子を渡してください:
# config.txt を変更する前に config.txt.bak を作成する
sed -i '.bak' 's/old/new/g' config.txt
修正方法 2 — Homebrew 経由で GNU sed をインストールする
Linux スクリプトを macOS 向けに何度も修正するのは手間がかかります。GNU sed をインストールすれば、両方のプラットフォームで構文を統一できます:
brew install gnu-sed
Homebrew はシステムのバイナリを上書きしないよう gsed としてインストールします。直接使用するには:
gsed -i 's/old/new/g' config.txt
普通に sed と打ちたい場合は、~/.zshrc または ~/.bashrc の PATH に GNU ツールのディレクトリを追加してください:
export PATH="$(brew --prefix)/opt/gnu-sed/libexec/gnubin:$PATH"
プロファイルを再読み込みすると、sed --version が GNU バージョンを表示します。それ以降は Linux のスクリプトをそのまま実行できます。
修正方法 3 — クロスプラットフォームなスクリプトを書く
Homebrew が使えない場合は、実行時に OS を検出してください。ラッパー関数を使えば複数引数の呼び出しをきれいに処理でき、変数にコマンドを格納する際のクォートの落とし穴も回避できます:
#!/usr/bin/env bash
sed_inplace() {
if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' "$@"
else
sed -i "$@"
fi
}
sed_inplace 's/version=.*/version=2.0/' package.properties
このパターンは Makefile、デプロイスクリプト、CI の YAML など、どのプラットフォームで実行されるかを保証できない場所でも使用できます。
確認
修正を適用した後、編集が正しく反映されているか確認してください:
# 修正済みコマンドを実行する
sed -i '' 's/foo/bar/g' test.txt
# 結果を確認する
grep 'bar' test.txt
# 意図しないバックアップファイルが作成されていないか確認する
ls -la test.txt*
置換結果が grep の出力に表示され、不要なバックアップファイルがなければ(意図して作成した場合を除き)完了です。
よくある追加の問題
- **スクリプトがローカルでは動作するが CI で失敗する:**多くの CI コンテナ(GitHub Actions、CircleCI)は GNU sed を搭載した Linux で動作します。上記の
sed_inplaceラッパーを追加して、両方の環境で同期を保ってください。 - **sed -i '' がシェルでは動作するが Makefile では動作しない:**Make のデフォルトシェルは
/bin/shで、クォートの扱いが異なります。Makefile の先頭にSHELL := /bin/bashを追加するか、丁寧にエスケープしてください:sed -i $'\'' $'\'' 's/…/…/' file - Python や Ruby がサブプロセスを呼び出す場合:
-iと''を1つの文字列としてではなく、リストの別々の要素として渡してください。subprocess.run(['sed', '-i', '', 's/a/b/', 'file'])は動作しますが、1つの要素にまとめると動作しません。

