flat7th

+ シェルスクリプトでバイナリ操作

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 35
LittleEndianではこんなのです。
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 を実行します。
2行目以降も同様。


次に、バイナリを出力する部分は以下です。
# 第一パラメータを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の前で頑張っている皆さんのお仕事に、少しでもお役に立てれば幸です。

自分はこれからもこういうニッチな方向でいきたいと思います。