exec コマンドに --raw オプションを追加し、生バイナリ出力を正しく扱えるようにした

basecamp/kamal

kamal accessory execkamel app execkamal server exec が標準出力を SSHKit の capture で取得するため、バイナリデータの末尾の空白や NUL バイトが除去され、データが破損していました。本 PR は --raw オプション を導入し、出力を加工せずそのまま STDOUT に流すことで、バイナリパイプラインを安全に利用できるようにします。

背景

kamal accessory exec がバイナリデータを扱う際に、capture が改行や NUL バイトをトリムしてしまい、PostgreSQL のカスタムアーカイブや tar アーカイブがサイズ減少し復元に失敗する問題が報告されました(#1861)。この現象は標準出力の整形処理がバイナリに不向きであることが根本原因です。バイナリデータをそのまま転送できるオプションが必要と判断され、--raw が実装されました。

技術的な変更

CLI 定義へのオプション追加

lib/kamal/cli/accessory.rblib/kamal/cli/app.rblib/kamal/cli/server.rb の各 exec コマンドに以下のオプション定義が追記されました。

option :raw, type: :boolean, default: false, desc: "Output raw, unmodified stdout"

このオプションは --interactive(および --detach)と同時に指定できないよう、互換性チェックで排除されています。

with_raw_output ヘルパーの導入

lib/kamal/cli/base.rbwith_raw_output メソッドが追加され、--raw が有効な場合は一時的にログレベルを :error に下げ、コマンド本体の出力だけを残すようにしました。また say メソッドは options[:raw] が有効なときは出力しないよう条件分岐が加えられています。

def with_raw_output(raw, &block)
  raw ? KAMAL.with_verbosity(:error, &block) : block.call
end

def say(message = "", * )
  super unless options[:raw]
  KAMAL.log(message.to_s)
end

puts_by_host のバイナリ対応

lib/kamal/sshkit_with_ext.rbputs_by_hostraw: 引数を受け取り、raw が true のときは $stdout.binmode でバイナリモードに切り替えて出力をそのまま書き込みます。従来のホスト情報プレフィックスや余分な改行は付加されません。

def puts_by_host(host, output, type: "App", quiet: false, raw: false)
  if raw
    $stdout.binmode
    $stdout.write(output)
  else
    unless quiet
      puts "#{type} Host: #{host}"
    end
    puts "#{output}\n\n"
  end
end

exec 実装へのフラグ反映

exec メソッドは raw = options[:raw] を取得し、with_raw_output(raw) do … end でラップされました。capture_with_info の呼び出し時に strip: !raw を渡すことで、--raw 時は出力トリミングを抑制しています。また puts_by_hostraw: raw を渡してバイナリ出力モードへ遷移させています。

with_raw_output(raw) do
  pre_connect_if_required
  cmd = Kamal::Utils.join_commands(cmd)
  hosts = KAMAL.hosts
  quiet = options[:quiet]
  on(hosts) do |host|
    execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{host}"), verbosity: :debug
    puts_by_host host, capture_with_info(cmd, strip: !raw), quiet: quiet, raw: raw
  end
end

テストの追加

test/cli/server_test.rbtest/integration/app_test.rb--raw の挙動検証が追加され、バイナリデータがそのまま出力されること、--interactive との併用が例外を投げることが自動テストで保証されています。

設計判断

後方互換性の確保

既存の exec 呼び出しはデフォルトで --raw が無効なため、従来通りの文字列整形ロジックが保持されます。新しいフラグは既存オプションと衝突しないよう option :raw を追加し、互換性チェックで --interactive/--detach と排除しています。これにより既存ユーザーへの影響をゼロにしつつ機能拡張が実現されています。

ロギングレベルの調整

with_raw_output がログレベルを :error に一時的に下げる設計は、バイナリデータがログ文字列と混ざり合うリスクを回避しつつ、非 --raw 実行では従来通りの詳細ログを維持するというトレードオフを選択しています。

最小侵入の実装

変更は主にオプション定義・ヘルパーメソッド・呼び出し側のラップに留まり、ビジネスロジック自体は変更していません。この点がコードベース全体への影響を最小化し、保守性を保ったまま新機能を提供できた理由です。

まとめ

--raw オプションの導入により、kamal exec 系コマンドはバイナリデータをそのままパイプできるようになり、Issue #1861 で報告されたデータ破損が解消されました。実装は後方互換と最小侵入を意識した設計判断に基づき、オプション追加・ログレベル抑制・出力メソッド拡張という三層で実装されています。テストケースも充実しており、今後のバイナリ操作シナリオでも安全に利用できる基盤が整いました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
7f2a85eb

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景、技術的変更、設計判断(任意)、まとめが揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックは正しい `言語:ファイルパス` 形式で記述され、GitHubリンクは `[#番号](URL)` 形式で正しくリンク化されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

エンジニア向けの専門的な内容で、余計な基礎説明はなく適切です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論→各論→結論のパラグラフ構成で、トピックセンテンスが先頭にあり、1段落1トピック・長さも適切です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のコードスニペットは提供されたDiffと完全に一致し、変更点・追加点が正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

用語はPRで使用されたものと一致し、誤用や不適切な表現はありません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的説明はDiffとPR Descriptionに基づいており、根拠が明確で誤りはありません。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

すべての主張はPRタイトル・説明・Diffに裏付けられており、捏造や推測はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号 #1876、Issue番号 #1861 など数値情報は正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルはPRの意図(--raw オプション追加)を正確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事はPR情報以外の外部知識(バージョンサポート等)を含んでいません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現に関する記載はなく、PRの記述と矛盾はありません。