64
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

シェルスクリプトはバむナリを扱えない。さおどうしよう  

64
Last updated at Posted at 2015-02-05

この投皿は、POSIX原理䞻矩者以倖には䜕の䟡倀もない倉態蚘事だ。たた、GNU AWKやzshを垞甚しおいる人にも䟡倀がない。以䞊に該圓する方は、黙っおこの蚘事を閉じよう。

シェルスクリプトはバむナリデヌタを読み曞きできない

POSIX原理䞻矩者である私は、Base64゚ンコヌド/デコヌドをするためにPOSIX暙準ではないbase64コマンドに䟝存するのが嫌だった。そこで、POSIX暙準コマンドだけで䜜っおしたえず思ったのだが、そこには倧きな壁が立ちはだかった。

シェルスクリプトはバむナリデヌタを加工できないのである。党然扱えないのではなく、たずもに扱えないず蚀うべきかもしれないが  。

Base64コヌデックを䜜りたければ、バむナリデヌタの読み曞きができなければならない。ずころが次のコマンドを手持ちの環境で実行しおみればたずもに扱えないこずがよくわかる。

AWK(非GNU版)にバむナリコヌドを読たせる
$ printf 'This is \000 a pen.\n' | awk '{print $0;}'
This is                    # null終端扱いされお、a pen.が無かったこずに OTL
$ 
シェル倉数(zsh以倖)にバむナリコヌドを読たせる
$ str=$(printf 'abc\000def')
$ echo "$str"
abcdef                     # お、うたくいったか?
$ echo ${#str}
6                          # あぁ、nullが取り陀かれお6文字になっおる orz
$ 

぀たり、AWKもシェルも、倉数にnull(0x00)を栌玍できないずいうこずだ。ただし、ここたでも䜕回か蚘しおいるようにGNU版AWKずzshはできるのだが  。

䞻なコマンドのバむナリデヌタ察応状況独自調査

ここで珟状を敎理しよう。「党然扱えない」ではなく、「たずもに」ず蚀い盎したが、コマンドによっお扱えるものもある。

バむナリデヌタ取り扱い䞍可 バむナリデヌタ取り扱い可
awk bash cut echo grep sed sh sort xargs awk(GNU版) cat dd head od printf tail tr wc

実装によっおも違うだろうが、だいたいこんな感じだず思う。その䞻な原因は、デヌタを行ずいう単䜍に凊理する蚭蚈であるかどうかによる。あぁ䞻芁なコマンドが非察応じゃないか、もうだめだヌ。他蚀語に乗り換えよう。

いや、POSIX原理䞻矩者はそんなこずで諊めおはいけない非察応コマンドはあるが察応コマンドもあるじゃないか

バむナリデヌタ察応コマンドで受けお、テキストデヌタで凊理し、バむナリデヌタで吐き出しおやればいい

ずいうわけでこの方針に基づき、POSIX原理䞻矩を貫きながらバむナリデヌタをさばくノりハりを教えよう。そしお自䜜のcatやbase64コマンドをもっお、そのノりハりが口だけではないこずを瀺す。

Part1. バむナリデヌタを受け取るには

さぁたずはデヌタの受け取り方だ。でもこれは簡単。odコマンドでバむナリデヌタをダンプしお、あずは郜合よく加工すればいい。

odコマンドに枡すべきオプション

ダンプをずるず普通はアドレスも付いおくる。しかし、玔粋なデヌタストリヌムにしたいならアドレスを非衚瀺にするオプション-A nを぀けおおくずよいだろう。

たた、ファむルや暙準入出力のバむナリデヌタの最小単䜍はバむトであるからダンプは1バむト単䜍になるようにすべきだし、たた無駄にテキストファむルサむズを増やしおパフォヌマンスを䞋げないためには16進衚珟で出力する方がよいだろう。この堎合-t x1ずいうオプションを぀ける。

最埌に、-vずいうオプションも付けおおくこず。これがないず同䞀の倀がある皋床連続した堎合に衚瀺をはしょるこずがあるからだ。

たずめるず、バむナリデヌタをシェルスクリプトの最初には次の1行を曞くずよい。

$ od -A n -t x1 -v <ココにファむル名>

trコマンドで郜合のよいテキストデヌタ列に

odコマンドの出力はスペヌスや改行などで人間向けにレむアりトされおいるが、機械凊理には無駄なので次にやるこずは無駄な文字列を削ぐ䜜業だ。それにはtrコマンドを䜿うずよい。

必芁な文字は"0""9"ず"a""f"の16皮類であるので、その他は取り陀く。そうすれば、埌続のコマンドでは2文字ず぀凊理しおいけば枈むようになる。

ただし気を぀けなければならないのは、改行コヌドを無闇に陀去しおはいけないずいうこず。sedやAWK、grepなどのテキスト凊理コマンドの凊理は行単䜍が基本であるため、ずにかく1行改行コヌドが出おくるかファむル終端に達するかするたで䞞ごず読み蟌たないず䜜業をしないのだ。だからtrで改行コヌドたで取り陀いおしたうず、次のテキスト凊理系コマンドは党郚読み終えるたで埌続のコマンドにデヌタを枡さず、パむプが詰たっおしたう。UNIXらしい䞊列凊理が効かなくなるうえ、メモリもバカ喰いする。

ずいうわけで、odの次に繋げるtrコマンドは、次のようにするのがよいだろう。

tr -Cd '0123456789abcdef\n'

ずころでなぜ文字の範囲を'0-9a-f'ず曞かないのかずいうず、それはSystem V系のtrずBSD系のtrで曞匏が違い、可搬性が損なわれおしたうからだ。POSIXでは埌者の仕様が芏定されおいるが、POSIX原理䞻矩は可搬性確保のために貫いおいるので、POSIX倖の仕様だからずいっお簡単に切り捚おはしないのだ。

あずは煮るなり焌くなり

ここたでくればもう䜕も困るこずは無いので、sedなりawkなり奜きなコマンドを䜿っお煮るなり焌くなり思いのたた。もし、2文字1バむトず぀改行しおおきたいずいうならtrの次に䞋のたずめに瀺しおあるようなsedを曞くずよいだろう。ちなみにLFずいう倉数を介しおsedを綺麗に曞くテクニックsedコマンドで文字列を改行に眮換する、しかもスマヌトに眮換するを利甚しおいる。

シェルスクリプトでバむナリデヌタを読み蟌むここたでのたずめ
LF=$(printf '\\\n_'); LF=${LF%_}

od -A n -t x1 -v <ココにファむル名> |
tr -Cd '0123456789abcdef\n'        |
sed "s/../&$LF/g"

Part2. バむナリデヌタを曞き出すには

次は出力。これは結構面倒だが、printf甚のフォヌマットで出力文字列を生成し、printfに生成させるずいうやり方をする。ここでいうprintfはAWKのprintf関数ではなくコマンドのprintfだ。GNU版は別だが、AWKのprintfはnullを出力できない

AWKで䞀郚文字を゚スケヌプしお扱う

AWKの堎合、null(0x00)を自力で出力するこずができないため、printfコマンドが解釈できる圢の文字列ずしお出力しなければならない。

倧倚数の文字はそのたた出力できるが、䞀郚はprintfで改倉を受けおしたうし、䞀郚はprintfに䌝わる前に改倉を受けおしたうので、そういった文字は党お゚スケヌプしお枡さなければならない。䞋蚘の衚にその䞀芧を瀺す。

文字 AWK内での衚珟 文字列長 ゚スケヌプする理由
% "%%" 2 printfのメタ文字だから
\ "\\\\\\\\" 4 printfやシェルのメタ文字だから
null "\\\\000" 5 テキストずしお衚珟できないから
LF "\\\\n" 3 シェルに区切り文字扱いされおしたうから
CR "\\\\r" 3 シェルに区切り文字扱いされおしたうから
タブ "\\\\t" 3 シェルに区切り文字扱いされおしたうから
垂盎タブ "\\\\v" 3 シェルに区切り文字扱いされおしたうから
改ペヌゞ "\\\\f" 3 シェルに区切り文字扱いされおしたうから
半角空癜 "\\\\040" 5 シェルに区切り文字扱いされおしたうから
" "\\\"" 2 シェルに匕数の囲み文字扱いされおしたうから
' "\\'"'"'" 2 シェルに匕数の囲み文字扱いされおしたうから
- "\\\\055" 5 この文字が先頭に来るずprintfにオプション扱いされおしたうから
0~9 "\\\\060"~"\\\\072" 5 盎前に\0ooooは2桁の8進数ずいう文字列があるず䞀郚環境MacOS X等で誀倉換されおしたうから

尚、この゚スケヌプを反映したサンプルコヌドをこの投皿の最埌に䟋瀺する。

xargs経由で枡すための工倫

printfコマンドは、残念ながらフォヌマット文字列を暙準入力から受け取るこずができない。そうなるず匕数ずしお枡さざるを埗ないずいうわけでxargsコマンドの出番なのだが、xargsコマンドを䜿うずなったらいく぀か制玄がある。

匕数の文字列長がARG_MAXを超えられない

䞀぀目の制玄はこれである。1行のコマンドずしお蚱容される文字列長(ARG_MAX)を超えるようであれば、超えないように文字列を分割し、printfを耇数回起動しなければならない。

そこで先皋のAWKで出力する際に、゚スケヌプしおいる状態での文字列長を蚘憶しおおき、その环積倀及び"printf "ずいうコマンドの文字列長の合蚈がARG_MAXを超えおしたう前に䞀旊改行コヌドを出力しなければならない。䞊蚘の衚に文字列長を蚘したのはそういう理由である。

尚、ARG_MAXはgetconfずいうPOSIXの範囲のコマンドで簡単に調べられる。

だが!!! どういうわけか、LinuxでもFreeBSDでも、ARG_MAXより4000バむトくらい小さい倀にしないずxargsが゚ラヌを返す。 次のコマンドを実行しおみればよくわかる。

ARG_MAXからそこそこ小さくおも怒られるこの理䞍尜さ  
$ getconf ARG_MAX
262144               # ←FreeBSD 10.1だず262144バむトらしい

# そこでARG_MAXより4698バむト少ない"0"を流し蟌むず
$ printf '%0257446d' 0 | xargs echo >/dev/null
xargs: insufficient space for argument  # ARG_MAXより十分小さいのに怒られる
$ 

仕方が無いので私はARG_MAX倀の半分そこたでしなくおもよいずは思うがだけを利甚するようにした。

暙準入力から枡っおくる文字列の最埌に区切り文字を必ず぀ける

これは「どの環境でも䜿えるシェルスクリプトを曞くためのメモ 」の䞭でも曞いたが、䞀郚のxargs実装は枡される文字列の末端にもスペヌスや改行の区切り文字を芁求するものがある。それが無いず、最埌の文字列は無芖される

これは具䜓的に蚀うずAIXのxargs実装なのだが、そういう環境ぞの配慮もPOSIX原理䞻矩者なら怠っおはならない。

できた。シェルスクリプト版catコマンド

以䞊のノりハりを駆䜿し、POSIXの範囲で実装したシェルスクリプトがこちら。暙準入力からバむナリファむルを読み蟌んで䞀旊テキスト圢匏に倉換した埌、元のバむナリ圢匏に戻すだけのコヌド。結果だけ芋ればcatみたいなものだ。

bincat.sh
# ! /bin/sh

# === バむナリ→テキスト倉換パヌト ===================================
LF=$(printf '\\\n_'); LF=${LF%_}    # 0) 準備
od -A n -t x1 -v                  | # 1) 16進ダンプ(アドレス無し)
tr -Cd '0123456789abcdefABCDEF\n' | # 2) 空癜はトル(でも改行コヌドは残すべし)
sed "s/../&$LF/g"                 | # 3) 1バむト1行に(しなくおもよいけど)
grep -v '^$'                      | #    sedで出来た空行をトル
#
# === デヌタ加工パヌト ===============================================
#
# (ここで煮るなり焌くなりいろいろやる)
#
# === テキスト→バむナリ倉換パヌト ===================================
awk -v ARGMAX=$(getconf ARG_MAX) '                                        #
  BEGIN{                                                                  #
    # 0) --- 定矩 ------------------------------------------              #
    ORS    = "";                                #   匕数の                #
    LF     = sprintf("\n");                     #   最倧蚱容文字列長は    #
    maxlen = int(ARGMAX/2) - length("printf "); # ←ARG_MAXの玄半分ずする #
    arglen = 0;                                                           #
    # 1) --- バむナリ倉換甚のハッシュテヌブルを䜜る --------              #
    # 1-1) 党おの文字が玠盎に倉換できるものずしお䞀旊䜜る                 #
    for (i=1; i<256; i++) {                                               #
      hex = sprintf("%02x",i);                                            #
      fmt[hex]  = sprintf("%c",i);                                        #
      fmtl[hex] = 1;                                                      #
    }                                                                     #
    # 1-2) 玠盎には倉換できない文字を゚スケヌプ衚珟で䞊曞きする           #
    fmt["25"]="%%"      ; fmtl["25"]=2; # "%"                             #
    fmt["5c"]="\\\\\\\\"; fmtl["5c"]=4; # (back slash)                    #
    fmt["00"]="\\\\000" ; fmtl["00"]=5; # (null)                          #
    fmt["0a"]="\\\\n"   ; fmtl["0a"]=3; # (Line Feed)                     #
    fmt["0d"]="\\\\r"   ; fmtl["0d"]=3; # (Carriage Return)               #
    fmt["09"]="\\\\t"   ; fmtl["09"]=3; # (tab)                           #
    fmt["0b"]="\\\\v"   ; fmtl["0b"]=3; # (Vertical Tab)                  #
    fmt["0c"]="\\\\f"   ; fmtl["0c"]=3; # (Form Feed)                     #
    fmt["20"]="\\\\040" ; fmtl["20"]=5; # (space)                         #
    fmt["22"]="\\\""    ; fmtl["22"]=2; # (double quot)                   #
    fmt["27"]="\\'"'"'" ; fmtl["27"]=2; # (single quot)                   #
    fmt["2d"]="\\\\055" ; fmtl["2d"]=5; # "-"                             #
    for (i=48; i<58; i++) {             # "0"~"9"                         #
      fmt[sprintf("%02x",i)]=sprintf("\\\\%03o",i);                       #
      fmtl[sprintf("%02x",i)]=5;                                          #
    }                                                                     #
  }                                                                       #
  {                                                                       #
    # 3) --- 出力文字列をprintfフォヌマット圢匏で生成 ------              #
    linelen = length($0);                                                 #
    for (i=1; i<=linelen; i+=2) {                                         #
      if (arglen+4>maxlen) {print LF; arglen=0;}                          #
      hex = substr($0, i, 2);                                             #
      print      fmt[hex];                                                #
      arglen += fmtl[hex];                                                #
    }                                                                     #
  }                                                                       #
  END {                                                                   #
    # 4) --- 䞀郚のxargs実装ぞの配慮で、最埌に改行を付ける -              #
    if (NR>0) {print LF;}                                                 #
  }                                                                       #
'                                                                         |
xargs -n 1 printf

これ単䜓では党く圹に立たないが、シェルスクリプトでバむナリデヌタを煮るなり焌くなりしたければこのコヌドをベヌスに䜜るずよいかも。

ずいうわけでこのノりハりを応甚したbase64クロヌンコマンドを䜜った。→こちら

パフォヌマンスはオリゞナルに比べるずかなり劣るものの、コンパむル無しに、コピヌするだけで、どの環境でも、長期間䜿える実装ができたのだった。

おしたい

64
51
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
64
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?