+ シェルスクリプトでバイナリ操作
created 2008-06-17 modified 2022-11-19
Unix系シェルスクリプト(bash)でバイナリファイルを作成したり、読み込んで処理したい、という話です。
Webを見ていたら、odとechoで十分、というページを見かけたのでやってみました。
サンプルでは echo よりは printf コマンドを使っています。
サンプル | 備考 |
---|---|
参考ソースです。ご自由にお使いください。以前置いていたものは配置が不親切だったので、修正しました。 | |
bindata_2.zip | ↑が機能しない場合、これでいかがでしょう... |
バイナリファイル作成サンプル
データ作成スクリプトファイルとしてこんなのを食わせて#シャープで始まる行はコメント int32 255 int16 255 str 255
期待するバイナリとして(BigEndianでは)こんなのを作りたい、とします。
00 00 00 ff 00 ff 32 35 35LittleEndianではこんなのです。
ff 00 00 00 ff 00 32 35 35
完全な内容はzipを参照してください(ファイル名 mkbin)。
一部のみ説明します。
根幹は、スクリプトを読み込んでバイナリデータを標準出力に吐く関数をつくり、以下のようにファイルにリダイレクトするだけです。
cat $INFILE| read_loop > $OUTFILE
read_loop は以下のとおりです。
function read_loop { while read type value do proc_line $type $value done } function proc_line { p_type=$1 #第一パラメータを p_type に設定 shift #パラメータを1個消費して p_value=$@ #残った全パラメータを p_value に設定 case $p_type in \#) true ;; int32) (put_int32 $p_value) ;; int16) (put_int16 $p_value) ;; int8) (put_int8 $p_value) ;; str) (put_str $p_value) ;; esac }
while コマンドは、パラメータで指定された内容をコマンドとして実行し、そのプロセス終了コードがゼロの間、do から done を繰り返します。
read コマンドは、パラメータで指定された文字列をシェル変数名とみなし、標準入力から読んだ行を変数に格納します。複数の変数名を指定すると、スペース区切りでその順に格納し、最後の変数には、行末まで全部を格納します。
そのため、
int32 255 int16 255というファイルを上記 read_loop 関数につっこむと、
1行目について
- type に "int32" を、
- value に "255" を格納して、
- proc_line int32 255 を実行します。
- そして、put_int32 255 を実行します。
次に、バイナリを出力する部分は以下です。
# 第一パラメータを32ビット(4バイト)数値として出力 function put_int32 { val=$1 hv=$(printf '%08x' $val) v0=$(echo $hv| cut -b1-2) v1=$(echo $hv| cut -b3-4) v2=$(echo $hv| cut -b5-6) v3=$(echo $hv| cut -b7-8) #BE #printf "%b%b%b%b" "\\x$v0\\x$v1\\x$v2\\x$v3" #LE printf "%b%b%b%b" "\\x$v3\\x$v2\\x$v1\\x$v0" } #put_int16、put_int8 も同様に作成します #文字列として出力 function put_str { val=$@ printf "%s" "$val" }
要するに printf コマンドでバイナリを出力している、と。
以上で作成スクリプト(mkbin)は完成。
mkbin 入力テキスト 出力バイナリファイルと使います。
バイナリファイル読込サンプル
こっちも詳しくはzip参照でおながいします(dumpbin)。サンプルは、1レコードに複数(固定数)の項目を含む、可変長レコードのバイナリデータを、レコードごと、項目ごとに切り出して、16進数表示するというものです。
サンプルで扱うバイナリファイルの仕様は下記です。
ファイル := レコードの繰り返し。 |__ レコード := レコードサイズ、オフセット領域、値領域 |__ レコードサイズ := 4バイト整数。次のレコードまでのバイト数。 |__ オフセット領域 := 1項目ごとに4バイトオフセット値の繰り返し。 | |__オフセット値 := レコード先頭を基準(ゼロ)として該当項目の値領域を指す値。 |__ 値領域 := int32 か int16 か int8 か str。 ※特定のレコードで、特定の項目の値を指定したくない(=RDBのnull値のようなもの)場合は、 オフセット値にゼロを書き、値領域に何も書かない。
たとえば6kBytesくらいのバイナリを扱いたい場合、od コマンドの出力を全部つなげて1行にしてしまえばいいのですな(=そんな荒業っぽいことをしても落ちない)。それで、欲しいところをcutで取り出すと。
256バイトずつodで出して、それを1行につなげて変数に代入するには
function concat_lines { while read line do printf '%s ' "$line" done printf '\n' } REC_BUF=$(cat $FILE_NAME| od -v -An -tx1 -w256 -j$REC_POS -N$REC_SIZE| concat_lines)
でOK。
(説明)
- cat コマンドはファイルを標準出力に出しているだけです。
- od コマンドで開始位置と読み込みサイズを指定して16進数文字列でダンプします。
- od コマンドの出力は複数行になりますが、それを、concat_lines というシェル関数で、1行にしています。printf コマンドで改行せず出力し、最後に改行します。
- シェル変数=$(コマンド) で、コマンドの結果をシェル変数に代入しています。
こんな風に1行につっこんでから、たとえばオフセット16バイトから4バイトが欲しければ
ITEM=$(echo $RECBUF | cut -b48-60)または
ITEM=$(echo $RECBUF | cut -d\ -f17-20などとすると、
00 11 22 33というような16進数文字列の形で値を得ることができます。
追記:2012-11-28
bashの機能で、もっと簡単にできることに気づきました。
ITEM=${RECBUF:48:12}
でいけます。
${変数名:オフセット:長さ} です。なお
${変数名:オフセット} だとオフセット以降末尾までが取れます。
サンプルは
dumpbin 入力バイナリファイルと使います。
発展的な話として、サンプルは16進数文字列として切り出しするだけですが、バイナリ自体に型情報が含まれている、あるいは別に型情報を取れるなら、odコマンドで値解釈もできそうです(文字列は文字列として表示とか、2バイト整数は0~65535の数文字列として表示、など)。我こそはと思う方はodコマンドのマニュアルを参照し実装してみてください。
実行例
zip ファイルに含まれるコマンドの利用例を示します。[keizo@suzuki bindata]$ ls -l 合計 12 -rwxr-xr-x. 1 keizo users 2329 6月 17 2008 dumpbin -rwxr-xr-x. 1 keizo users 1551 4月 11 22:03 mkbin drwxr-xr-x. 2 keizo users 4096 4月 11 22:04 sample [keizo@suzuki bindata]$ cd sample [keizo@suzuki sample]$ [keizo@suzuki sample]$ ls -l 合計 4 -rw-r--r--. 1 keizo users 682 4月 11 22:00 input_script.txt [keizo@suzuki sample]$ cat input_script.txt | head -n 20 #### record 1 int32 0x26 int32 0x14 int32 0x16 int32 0x1c int32 0x22 int16 257 str 999999 int16 4 str 6666 int32 1 #### record 2 [keizo@suzuki sample]$ [keizo@suzuki sample]$ [keizo@suzuki sample]$ ../mkbin input_script.txt output1_mkbin.bin [keizo@suzuki sample]$ [keizo@suzuki sample]$ [keizo@suzuki sample]$ od -t x1 output1_mkbin.bin | head -n 20 0000000 26 00 00 00 14 00 00 00 16 00 00 00 1c 00 00 00 0000020 22 00 00 00 01 01 39 39 39 39 39 39 04 00 36 36 0000040 36 36 01 00 00 00 26 00 00 00 14 00 00 00 16 00 0000060 00 00 1c 00 00 00 22 00 00 00 01 01 39 39 39 39 0000100 39 39 04 00 36 36 36 36 01 00 00 00 24 00 00 00 0000120 00 00 00 00 14 00 00 00 1a 00 00 00 20 00 00 00 0000140 39 39 39 39 39 39 04 00 36 36 36 36 01 00 00 00 0000160 20 00 00 00 14 00 00 00 16 00 00 00 00 00 00 00 0000200 1c 00 00 00 01 01 39 39 39 39 39 39 01 00 00 00 0000220 1c 00 00 00 14 00 00 00 16 00 00 00 00 00 00 00 0000240 00 00 00 00 01 01 39 39 39 39 39 39 16 00 00 00 0000260 14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000300 01 01 14 00 00 00 00 00 00 00 00 00 00 00 00 00 0000320 00 00 00 00 00 00 0000326 [keizo@suzuki sample]$ [keizo@suzuki sample]$ [keizo@suzuki sample]$ ../dumpbin output1_mkbin.bin file name=output1_mkbin.bin, file size=214 RECORD 1: size=38 vtop=20 000000 26 00 00 00 14 00 00 00 16 00 00 00 1c 00 00 00 000010 22 00 00 00 01 01 39 39 39 39 39 39 04 00 36 36 000020 36 36 01 00 00 00 000026 item0001 00000014 2:01 01 item0002 00000016 6:39 39 39 39 39 39 item0003 0000001c 6:04 00 36 36 36 36 item0004 00000022 4:01 00 00 00 RECORD 2: size=38 vtop=20 000026 26 00 00 00 14 00 00 00 16 00 00 00 1c 00 00 00 000036 22 00 00 00 01 01 39 39 39 39 39 39 04 00 36 36 000046 36 36 01 00 00 00 00004c item0001 00000014 2:01 01 item0002 00000016 6:39 39 39 39 39 39 item0003 0000001c 6:04 00 36 36 36 36 item0004 00000022 4:01 00 00 00 RECORD 3: size=36 vtop=20 00004c 24 00 00 00 00 00 00 00 14 00 00 00 1a 00 00 00 00005c 20 00 00 00 39 39 39 39 39 39 04 00 36 36 36 36 00006c 01 00 00 00 000070 item0001 00000000 0:(none) item0002 00000014 6:39 39 39 39 39 39 item0003 0000001a 6:04 00 36 36 36 36 item0004 00000020 4:01 00 00 00 RECORD 4: size=32 vtop=20 000070 20 00 00 00 14 00 00 00 16 00 00 00 00 00 00 00 000080 1c 00 00 00 01 01 39 39 39 39 39 39 01 00 00 00 000090 item0001 00000014 2:01 01 item0002 00000016 6:39 39 39 39 39 39 item0003 00000000 0:(none) item0004 0000001c 4:01 00 00 00 RECORD 5: size=28 vtop=20 000090 1c 00 00 00 14 00 00 00 16 00 00 00 00 00 00 00 0000a0 00 00 00 00 01 01 39 39 39 39 39 39 0000ac item0001 00000014 2:01 01 item0002 00000016 6:39 39 39 39 39 39 item0003 00000000 0:(none) item0004 00000000 0:(none) RECORD 6: size=22 vtop=20 0000ac 16 00 00 00 14 00 00 00 00 00 00 00 00 00 00 00 0000bc 00 00 00 00 01 01 0000c2 item0001 00000014 2:01 01 item0002 00000000 0:(none) item0003 00000000 0:(none) item0004 00000000 0:(none) RECORD 7: size=20 vtop=20 0000c2 14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000d2 00 00 00 00 0000d6 item0001 00000000 0:(none) item0002 00000000 0:(none) item0003 00000000 0:(none) item0004 00000000 0:(none) [keizo@suzuki sample]$ [keizo@suzuki sample]$ ../dumpbin output1_mkbin.bin > output2_dumpbin.txt [keizo@suzuki sample]$ [keizo@suzuki sample]$ ls -l 合計 12 -rw-r--r--. 1 keizo users 682 4月 11 22:00 input_script.txt -rw-r--r--. 1 keizo users 214 4月 11 22:05 output1_mkbin.bin -rw-r--r--. 1 keizo users 2117 4月 11 22:07 output2_dumpbin.txt [keizo@suzuki sample]$
まとめ
ポイントは- read コマンドを覚えると便利
- printf コマンドは通常は端末に表示できないバイナリ値を出力したり、改行なしで出力したりできるので便利
- シェル特有の if 文の仕組み("[" は testコマンドのエイリアスである、とか)をちゃんと理解すると便利
といったところでしょうか。
以前、わざわざflex(lex)を使ってバイナリデータ作成言語なるものをこしらえたことがありましたが(下記)、そんなことしなくてもbashスクリプトで十分できるもんですね。
リンク | 備考 |
---|---|
lex/yacc でバイナリデータ作成言語 |
加筆メモ
このスクリプトは、私自身が仕事で必要になり、慌ててつくったものでした。あるRDBのデータを、フォーマット非公開のバイナリ形式でエクスポートして、独自のプログラムで新DBにインポートする、という案件でした。
現在、google で シェル バイナリ などと検索すると、このページが上位に表示されているようです。
そこで、せっかくなので解説をつけてみました。余計わかりにくくなってしまったかもしれませんが、このページが、いまPCの前で頑張っている皆さんのお仕事に、少しでもお役に立てれば幸です。
自分はこれからもこういうニッチな方向でいきたいと思います。