CrunchWEBExtract the source files from HTML files.Version: $Revision: 1.10 $
Copyright (C) 1999 by Maoyam
"CrunchWEB" について下の囲みにまとめたようなアイデアが、
"UNIX MAGAZINE 1999/12月号 ( そこで、「HTMLからソースコード部分を抜き出す」ツールを、
C 言語で実装してみました。
使い方次に示す、記述ルールに従った HTML ファイルからソース・ファイルを抜き出すには、 次のようにします。
また、HTML ファイルは複数指定もできます。
出力ファイルに変更がある場合に限って、 既存ファイルの上書きが行われます。 複数の HTML ファイルにまたがって埋め込まれたソース・ファイルは、 同一の出力ファイルとして再構成されます。 この様な使用法では、 HTML ファイルをコマンド・ライン指定する順序 に注意が必要です。 パーミッション不正などの理由で、 <pre>タグの "file"属性で指定されたファイル・パスへの出力ができない場合、 ソース・コードは標準出力に吐き出されます。 エラー・メッセージなどは、英文だけになっています。 HTML 記述ルール"CrunchWEB" でソースコードを抜き出せるようにする HTML ファイル記述ルールは、以下のとおり。
ソースコードとドキュメンテーション以下に、"CrunchWEB" のソースコードと各部分の説明を示します。 "CrunchWEB" ソース一式は この tar-ball にまとめてあります。 INDEX
クレジット表記crunchWEBのソースコードクレジット部分。 GNU一般公有使用許諾書(バージョン2) に従って再配布可能とします。
インクルード・ファイルとデバッグ・レベルUNIXプラットフォームでのインクルードを行なう。 しかし、このパラグラフでのソースコード表示部分でインクルードファイル名が Webブラウザに表示されない。 これは、HTMLのタグと C言語のシステム定義インクルード記述が文法上の衝突しているため。 実際にインクルードされているファイル名は、次に列挙したものになっている。
また、
プログラム終了コードこのプログラムの終了コードを定義している。
正常終了は、
パーサ関連の宣言・定義ここでは、"WEB Styled HTML"
ファイルからソースファイルを切り出すパーサに関わる宣言などを示す。
このパーサが理解するトークンは BEGIN_TAG ("<pre")・ TAG_ATTR_FILEPATH ("file=")・ END_TAG ("</pre>")、 出力ソースファイル名パスの区切りなどのデリミタは EOL (\0)・ TAG_RPAREN (>)・ FILEPATH_QUOTE (") として定義されている。
パーサ処理中のコンテクスト情報を管理するためのデータ構造は、
パーサの内部状態を有限状態機械 (Finite State Machine; FSM)
として表現した。
この状態機会がとるすべての遷移状態を、
状態遷移表のACTIONを実装する関数プロトタイプは、
状態遷移表の各列のACTIONを扱うのは、以下の2関数。 これらのACTION実装関数とそれぞれが受け持つ遷移状態の関連付けは、 action_table[]配列で定義されている。
以下のいくつかの関数は、パーサ処理の下請け的な処理を行なう。
出力ファイル更新にかかわる定義・宣言ここでは、この仕組みのために必要な定義を行っている。 一時ファイルのファイル名は、<pre>タグの file 属性で指定された出力ファイル名に このプログラムのプロセスIDを付加したモノにする。
ユーティリティ・ルーチン (プロトタイプ宣言)USAGEやエラー/警告メッセージを表示するなどの便利なルーチンについて、 プロトタイプ宣言している。
プログラム・エントリ(main()関数)ここでは、"crunchWEB"のプログラム・エントリについて説明する。
コマンド引数には、一つ以上の
"WEB Styled HTML"ファイルを指定する。
コマンド引数の確認が済むと、一時ファイル名生成に必要な、 このプログラムのプロセスIDを得る。
コマンド引数に指定された入力ファイル毎に、 読込アクセス許可を確認してから、 パーサ関数に入力ファイル・パスを渡していく。 入力アクセスが許可されないファイルについては警告メッセージを表示し、 それ以外の指定ファイルの処理を進める。 パーサ関数がエラーを報告したら、 その終了コードでプログラムを即時中止する。
全ての入力ファイル・パスをパーサ処理した後で、 実際の出力ファイルと変更があるモノについて上書きを行う。 この処理中になんらかの以上が発生すれば、直ちにプログラムを終了する。
プログラムの終了は、必ずここで行う
パーサ処理の実装ここでは、
"CrunchWEB"プログラムのパーサ処理の実装について説明する。 パーサに関する宣言・定義については、 「パーサ関連の宣言・定義」 を参照。
パーサ処理の初期段階は、
コンテクスト情報を初期化する事から始まる。
コンテクスト情報の初期化が済むと、パーサのメイン・ループに進む。
パーサ・ループから抜けてきた時、入力ファイルがEND_TAG (</pre>)
を書き忘れていた場合に対応する必要がある。 出力ストリームのクローズ時に異常が発生したら、 エラーメッセージを表示してプログラムを即時終了させる。
最後に残された処理は、入力ストリームのクローズだけになる。 入力ストリームのクローズ時に異常が発生したら、 エラーメッセージを表示しプログラムを即時中止させる。
「入力行読み捨て中状態
(CRUNCH_IGNORE)」のACTION定義
|
static CRUNCH_STATES seek_BEGIN_TAG(CRUNCH_CONTEXT *c, CRUNCH_STATES state) { |
FSMの呼び出し時の状態は、CRUNCH_IGNOREでなければならない
if (state != CRUNCH_IGNORE) { state = CRUNCH_ILLEGAL_STATE_CONTROL; } |
入力バッファが、BEGIN_TAG (<pre)で始まっていれば、
今回の読込行の次行以降には、ソースコード断片が存在している。
この場合は、BEGIN_TAGの"file"属性値の取得を行なう。
else if (strncmp_ignore_case(BEGIN_TAG, c->line, strlen(BEGIN_TAG)) == 0) { char * spit_out_file_path = search_spit_out_file_path(c->line); |
"file"属性値が指定されていなければ、ソースコード断片は標準出力
stdout に出力するようにコンテクストを設定する。
入力ファイル名と入力ファイル中の行番号を、
標準出力中の区切り行として出力する。
if (spit_out_file_path == NULL) { (void)fprintf(stdout, "----- %s:L%u -----\n", c->web_file_path, c->line_no); c->spit_out_stream = stdout; } |
"file"属性値が指定されていたならば、 出力ファイル名を output_files に記録し、 一時ファイルを書き込み/追加モードでオープンする。
一時ファイルのオープンで異常が発生した時には、 ソース・コードを標準出力に吐き出すように、 コンテクストを stdout へ設定する。
else { char *tmp_file_path; tmp_file_path = add_output_file_name(spit_out_file_path); if ((tmp_file_path == NULL) || (c->spit_out_stream = fopen(tmp_file_path, (access(tmp_file_path, F_OK) == 0)? "a":"w") ) == NULL) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Can't create/wite-open " "tmp-file \"%s\"\n", (void *)tmp_file_path); (void)fprintf(stdout, "----- In %s: Line# %u " "(Tmp-file Access Denied): " "Output File is \"%s\" -----\n", c->web_file_path, c->line_no, spit_out_file_path); c->spit_out_stream = stdout; } } |
BEGIN_TAGが判別できているのでパーサの状態は、 CRUNCH_IGNORE から CRUNCH_SPIT_IT に遷移する。
state = CRUNCH_SPIT_IT; } |
今回の読込バッファは、BEGIN_TAG で始まっていないので読み捨てる。
パーサの状態は、 CRUNCH_IGNORE のままにする。
else { /* Ignore this read line buffer */ state = CRUNCH_IGNORE; } return state; } |
ここでは、パーサの状態が「ソース断片出力中」時の ACTION 定義関数 spit_it_out() 実装を述べる。
static CRUNCH_STATES spit_it_out(CRUNCH_CONTEXT *c, CRUNCH_STATES state) { |
パーサの状態は、CRUNCH_SPIT_IT でなければならない。
if (state != CRUNCH_SPIT_IT) { state = CRUNCH_ILLEGAL_STATE_CONTROL; } |
今回の読込バッファが、END_TAG (</pre>)で始まっていれば、
標準出力以外の出力ストリームに限りクローズを行なう。
クローズ時に異常が発生すれば、プログラムを即時中止させる。
ソースコード断片の最終行に到達したので、 パーサの状態は CRUNCH_SPIT_IT から CRUNCH_IGNORE へ状態遷移させる。
else if (strncmp_ignore_case(END_TAG, c->line, strlen(END_TAG)) == 0) { if ((c->spit_out_stream != stdout) && fclose(c->spit_out_stream) != 0) { print_err_WHICH_LINE(__FILE__, __LINE__); print_err("Fail Close Output " "Stream (0x%x)\n", (void *)c->spit_out_stream); perror("ERRNO MSG"); return EXIT_FAIL_CLOSE_OUTPUT; } c->spit_out_stream = (FILE *)NULL; state = CRUNCH_IGNORE; } |
CRUNCH_SPIT_IT 状態では、END_TAGで開始していない読込行は、
コンテクストに設定されている出力ストリームへすべて吐き出す。
出力時に異常が発生したら、プログラムを即時中止させる。
パーサの状態は、CRUNCH_SPIT_IT のままにする。
else { /* Write the read buffer to output stream */ if (fputs(c->line, c->spit_out_stream) == EOF) { print_err_WHICH_LINE(__FILE__, __LINE__); print_err("Fail Write Out " "[%s] ", (void *)c->line); print_err("into the output " "stream (0x%x)\n", (void *)c->spit_out_stream); perror("ERRNO MSG"); return EXIT_FAIL_FPUTS; } state = CRUNCH_SPIT_IT; } return state; } |
比較される2つの文字列の内容は、一切、破壊されてはいけない。
static int strncmp_ignore_case(char *tag, char *line, size_t sz) { int result; size_t i; for (i = 0; i < sz && line[i] != EOL; ++i) { if (tag[i] == EOL) { result = -1; /* tag[] < line[] */ break; } result = toupper((int)tag[i]) - toupper((int)line[i]); if (result != 0) break; } return result; } |
コンテクスト構造の入力バッファは、 出力ファイル・パス名の終端文字位置に EOL ('\0') が加えられている事に注意!
static char * search_spit_out_file_path(char *line) { char *result; for (line += strlen(BEGIN_TAG); *line != EOL && isspace((int)*line); ++line); if (*line == EOL || strncmp_ignore_case(TAG_ATTR_FILEPATH, line, strlen(TAG_ATTR_FILEPATH)) != 0) { /* Unfound/Exist Illegal PRE-Tag Attribute */ return NULL; } line += strlen(TAG_ATTR_FILEPATH); if (*line == FILEPATH_QUOTE) { ++line; } for (result = line; !is_TAG_ATTR_FILEPATH_delimit(*line); ++line) ; *line = EOL; return result; } |
static int is_TAG_ATTR_FILEPATH_delimit(char code) { return code == EOL || code == TAG_RPAREN || code == FILEPATH_QUOTE || isspace((int)code); } |
任意のストリームを出力先に指定できる。
static void dump_FSM_context(FILE *stream, char *header, CRUNCH_CONTEXT *c) { fprintf(stream,"%s<<< Contents of CRUNCH_CONTEXT >>>\n", header); fprintf(stream,"%s web_file_path = [%s]\n", header, c->web_file_path); fprintf(stream,"%s instream = 0x%x\n", header, c->instream); fprintf(stream,"%s line[] = [%s]\n", header, c->line); fprintf(stream,"%s line_no = %ld\n", header, c->line_no); fprintf(stream,"%s spit_out_stream = 0x%x (stdout = 0x%x)\n", header, c->spit_out_stream); } |
ここでは、 一時ファイルへ退避したソース・コードと出力先ファイルの内容に、 変更があった場合に行うファイル更新処理の実装を示す。
<pre>タグの
file 属性値に指定された出力ファイル・パスを引数にとり、
output_files に記録して行く。
既に、記録されている場合は、重複した記録は行わない。
メモリ獲得などが正常に行えたら、
一時ファイル名を作成してそれを返す。
以上が発生すれば、 NULL を返す。
static char * add_output_file_name(char *spit_out_file_path) { if ((output_files == NULL) || find_output_path(spit_out_file_path) == NULL) { if ((output_files = (char **)realloc( (void *)output_files, sizeof(output_files) * ++output_file_cnt) ) == (char **)NULL) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Can't allocate `output_files' " "(output_file_cnt = %d)\n", (void *)output_file_cnt); return NULL; } if ((output_files[output_file_cnt - 1] = (char *)malloc(strlen(spit_out_file_path) + 1) ) == NULL) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Can't allocate areas of " "output file path (%s).\n", (void *)spit_out_file_path); return NULL; } strcpy(output_files[output_file_cnt - 1], spit_out_file_path); } return make_tmp_file_path(spit_out_file_path); } |
出力ファイル名を記録している output_files に、 引数で渡された出力ファイル名を検索する。 検索できた場合は、そのアドレス位置を返し、 そうでなければ NULL を返す。
static char *find_output_path(char *spit_out_file_path) { char **found; int i; for (found = output_files, i = 0; i < output_file_cnt; ++i, ++found) { if (strcmp(*found, spit_out_file_path) == 0) { return *found; /* The file is stored, yet. */ } } return NULL; /* The file is new one. */ } |
引数で渡された出力ファイル・パスとこのプロセスのプロセスIDから、 一時ファイル名を作成する。
一時ファイル名は、 /tmp/<出力ファイル・パス>.<プロセスID (16進、8桁)> となる。
static char * make_tmp_file_path(char *spit_out_file_path) { size_t spit_out_file_path_sz = (size_t)(strlen(TMP_PATH) + strlen(spit_out_file_path) + (sizeof(pid) * 2) + 2); if (tmp_path_size < spit_out_file_path_sz) { if ((tmp_path = (char *)realloc(tmp_path, spit_out_file_path_sz)) == NULL) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Can't allocate `tmp_path'. " "`pid' = 0x%x", (void *)pid); print_err("(spit_out_file_path = \"%s\")\n", (void *)spit_out_file_path); } tmp_path_size = spit_out_file_path_sz; } (void)sprintf(tmp_path, TMP_PATH "%s.%x", spit_out_file_path, pid); return tmp_path; } |
output_files に記録しておいた出力ファイル名のそれぞれに対応する 一時ファイルをメモリ・マッピングし、 出力ファイルへ一時ファイルの内容を書き出す。
static EXIT_CODE update_outputs(void) { int i, s_desc; char **dest, *source; struct stat s_st; void *sp; for (i = 0, dest = output_files; i < output_file_cnt; ++i, ++dest) { /* Get the tmp-file path of `dest' file */ source = make_tmp_file_path(*dest); /* Mapping the `source' file to memory */ if ((sp = map_file(source, O_RDWR, &s_st)) == NULL) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Fail to mapping `source (%s)'.\n", (void *)source); return EXIT_FAIL_TO_MMAP_TMP_FILE; } if ((access(*dest, F_OK) == 0) && access(*dest, W_OK) != 0) { /** * If `dest' file is write-protected, * Discharge the tmp-file into `stdout' **/ discharge_file_contents(source, sp, &s_st); } else { /* Try to write out `dest' file */ create_output_file(*dest, source, sp, &s_st); } /* Unmapping `source' file */ if (munmap(sp, s_st.st_size) != 0) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Fail to unmapping `source (%s)' ", (void *)source); print_err("mapping to 0x%x.\n", (void *)sp); perror("ERRNO MSG"); return EXIT_FAIL_TO_UNMAP_TMP_FILE; } if (unlink(source) != 0) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Fail to unlink `source (%s)'.\n", (void *)source); perror("ERRNO MSG"); return EXIT_FAIL_TO_UNLINK_TMP_FILE; } } return EXIT_NORMAL; } |
ファイルを読み書き可モードでオープンし、
ファイルの stat 構造体を取得し、
メモリ・マッピングを行う。
リターン時には、ファイルはクローズしておく。
マッピングしたメモリ・アドレスを返す。
異常時には NULL を返す。
static void * map_file(char *path, int flags, struct stat *st) { int desc; void *p = NULL; if ((desc = open(path, O_RDWR | flags)) == -1) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Can't open (flags = 0x%x) ", (void *)(O_RDWR | flags)); print_err("\"%s\" file.\n", (void *)path); perror("ERRNO MSG"); } else if (fstat(desc, st) != 0) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Can't get stat-struct (at 0x%x) ", (void *)st); print_err("of \"%s ", (void *)path); print_err("(descriptor = %d)\" file.\n", (void *)desc); perror("ERRNO MSG"); } else if ((p = mmap(0, st->st_size, (PROT_READ|PROT_WRITE), MAP_PRIVATE, desc, 0) ) == MAP_FAILED) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Can't memory-mapping \"%s ", (void *)path); print_err("(descriptor = %d)\" file. ", (void *)desc); print_err("Size of the file is %u.\n", (void *)(st->st_size)); perror("ERRNO MSG"); p = NULL; /* `MAP_FAILED' is not `NULL'. */ } if (desc != -1) close(desc); return p; } |
static void discharge_file_contents(char *source, void *sp, struct stat *st) { printf("--- %s ---\n", source); if (write(STDOUT_FILENO, sp, st->st_size) < st->st_size) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Fail to discharge `source (%s, ", (void *)source); print_err("mapping at 0x%x)' into `stdout'. ", (void *)sp); print_err("The `source' size is %u.\n", (void *)(st->st_size)); perror("ERRNO MSG"); } return; } |
出力ファイル `dest' が存在していれば、それをメモリ・マッピングする。 新規ファイルやファイルサイズが一時ファイルと異なるような場合は、 一時ファイルをマッピングしているメモリ領域から新規作成した出力ファイルへ write(2) システムコールで更新を行う。 ファイルサイズが等しい場合は、 マッピングした領域同士を memcmp(3) 関数で比較し結果が異なる場合のみ memcpy(3) 関数でファイル内容のコピーを行う。
書き込みやオープンで異常が発生すれば、 discharge_file_contents() で一時ファイルの内容を標準出力へ吐き出す。
static void create_output_file(char *dest, char *source, void *sp, struct stat *st) { int desc; struct stat d_st; void *dp; /* When `dest' file is not exist, We must reate it! */ d_st.st_size = 0; dp = NULL; /* Memory-mapping `dest' file */ if ((((access(dest,F_OK) == 0) /* `dest' is exist */ && access(dest, W_OK) == 0) /* `dest' is writable */ && access(dest, R_OK) == 0) /* `dest' is readable */ && (dp = map_file(dest, O_RDWR, &d_st)) == NULL) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Fail to mapping `dest (%s).\n", (void *)dest); perror("ERRNO MSG"); discharge_file_contents(source, sp, st); } else if (d_st.st_size != st->st_size) { /** * If sizes of `dest' and `source' are differ, * Create the `dest' file. **/ if ((desc = open(dest, O_CREAT|O_WRONLY|O_TRUNC, (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP)) ) == -1) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Fail to open `dest (%s)' " "for write only, create & " "truncate.\n", (void *)dest); perror("ERRNO MSG"); discharge_file_contents(source, sp, st); } else { if (write(desc, sp, st->st_size) != st->st_size) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Fail to write out " "`dest (%s)' file. ", (void *)dest); print_err("The size of `source (%s, ", (void *)source); print_err("mapping at 0x%x)' ", (void *)sp); print_err("is %u.\n", (void *)(st->st_size)); perror("ERRNO MSG"); discharge_file_contents(source, sp, st); } if (close(desc) == -1) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Fail to close " "`dest (%s)'.\n", (void *)dest); perror("ERRNO MSG"); } } } else if (memcmp(dp, sp, st->st_size) != 0) { /** * If the mapping areas of `dest' and `source' * are differ, Memory-copy between those ares. **/ (void)memcpy(dp, sp, st->st_size); } if ((dp != NULL) && munmap(dp, d_st.st_size) != 0) { print_err_WHICH_LINE(__FILE__,__LINE__); print_err("Fail to unmapping `dest (%s, ", (void *)dest); print_err("mapping at 0x%x)'.\n", (void *)dp); perror("ERRNO MSG"); } return; } |
ここでは、USAGEやエラー/警告メッセージ表示などの ユーティリティ関数の実装を示している。 それぞれの概説と宣言内容は、 「ユーティリティ・ルーチン (プロトタイプ宣言)」を参照。
/** * Definitions of the Utilities **/ static void usage(char *command_name) { print_err(USAGE_FORMAT, (void *)command_name); } |
static void print_err_WHICH_LINE(char *F, int L) { (void)fprintf(stderr,"Oops! %s:",F); (void)fprintf(stderr, "%d: ",L); } |
static void print_err(char * format, void *val) { (void)fprintf(stderr, format, val); } |