#ifdef 分岐を制御するプリプロセッサー定義の探索

由緒正しいソースコードをメンテナンスしていると、大量の #ifdef 分岐に泣かされることが、よくある。

よし、ソースコードMakefile で定義されているプリプロセッサー(つまり #define や -D オプション)をあらかじめ列挙しよう! そうだ、分岐に使われるものだけを狙い撃ちしたら便利じゃないか? とおもいたち、条件分岐マクロに使われているプリプロセッサーを抽出するスクリプトをでっちあげてみた。:

#! /bin/sh
extract_ifdef_branch() {
    find . -type f -name "*.h" -o -type f -name "*.c"\
 |while read f; do egrep '^[[:blank:]]*#[[:blank:]]*if' $f; done
}

# convert tab to space, and shrink multiple spaces to one space
replace_tab_to_space() {
    sed -e 's/\t/ /g; s/ \+/ /g'
}

# change '  #  HOGERA' to '#HOGERA'
trancate_heading_blanks() {
    sed -e 's/^ *# *\(.*\)$/#\1/'
}

# remove "#if", "#ifdef" and "#ifndef"'s
remove_heading_if() {
    sed -e 's/^#if[^ ]* \(.*\)$/\1/'
}

replace_paren_and_logics_to_space() {
    sed -e 's/[()]/ /g; s/[<>!][=]*/ /g; s/&&/ /g; s/||/ /g'
}

# remove C style comment
remove_comments() {
    sed -e 's|/\*.*\*/||g'
}

TMPNAME=`mktemp`

extract_ifdef_branch\
 |replace_tab_to_space\
 |trancate_heading_blanks\
 |remove_comments\
 > ${TMPNAME}

( # collect "#if" branches
# "#if defined" versions
cat ${TMPNAME}\
 |egrep '^#if +[ (!]*defined'\
 |sed -e 's/defined/ /g'
# other "#if", "#ifdef" and "#ifndef" versions
cat ${TMPNAME}\
 |egrep -e '^#if ' -e '^#ifdef' -e '^#ifndef'\
 |egrep -v '^#if +[ (!]*defined'
)\
 |remove_heading_if\
 |replace_paren_and_logics_to_space\
 |sed -e 's/ \+/\n/g'\
 |sort |uniq\
 |egrep -v -e "^$" -e "^TRUE$" -e "^FALSE$" -e '^[0-9]*$'

rm ${TMPNAME}

上記を extract-ifdef-branch.sh として保存しているとして、次を実行すると、カレントディレクトリー以下にあるファイルを全検索してマクロ分岐条件の #define 定義行をファイル名といっしょに確認できる。:

$ sh extract-ifdef-branch.sh\
 |while read n;\
 do echo "----- $n -----";\
 egrep -rnH "^[[:blank:]]*#[[:blank:]]*define[[:blank:]]+$n" .;\
 done

ただし抽出した #define は、 #ifdef の特定条件下でのみ有効だとかコメントアウトされていて無効という場合もあるので、きちんとソースを開いて確認する作業は必要。
それでもむやみに追いかけるよりは効率的 …だよね?

蛇足だけれど、 Makefile その他でプリプロセッサー定義の追加も対象にするために grep 条件に "-D<空白文字類>*$n" *1 を追加してもよいだろう。

#ifdef 系の分岐は #ifdef, #ifndef, #if defined, #if !defined, #if (!defined ... とバリエーションがある上、論理演算で結ばれていたり、バージョン番号との比較があったりと、かなり複雑。たぶん上のスクリプトには、いろいろ漏れがあるとおもうので、それぞれ気がついたら工夫して対処してほしい。

分岐を探す対象は *.h と *.c にしぼってあるし、コメントは C89 形式の /* ... */ スタイルで、ちゃんと閉じられているものしか取り除かない。 C++ に適用したい人は extract_ifdef_branch 関数や remove_comments 関数に手を入れるといい。

*1:[ [ :blank: ] ] 途中のスペースなしで :blank: を [ ] でくくりたいんだけれど、はてな記法とかちあっているようで表示できないので <空白文字類> と書いた