CrunchWEB

Extract the source files from HTML files.

Version: $Revision: 1.10 $
Copyright (C) 1999 by Maoyam

使い方
(How to Use)
HTML 記述ルール
(Rules)
ソース・キット
(Source Kit)
変更点
(Change Log)
TODOリスト

"CrunchWEB" について

下の囲みにまとめたようなアイデアが、 "UNIX MAGAZINE 1999/12月号 (ASCII発行)" に紹介されていました。 記事の執筆者は、 ソニーコンピュータサイエンス研究所 増井俊之氏です。

そこで、「HTMLからソースコード部分を抜き出す」ツールを、 C 言語で実装してみました。
ツール名は "CrunchWEB" と呼ぶことにし、自由なソフトウェア(free software)として公開します。

スタンフォード大のDonald E. Knuth 教授が開発した 「文芸的プログラミング」ワークベンチ "WEB"は、 PASCAL言語とTeX文書処理システムの下、プログラムの ソースコードとドキュメンテーションを統合させる有名な試みであった。

その後、"WEB" は C, Fortran 等の各種プログラム言語向けに変種が作成されていった。 が、TeXの記述に習熟する必要があることや、 ドキュメンテーションを生成する作業 (weave) とプログラムを抜きだす作業 (tangle) が 2つの独立したパスになっていて繁雑なことから 一般のプログラマにひろく普及することはなかった。

ところで言語仕様が簡単な HTML 言語は、 多くのプログラマにとって手軽な "ドキュメンテーション・ツール" として普及している。 では、簡単なルールに従った HTMLファイルからソースコード部分を抜き出すツールがあれば、 簡易 "WEBワークベンチ" 環境を準備できるではないか!

HTML ファイルをブラウザで開くだけでドキュメンテーションが得られるので、 オリジナル "WEB" の weave のようなツールは不要になる。 しかも、扱えるプログラム言語には特にこだわらない上に、 他の情報ソースとリンクづけたり、 分割コンパイルなどの必要性にも対応できそうだ。

(この記事には、 実装例として wtangle と名付けられた Perlプログラムも掲載されている)


使い方

次に示す、記述ルールに従った HTML ファイルからソース・ファイルを抜き出すには、 次のようにします。

% crunch_WEB source.html

また、HTML ファイルは複数指定もできます。

% crunch_WEB source_1.html source_2.html

出力ファイルに変更がある場合に限って、 既存ファイルの上書きが行われます。

複数の HTML ファイルにまたがって埋め込まれたソース・ファイルは、 同一の出力ファイルとして再構成されます。 この様な使用法では、 HTML ファイルをコマンド・ライン指定する順序 に注意が必要です。

パーミッション不正などの理由で、 <pre>タグの "file"属性で指定されたファイル・パスへの出力ができない場合、 ソース・コードは標準出力に吐き出されます。

エラー・メッセージなどは、英文だけになっています。


HTML 記述ルール

"CrunchWEB" でソースコードを抜き出せるようにする HTML ファイル記述ルールは、以下のとおり。

  1. ソースコードは、次のように<pre> HTML タグで囲む。
    <pre file=ソース・ファイル名>
    ...ソースコード...
    </pre>

  2. ソース・ファイル名の部分は、 ダブル・クォートで囲んでもかまわない。
    <pre file="ソース・ファイル名">
  3. 一つのソース・ファイルの内容をいくつかの部分に分けた場合は、 HTMLファイル中の出現順で出力ファイルに追加されていく。 つまり、次のように記述すれば....
    <pre file="a.c">
    int main() {
    </pre>
    "Hello World"を表示。
    <pre file="a.c">
          printf("Hello World!\n");
    }
    </pre>
    出力ファイル "a.c" は、このようになる。
    
    int main() {
          printf("Hello World!\n");
    }

ソースコードとドキュメンテーション

以下に、"CrunchWEB" のソースコードと各部分の説明を示します。
また、このHTMLファイル自体も"CrunchWEB" 対応になっています。

"CrunchWEB" ソース一式は この tar-ball にまとめてあります。


INDEX


クレジット表記

crunchWEBのソースコードクレジット部分。
GNU一般公有使用許諾書(バージョン2) に従って再配布可能とします。

/*****************************************************************************
 * File: crunch_WEB.c
 * 	(Extracted from
 *	   "$Id: crunchWEB.html,v 1.10 2001/05/19 11:36:12 ma Exp ma $")
 *
 * Abstracts:
 *  This program extracts source files from the HTML files.
 *  (The more informations:
 *     "http://sapporo.cool.ne.jp/maoya/crunchWEB/crunchWEB.html")
 *
 * Copyright (C) 1999 Maoyam Tokyo, Japan (mailto:maoyam@mail.goo.ne.jp)
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *
 ****************************************************************************/


インクルード・ファイルとデバッグ・レベル

UNIXプラットフォームでのインクルードを行なう。 しかし、このパラグラフでのソースコード表示部分でインクルードファイル名が Webブラウザに表示されない。 これは、HTMLのタグと C言語のシステム定義インクルード記述が文法上の衝突しているため。 実際にインクルードされているファイル名は、次に列挙したものになっている。

unistd.h-- Unix Standard Definitions
stddef.h-- ANSI-C Standard Definitions
stdio.h-- ANSI-C Standard I/O Streams Definitions
errno.h-- Unix System-Call Error Number Definitions
limits.h-- Unix (POSIX) System Resource Limits Definitions
ctype.h-- ANSI-C Character Type Utility Definitions
string.h-- ANSI-C Strings Utility Definitions
sys/types.h-- Unix Types Definitions
sys/stat.h-- Unix File Status Definitions (for file size)
sys/mman.h-- Unix Memory Mapping Files/Devices Definitions
(needs to define _POSIX_MAPPED_FILES)
fcntl.h-- ANSI-C File Descriptor Control Definitions

/**
 * Refer the Platform's Definitions
 **/
#include 	/* unistd.h*/
#include 	/* stddef.h*/
#include 	/* stdio.h*/
#include 	/* errno.h*/
#include 	/* limits.h*/
#include 	/* ctype.h*/
#include 	/* string.h*/
#include 	/* sys/types.h */
#include 	/* sys/stat.h */
#include 	/* sys/mman.h */
#include 	/* fcntl.h */

また、DEBUGはデバッグ・レベルを指定するための変数。 C言語で一般的な#defineによる方式と違って、 Debugger 上で動的にデバッグ・レベルを変更する事が可能になる。

/**
 * DEBUG Level Switch ('0' means *NO-DEBUG*)
 **/
static int DEBUG = 0;


プログラム終了コード

このプログラムの終了コードを定義している。 正常終了は、EXIT_NORMAL(=0)となる。 また、コマンド・ライン・オプションの不正は EXIT_ILLEGAL_CMD_ARGS (=255)となる。 これら以外のエラーコードは、次のエラー状況を表わしている。
SYMBOLCode Value Error Situations
EXIT_FAIL_FOPEN 1 入力ファイル(HTMLファイル)オープン異常終了
EXIT_FAIL_FPUTS 2 ソースコード断片のファイル出力異常終了
EXIT_FAIL_CLOSE_OUTPUT 3 出力ファイル・クローズ異常終了
EXIT_FAIL_CLOSE_OUTPUT_OUT_OF_PARSE_LOOP 4 出力ファイル・クローズ異常終了 (出力ファイルをオープンしたまま、入力ファイルの読込が終了した。 HTMLファイルに</pre>タグが抜けている)
EXIT_BROKEN_FSM_STATE_CONTROL 5 このプログラム内のパーサを制御している有限状態機械が、 異常な遷移状態に落ち込んだ
EXIT_FAIL_CLOSE_INPUT_STREAM 6 入力ファイル(HTMLファイル)クローズ異常終了
EXIT_FAIL_TO_MMAP_TMP_FILE 7 一時ファイル メモリ・マッピング異常終了
EXIT_FAIL_TO_UNMAP_TMP_FILE 8 一時ファイル アンマッピング異常終了
EXIT_FAIL_TO_UNLINK_TMP_FILE 9 一時ファイル 削除 (unlink(2)) 異常終了

/**
 * This Program's Exit Codes
 **/
typedef enum EXIT_CODE_t {
	 EXIT_NORMAL = 0
	,EXIT_FAIL_FOPEN = 1
	,EXIT_FAIL_FPUTS
	,EXIT_FAIL_CLOSE_OUTPUT
	,EXIT_FAIL_CLOSE_OUTPUT_OUT_OF_PARSE_LOOP
	,EXIT_BROKEN_FSM_STATE_CONTROL
	,EXIT_FAIL_CLOSE_INPUT_STREAM
	,EXIT_FAIL_TO_MMAP_TMP_FILE
	,EXIT_FAIL_TO_UNMAP_TMP_FILE
	,EXIT_FAIL_TO_UNLINK_TMP_FILE
	,EXIT_ILLEGAL_CMD_ARGS =255
} EXIT_CODE;


パーサ関連の宣言・定義

ここでは、"WEB Styled HTML" ファイルからソースファイルを切り出すパーサに関わる宣言などを示す。
パーサのエントリとなるのは、
cruncher()関数。

/**
 * Declarations around "Parser for WEB Styled HTML files"
 **/
static int  cruncher(char * web_file_path); /* Parser Entry */

このパーサが理解するトークンは BEGIN_TAG ("<pre")・ TAG_ATTR_FILEPATH ("file=")・ END_TAG ("</pre>")、 出力ソースファイル名パスの区切りなどのデリミタは EOL (\0)・ TAG_RPAREN (>)・ FILEPATH_QUOTE (") として定義されている。

	/* Declare Tokens and Delimiters */
static char	 *BEGIN_TAG = "<" "pre"
		,*TAG_ATTR_FILEPATH = "file="
		,*END_TAG = "<" "/pre>"
		,EOL = '\0'
		,TAG_RPAREN = '>'
		,FILEPATH_QUOTE = '"';

パーサ処理中のコンテクスト情報を管理するためのデータ構造は、 CRUNCH_CONTEXT構造体で維持される。 コンテクストを構成する情報は、次表のとおり。

MEMBER SYMBOL COMMENT
web_file_path 入力元になるHTMLファイル・パス
instream 入力ファイルからのストリーム
line[LINE_MAX + 1] 入力行バッファ。 POSIX標準に従い、一行は /usr/include/limits.h 中に定義されている
LINE_MAX 個の文字だけが含まれていると仮定している。
line_no 現在読み込んだ行の、入力ファイル中の行番号。
spit_out_stream 切り出されるソースコード断片の出力先ストリーム
	/* Define the Context of Parsing WEB Styled HTML Files */
typedef struct crunch_context_t {
	char	*web_file_path;	/* Path of the WEB Styled HTML File */
	FILE	*instream;	/* Input stream from "web_file_path" */
	char	line[LINE_MAX + 1]; /* Read line buffer */
	long int line_no;	/* Current read line number */
	FILE	*spit_out_stream; /* Output stream */
} CRUNCH_CONTEXT;

パーサの内部状態を有限状態機械 (Finite State Machine; FSM) として表現した。 この状態機会がとるすべての遷移状態を、 CRUNCH_STATES型に定義している。
状態遷移表は次のとおり。
入力行読み捨て中
CRUNCH_IGNORE
(初期状態)
ソース断片出力中
CRUNCH_SPIT_IT
読込行にBEGIN_TAGあり ACTION: 出力ファイル・パスをオープン
→ CRUNCH_SPIT_IT
ACTION: 読込バッファ内容を出力
→ CRUNCH_SPIT_IT
読込行にEND_TAGあり ACTION: 読込バッファ内容は読み捨て
→ CRUNCH_IGNORE
ACTION: 出力ストリームをクローズ
→ CRUNCH_IGNORE
上記以外の内容の読込行 ACTION: 読込バッファ内容は読み捨て
→ CRUNCH_IGNORE
ACTION: 読込バッファ内容を出力
→ CRUNCH_SPIT_IT
入力ファイルの終端に到達 ACTION: 入力ストリームをクローズ
→ CRUNCH_IGNORE
ACTION: 入出力ストリームをクローズ
→ CRUNCH_IGNORE

状態遷移表のACTIONを実装する関数プロトタイプは、 CRUNCH_ACTION関数ポインタ型定義で示される。

	/* Definition States of this Finite State Machine (FSM) */
typedef enum CRUNCH_STATES_t {
	 CRUNCH_IGNORE = 0
	,CRUNCH_SPIT_IT
	,MAX_CRUNCH_STATES
	,CRUNCH_ILLEGAL_STATE_CONTROL
} CRUNCH_STATES;

	/* The "CRUNCH_ACTION" is a pointer to a action-function. */
typedef CRUNCH_STATES (*CRUNCH_ACTION)(CRUNCH_CONTEXT *context,
					CRUNCH_STATES state);

状態遷移表の各列のACTIONを扱うのは、以下の2関数。
"入力行読み捨て中"状態のACTION群は seek_BEGIN_TAG()、 "ソース断片出力中"状態のACTION群は spit_it_out() でそれぞれ実装されている。

これらのACTION実装関数とそれぞれが受け持つ遷移状態の関連付けは、 action_table[]配列で定義されている。

	/* Declare Actions and FSM-table */
static CRUNCH_STATES seek_BEGIN_TAG(CRUNCH_CONTEXT *context,
					CRUNCH_STATES state);
static CRUNCH_STATES spit_it_out(CRUNCH_CONTEXT *context,
					CRUNCH_STATES state);
static CRUNCH_ACTION action_table[] = {
			 seek_BEGIN_TAG /* at CRUNCH_IGNORE */
			,spit_it_out	/* at CRUNCH_SPIT_IT */
		};

以下のいくつかの関数は、パーサ処理の下請け的な処理を行なう。
dump_FSM_context()-- パーサのコンテクスト情報をダンプする
strncmp_ignore_case()-- 大文字・小文字の違いは無視する文字列比較関数
search_spit_out_file_path()-- BEGIN_TAG中の "file"属性値(つまり、ソースファイルパス)
を取り出す
is_TAG_ATTR_FILEPATH_delimit()-- "file"属性値の区切り文字(デリミタ)判別関数

	/* CRUNCH_CONTEXT Dumper */
static void dump_FSM_context(FILE *stream, char *header,
				CRUNCH_CONTEXT *c);

	/* Declare methods depended on Tokens and Delimiters */
static int  strncmp_ignore_case(char * tag, char *line, size_t sz);
static char *search_spit_out_file_path(char *line);
static int  is_TAG_ATTR_FILEPATH_delimit(char code);


出力ファイル更新にかかわる定義・宣言

ここでは、この仕組みのために必要な定義を行っている。
出力ファイルの内容は、一旦、一時ファイル (/tmp/下) に吐き出される。 これらの一時ファイルと、 指定された出力ファイルの内容に変化があったものだけが、 実際に更新される。

一時ファイルのファイル名は、<pre>タグの file 属性で指定された出力ファイル名に このプログラムのプロセスIDを付加したモノにする。

/**
 * Definition to update output files
 **/
#define	TMP_PATH	"/tmp/" /* Path to templates */
static pid_t	pid; /* For making template file names */
		/* Working buffer of template file path */
static size_t	tmp_path_size = (size_t)0L;
static char	*tmp_path = NULL;
static int	output_file_cnt = 0; /* Count of output file names */
static char	**output_files = NULL; /* List of output file names */

	/* Functions to update output files */
static char	*add_output_file_name(char *spit_out_file_path);
static char	*find_output_path(char *spit_out_file_path);
static char	*make_tmp_file_path(char *spit_out_file_path);
static EXIT_CODE update_outputs(void);
static void 	*map_file(char *path, int flags, struct stat *st);
static void	discharge_file_contents(char *source,
					void *sp, struct stat *st);
static void	create_output_file(char *dest, char *source,
					void *sp, struct stat *st);


ユーティリティ・ルーチン (プロトタイプ宣言)

USAGEやエラー/警告メッセージを表示するなどの便利なルーチンについて、 プロトタイプ宣言している。
SYMBOL COMMENT
usage() Usage (プログラムの使い方の簡易説明)を表示
例: % crunch_WEB crunchWEB.html
print_err_WHICH_LINE() エラー/警告が発生したソースファイル名と
ソース中の行番号を標準エラー出力に表示する
print_err() エラー/警告メッセージをフォーマットにしたがって、 標準エラー出力に表示する

/**
 * The Utilities
 **/
static void usage(char *command_name);
#	define	USAGE_FORMAT				\
	"CrunchWEB ($Revision: 1.10 $)\n"			\
	"Copyright 1999, Maoyam (maoyam@mail.goo.ne.jp)\n\n" \
	"This may be copied only under "		\
	"the GNU General Public License, "		\
	"which may be found in the CruncWEB source kit " \
	"(see "						\
	"http://sapporo.cool.ne.jp/maoya/crunchWEB/crunchWEB.html" \
	").\n\n" \
	"Usage: %s WEB_Styled_HTML_File_Path...\n"
static void print_err_WHICH_LINE(char *F, int L);
static void print_err(char * format, void *val);


プログラム・エントリ(main()関数)

ここでは、"crunchWEB"のプログラム・エントリについて説明する。

/**
 * Program Entry...
 **/
int
main(int ac, char **av) {
	int	i, exit_code;

if (DEBUG>0) (void)fprintf(stderr,"+++++DEBUG=%d++: "
"Entry main(%d, 0x%x[0] = [%s])...\n", DEBUG, ac, av, av[0]);

コマンド引数には、一つ以上の "WEB Styled HTML"ファイルを指定する。
これに違反している場合は、USAGEを表示し、 "コマンド引数不正"でプログラムを終了する。

	if (ac < 2) {
		usage(av[0]);
		exit_code = EXIT_ILLEGAL_CMD_ARGS;
	}

コマンド引数の確認が済むと、一時ファイル名生成に必要な、 このプログラムのプロセスIDを得る。

	else {
			/* `pid' for making template file names */
		pid	= getpid();

コマンド引数に指定された入力ファイル毎に、 読込アクセス許可を確認してから、 パーサ関数に入力ファイル・パスを渡していく。 入力アクセスが許可されないファイルについては警告メッセージを表示し、 それ以外の指定ファイルの処理を進める。 パーサ関数がエラーを報告したら、 その終了コードでプログラムを即時中止する。

		for (exit_code = EXIT_NORMAL, i = 1; i < ac; ++i) {
			if (access(av[i], R_OK) != 0) {
				print_err_WHICH_LINE(__FILE__,__LINE__);
				print_err("Can't read av[%d] ",
							(void *)i);
				print_err("\"%s\"\n", (void *)av[i]);
			} else if ((exit_code = cruncher(av[i])) != 0) {
				break;
			}
		}

全ての入力ファイル・パスをパーサ処理した後で、 実際の出力ファイルと変更があるモノについて上書きを行う。 この処理中になんらかの以上が発生すれば、直ちにプログラムを終了する。

		if (exit_code == EXIT_NORMAL) {
			exit_code	= update_outputs();
		}
	}

プログラムの終了は、必ずここで行う

if (DEBUG>0) (void)fprintf(stderr,"+++++DEBUG=%d++: "
"Exit from main(). Exit Code is %d.\n", DEBUG, exit_code);

	return	exit_code;
}


パーサ処理の実装

ここでは、 "CrunchWEB"プログラムのパーサ処理の実装について説明する。
処理対象となる入力ファイルのパスが、
main()関数から引数で渡されてくる。
戻り値は、「プログラム終了コード」 に定義されたプログラム終了コード。

パーサに関する宣言・定義については、 「パーサ関連の宣言・定義」 を参照。

/**
 * Definitions for Parser of WEB Styled HTML files
 **/
static int
cruncher(char * web_file_path) {
	static CRUNCH_CONTEXT	context;
	CRUNCH_CONTEXT	*c = &context;
	CRUNCH_STATES	state, next_state;

パーサ処理の初期段階は、 コンテクスト情報を初期化する事から始まる。
この時、入力ファイルのREAD ONLYオープンも行っている。

		/* Initialize the context and FSM-state */
	state = CRUNCH_IGNORE;
	c->web_file_path	= web_file_path;
	c->spit_out_stream = (FILE *)NULL;

		/* Open the input stream */
	c->instream = fopen(web_file_path,"r");
	if (c->instream ==NULL) {
		print_err_WHICH_LINE(__FILE__,__LINE__);
		print_err("Fail fopen(%s,\"r\")\n",
				(void *)c->web_file_path);
		return	(int)EXIT_FAIL_FOPEN;
	}

コンテクスト情報の初期化が済むと、パーサのメイン・ループに進む。
このループは、 入力ストリームからの読み出しがファイル終端 (EOF) に到達するまで、以下の処理を繰り返している。

  1. 読込バッファの初期化をしてから、一行分のファイル読込
  2. 読込んだバッファの内容を解釈するために、 action_table[]で定義付けられている現在の遷移状態に対応した ACTION 関数を、 コンテクスト情報と現在の遷移状態を渡して呼び出す
  3. ACTION関数の戻り値が不正な状態遷移の検出を示していたら、 エラーメッセージを表示して直ちに処理を終了
  4. ACTION関数の戻り値を次の遷移状態としてループを繰り返していく。 この時、読込行の行番号も更新

		/* Parser Loop */
	for (c->line_no = 1L;
		fgets((char *)memset((void *)c->line,
					(int)EOL,
					sizeof(c->line)),
			sizeof(c->line) - 1, c->instream) != NULL;
		++(c->line_no), state = next_state)
	{
		next_state = (action_table[state])(c, state);
		if (next_state == CRUNCH_ILLEGAL_STATE_CONTROL) {
			print_err_WHICH_LINE(__FILE__, __LINE__);
			print_err("Broken FSM-state Control "
				"(Last State = %ld)\n", (void *)state);
			dump_FSM_context(stderr, "\t", c);
			return	EXIT_BROKEN_FSM_STATE_CONTROL;
		}
	}

パーサ・ループから抜けてきた時、入力ファイルがEND_TAG (</pre>) を書き忘れていた場合に対応する必要がある。
このような入力ファイルでは、 終了直後の遷移状態が "CRUNCH_SPIT_IT" のままになっている。 さらに、出力ストリームが stdout 以外になっていれば、 それをクローズしなければいけない。
この対応をしないと、 複数の入力ファイルがコマンド・ラインで指定されていた場合に、 同時ファイル・オープン数の限界値を超えてしまいかねないので、重要。

出力ストリームのクローズ時に異常が発生したら、 エラーメッセージを表示してプログラムを即時終了させる。

		/**
		 * Now, we found out *END_TAG*.
		 * We must close output stream has been opened file.
		 **/
	if ((c->spit_out_stream != stdout) && state != CRUNCH_IGNORE) {
		if (fclose(c->spit_out_stream) != 0) {
			print_err_WHICH_LINE(__FILE__,__LINE__);
			print_err("Fail Close Output Stream (0x%x), "
				"at out of parser loop\n",
				(void *)c->spit_out_stream);
			perror("ERRNO MSG");

			return EXIT_FAIL_CLOSE_OUTPUT_OUT_OF_PARSE_LOOP;
		}
		c->spit_out_stream	= (FILE *)NULL;
	}

最後に残された処理は、入力ストリームのクローズだけになる。

入力ストリームのクローズ時に異常が発生したら、 エラーメッセージを表示しプログラムを即時中止させる。

		/* Close the input stream */
	if (fclose(c->instream) != 0) {
		print_err_WHICH_LINE(__FILE__,__LINE__);
		print_err("Fail Close Input Stream (0x%x), "
			"at out of parser loop\n",
			(void *)c->instream);
		perror("ERRNO MSG");

		return EXIT_FAIL_CLOSE_INPUT_STREAM;
	}
	c->instream	= (FILE *)NULL;

	return	EXIT_NORMAL;
}


「入力行読み捨て中状態 (CRUNCH_IGNORE)」のACTION定義
seek_BEGIN_TAG()関数の実装

ここでは、パーサが「入力行読み捨て中」状態にある時の、 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;
}


「ソース断片出力中」状態 (CRUNCH_SPIT_IT)のACTION定義
spit_it_out()関数の実装

ここでは、パーサの状態が「ソース断片出力中」時の 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;
}


大文字・小文字の違いを無視する文字列比較関数:
strncmp_ignore_case() の実装

比較される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;
}


出力ファイル・パス名を BEGIN_TAG の "file"属性から取得する関数:
search_spit_out_file_path() の実装

コンテクスト構造の入力バッファは、 出力ファイル・パス名の終端文字位置に 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;
}


BEGIN_TAGの "file"属性値の終端文字(デリミタ)判別関数:
is_TAG_ATTR_FILEPATH_delimit() の実装

static int
is_TAG_ATTR_FILEPATH_delimit(char code) {
	return	code == EOL
		|| code == TAG_RPAREN
		|| code == FILEPATH_QUOTE
		|| isspace((int)code);
}


パーサ・コンテクスト構造 (CRUNCH_CONTEXT) ダンプ関数:
dump_FSM_context() の実装

任意のストリームを出力先に指定できる。

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);
}

出力ファイルの更新にかかわる処理の実装

ここでは、 一時ファイルへ退避したソース・コードと出力先ファイルの内容に、 変更があった場合に行うファイル更新処理の実装を示す。


出力ファイル・パスの記録
add_output_file_name()の実装

<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);
}


出力ファイル名記録の検索
find_output_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. */
}


一時ファイル名の作成
make_tmp_file_path() の実装

引数で渡された出力ファイル・パスとこのプロセスのプロセス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;
}


変更された出力ファイルの更新処理
update_outputs() の実装

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;
}


ファイルをメモリ・マップする
map_file() の実装

ファイルを読み書き可モードでオープンし、 ファイルの 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;
}


一時ファイルの内容を標準出力に吐き出す
discharge_file_contents() の実装

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;
}


出力ファイルへの書き込み
create_output_file() の実装

出力ファイル `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やエラー/警告メッセージ表示などの ユーティリティ関数の実装を示している。 それぞれの概説と宣言内容は、 「ユーティリティ・ルーチン (プロトタイプ宣言)」を参照。

usage()関数の実装

/**
 * Definitions of the Utilities
 **/
static void
usage(char *command_name) {
	print_err(USAGE_FORMAT, (void *)command_name);
}

print_err_WHICH_LINE()関数の実装

static void
print_err_WHICH_LINE(char *F, int L) {
	(void)fprintf(stderr,"Oops! %s:",F);
	(void)fprintf(stderr, "%d: ",L);
}

print_err()関数の実装

static void
print_err(char * format, void *val) {
	(void)fprintf(stderr, format, val);
}


変更履歴 (Change Log)

  • Fri Dec 3 1999
    -- 出力ファイルの内容に変更があった場合だけ、上書きを行う。
    -- 複数の入力 HTML ファイルにまたがって埋め込まれた出力内容を正しく扱う。

TODOリスト

  • 標準出力 (stdout) には shell archive 形式で吐き出すようにしたい。
  • ソース・キットのできを、もうちょっとなんとかしたい

--- Happy Hacking! ---