echoとprintfとエスケープシーケンスと

By びるず (@bills-appworks.blue)
Published:

echoとprintfとエスケープシーケンスと

私はbsky-sh-cli (Bluesky in the shell)というシェルスクリプト実装のBlueskyクライアントCLI(Command Line Interface)を開発しています。 この記事では、その開発過程での愚行を晒してみたいと思います。

シェルスクリプトとエスケープシーケンス

素のechoコマンドや各種(文字列)展開では、エスケープシーケンスが解釈されて展開されます。 たとえば\nという文字列は改行コード(Unix系では0x0A)になります。 シェルスクリプト実装においては、意図しないエスケープシーケンス展開が行われないよう、留意する必要があります。

bsky-sh-cli実装(初期)におけるエスケープシーケンス対応

bsky-sh-cli実装では、関数呼び出しで多少の構造化をしています。シェルスクリプトの関数呼び出しにおいて戻り値を実装する場合、ひとつの方法として関数内で文字列を標準出力して呼び出し元ではコマンド置換を用いて文字列を参照する方法があります。

#!/bin/sh
functionA()
{
  echo 'ABC'
}

# 変数varAにfunctionAの出力文字列'ABC'を格納
varA=`functionA`
この場合、functionAのechoで出力している文字列にバックスラッシュ(\)が含まれていると、エスケープシーケンスとして解釈・展開されるため、呼び出し元では意図しない文字列になります。

実装ではこれを防ぐため、以下のような感じでecho出力をsedでバックスラッシュを重ねてエスケープし、呼び出し元ではエスケープシーケンスが展開されても意図する文字列となるように対処していました。

echo $var | sed 's/\\/\\\\/g'
エスケープシーケンス展開が1回程度ならばなんとかなりますが、関数呼び出しを何段階もしていたり、実装によってはさらに各種解釈・展開が発生するため、それに耐えられるように場当たり的にエスケープを重ねることをしていました。ソース管理をGitに移行してからのコードには残っていないと思いますが、手元のメモでは以下のようなおぞましいコード片があります。
RESULT=`echo "${TARGET}" | sed 's/\\\\\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\/g'`
繰り返し指定子も使っておらずバックスラッシュが何個あるのか数えたくもない呪物です(今も都合により場当たり的なエスケープ処理がありますが、ここまででは無い)。

なおechoコマンドでは-Eオプションによりエスケープシーケンス展開を抑制できますが、処理系によって使えない場合があり、なるべく多くの環境(可能であれば素のBourne Shellでも)での動作を目標としているbsky-sh-cliでは採用しませんでした。

printfへの移行

そのうちprintfコマンドではエスケープシーケンス展開が行われないようにすることが、処理系をあまり限定せず実装できそうなことに薄々気がつきましたが、

等の理由で渋っていました。

しかし開発を進めるうちエスケープ地獄に耐えられなくなり、意を決してechoからprintfへの移行リファクタリングを決行。エスケープ地獄から(地獄レベルからは)抜け出せたのでした。

今後

現在でも、Bluesky/ATProto API呼び出しでJSON文字列をハンドリングするため、ダブルクォート・バックスラッシュ・改行等のエスケープ/アンエスケープが必要な場面はあり、エスケープ処理が全く無くなったわけではありません。

echoコマンドによる実装もほぼ排除しましたが、将来的に色出力等の画面出力装飾も考えているので、特にポストテキスト以外のメタ項目の出力最終段はechoコマンドに戻るでしょう。

また、開発初期においてパラメタ指定のポストテキストをパラメタ解釈の早い段階でエスケープする実装にしていたため、現在対応したファイル指定・標準入力テキストとの共通処理で食い合わせが悪く、リファクタリングが必要となっています。コードがこれ以上膨らまないうちにやればいいのですが、全機能に影響が及ぶ苦労や機能追加をしたい欲求と板挟みに。

またどうせJSONハンドリングにjqコマンドを使用しているので、jqに頼る方法もあるのですが、極端な話ほとんどのロジックをjqフィルタに持っていくことも考えられ、「シェルスクリプト実装とは」という謎のこだわりとの闘いともなっています。