`echo`と`printf`では制御文字の扱いが異なる件

ffmpegで気づいたこと

ffmpegでは-headersオプションを使えば複数のhttp headerを付与しつつ-iで指定したurlに向かってhttpで叩けるが,複数の内容を利用する際には改行するための制御文字を間々で認識させる必要があるらしく,その場合は$'...'で表せる.

note.kiriukun.com

そしてこの$'...'については確かにzshのmanページでも取り扱い方法が記載されていた,以下を参照のこと.

man zshmisc | col -b | expand -t 4 | pcre2grep -Moe ' {0}(QUOTING\n)( {7,}(\S* *)*\S*\n*)*'

見た感じではprintprintfc++の実装そのままで,対してechozsh用に作られた安全なものという印象を受けた.

zshで実験

準備

以下のシェルスクリプトを用いる

#!/bin/zsh -eu
# this is testZsh4

echo "all arguments : ${*}"

zparseopts -D -E -a optionArray -A optionPair test:

if ((${+optionArray})); then
    for i in $optionArray; do
        echo "echo: component: ${i}"
        printf "printf: %s\n" $i
    done
fi

if ((${+optionPair})); then
    for key in ${(@k)optionPair}; do
        echo "echo: flags: ${key} => value: ${optionPair[$key]}"
        printf "printf: flags: %s => value: %s\n" $key $optionPair[$key]
    done
fi

echo "other arguments : ${*}"

-testオプションにつける引数を変えれば処理の比較ができる

soluna-eureka.hatenablog.com

方針

改行文字を含めた文字列ab\ncdを対象に色々と変えてみよう!

ab\ncd

% testZsh4 -test ab\ncd
all arguments : -test abncd
echo: component: -test
printf: -test
echo: component: abncd
printf: abncd
echo: flags: -test => value: abncd
printf: flags: -test => value: abncd
other arguments : 

両者共に\が消えた

'ab\ncd'

% testZsh4 -test 'ab\ncd'
all arguments : -test ab
cd
echo: component: -test
printf: -test
echo: component: ab
cd

printf: ab\ncd
echo: flags: -test => value: ab
cd
printf: flags: -test => value: ab\ncd
other arguments : 

echoだけ改行文字が読まれた,printfでは文字として扱われた

"ab\ncd"

% testZsh4 -test "ab\ncd" 
all arguments : -test ab
cd
echo: component: -test
printf: -test
echo: component: ab
cd
printf: ab\ncd
echo: flags: -test => value: ab
cd
printf: flags: -test => value: ab\ncd
other arguments : 

'ab\ncd'と変わらない

$'ab\ncd'

% testZsh4 -test $'ab\ncd'
all arguments : -test ab
cd
echo: component: -test
printf: -test
echo: component: ab
cd
printf: ab
cd
echo: flags: -test => value: ab
cd
printf: flags: -test => value: ab
cd
other arguments : 

両者とも改行文字が読まれた

$"ab\ncd"

% testZsh4 -test $"ab\ncd"
all arguments : -test $ab
cd
echo: component: -test
printf: -test
echo: component: $ab
cd
printf: $ab\ncd
echo: flags: -test => value: $ab
cd
printf: flags: -test => value: $ab\ncd
other arguments : 

"..."の動作はそのまま先頭に$が入っただけだった

結論

printfをみるにc++系の実装なら$'...'することでほぼ確実に制御文字を反映させられるっぽい,またechoではそれよりも簡単に制御文字を反映させられるっぽい

ffmpegでの使いかた

-headersオプションは多重の呼び出しが不可能である(2回目からは内容が置き換わる)ため,前述の手法で制御文字を有効にすることではじめて複数の内容を利用できる.とは言えその中で頻用される幾つかはffmpeg側でも専用のオプションとして提供されており,本来ならばそういったものを利用するべきではないかとも考えられる(処理がめんどいって?それはそう).

詳細は以下で確認できる,-loglevel traceで動作が全て標準出力される,-loglevel quietで何も表示されない,また-i [url]-headers [header]より前に持ってくると-headers [header]が認識されない(なぜかドキュメントにはのってないけど)

% ffmpeg \
-loglevel trace \
-headers $'Hoge: hoge\r\nHuga: huga' \
-i 'http://example.com/playlist.m3u8' \
-c copy \
output.mp4
...
User-Agent: Lavf/58.76.100
Accept: */*
Range: bytes=0-
Connection: close
Host: example.com
Icy-MetaData: 1
Hoge: hoge
Huga: huga
...

以上の結果が得られればヘッダが正しく設定されたと言える(該当の動画は存在しないので最終的にはerrorが返される)
本来\r\nms-doswindowsで利用される作法であり,旧MacOSでは\rのみ・現macOSでは\nのみがそれぞれの「標準の改行コード」である,なので\nだけで十分ではある…のだが心配なので\r\nすることを癖にしても良いかもしれない,実際の\rmacOSにおいて「同じ行の先頭まで戻る(そして出力が順に置き換わる)」動作を表すので余計な改行にはならないはず

おまけ

これ読めばわかると思われるが…?

QUOTING
       A character may be quoted (that is, made to stand for itself) by
       preceding it with a `\'.  `\' followed by a newline is ignored.

       A string enclosed between `$'' and `'' is processed the same way as the
       string arguments of the print builtin, and the resulting string is
       considered to be entirely quoted.  A literal `'' character can be
       included in the string by using the `\'' escape.

       All characters enclosed between a pair of single quotes ('') that is not
       preceded by a `$' are quoted.  A single quote cannot appear within single
       quotes unless the option RC_QUOTES is set, in which case a pair of single
       quotes are turned into a single quote.  For example,

          print ''''

       outputs nothing apart from a newline if RC_QUOTES is not set, but one
       single quote if it is set.

       Inside double quotes (""), parameter and command substitution occur, and
       `\' quotes the characters `\', ``', `"', `$', and the first character of
       $histchars (default `!').