シェルもどきな簡易コマンドパーサを試作した

ちょっと事情があって、バッチファイルやシェルの簡易版みたいな構文のパーサが欲しくなった。作りたいツールの内容的に、簡単なコマンド(というかスクリプト)で操作するようにしたいのだ。

多分調べ方が不十分だと思うのだが、この手のパーサのライブラリは見たことがない。既存のツールからソースを持ってくるとなると、各ツールの事情がまとわりついてきそうだ。

今回作りたいツールは仕事絡みだが、多分この手のパーサは仕事以外でも使いたくなると思う(今までの自分の実績的に)。なのでプライベートな時間に試作版をでっち上げてみた。

/*
 * Copyright (c) 2011 eel3 @ TRASH BOX <dov045a@yahoo.co.jp>
 *
 * This software is provided 'as-is', without any express or implied
 * warranty. In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 *     1. The origin of this software must not be misrepresented; you must not
 *     claim that you wrote the original software. If you use this software
 *     in a product, an acknowledgment in the product documentation would be
 *     appreciated but is not required.
 *
 *     2. Altered source versions must be plainly marked as such, and must not be
 *     misrepresented as being the original software.
 *
 *     3. This notice may not be removed or altered from any source
 *     distribution.
 */

/* ********************************************************************** */
/**
 * @brief   簡易コマンドパーサもどき(試作版)
 * @author  eel3 @ TRASH BOX
 * @date    2011/11/20
 *
 * @par 動作確認済み環境:
 *   - Microsoft Windows XP Professional (32bit) SP3
 *
 * @par 確認済みコンパイラ:
 *   - TDM-GCC 4.5.1
 *
 * @bug
 *   - そもそも仕様が不完全(というか深く考えていない)。
 */
/* ********************************************************************** */


#include <assert.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


/* ---------------------------------------------------------------------- */
/* データ型 */
/* ---------------------------------------------------------------------- */

#if (defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L)

	#include <stdbool.h>
	#include <stdint.h>

#else /* (defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L) */

	typedef  unsigned char   uint8_t;       /**<  8bit符号無し整数型 */

	#ifndef __cplusplus
		typedef  uint8_t  bool;

		#define  false  ((bool) 0)
		#define  true   ((bool) 1)
	#endif

	#define  _Bool  bool

#endif /* (defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L) */


/* ---------------------------------------------------------------------- */
/* コンフィグレーション */
/* ---------------------------------------------------------------------- */

/** コマンド入力の最大文字数 */
#define  MAX_INPUT_LINE  1024

/** ファイルパスの区切り文字 */
#define  PATH_SEP  '\\'


/* ---------------------------------------------------------------------- */
/* その他定数など */
/* ---------------------------------------------------------------------- */

/** コマンド入力の最大トークン数 */
#define  MAX_TOKEN  (MAX_INPUT_LINE / 2 + 1)
/** 各トークンの区切り文字 */
#define  CMD_SEP  " \t"

/** エラーコード */
enum {
	E_OK = 0,       /**< 正常終了 */
	E_NG = -1,      /**< 異常終了 */
	E_EOF = -2      /**< EOF検出 */
};


/* ----------------------------------------------------------------------
 * ファイル変数
 * ---------------------------------------------------------------------- */

/** プログラム名 */
static const char *program_name = "";


/* ---------------------------------------------------------------------- */
/* マクロ関数 */
/* ---------------------------------------------------------------------- */

/** |c| が改行文字なら真値を返す */
#define  IS_EOL(c)  (((c) == '\r') || ((c) == '\n'))

/** 配列 |ary| の要素数を返す */
#define  NELEMS(ary)  (sizeof(ary) / sizeof((ary)[0]))


/* ---------------------------------------------------------------------- */
/* エラー出力関連 */
/* ---------------------------------------------------------------------- */

/* ====================================================================== */
/**
 * @brief  エラー出力して終了する関数。標準エラーに出力される。
 *         末尾に改行が付加される。
 *
 * @param[in] *fmt  データフォーマット(printf準拠)
 * @param[in] ...   *fmtの指定に従った、任意の数のデータ項目
 */
/* ====================================================================== */
static void eprintf(const char *fmt, ...)
{
	FILE * const out = stderr;
	va_list args;

	assert(fmt != NULL);

	(void) fputs(program_name, out);
	(void) fputs(": ", out);

	va_start(args, fmt);
	(void) vfprintf(out, fmt, args);
	va_end(args);

	(void) fputc('\n', out);

	(void) fflush(out);
	exit(EXIT_FAILURE);
}


/* ---------------------------------------------------------------------- */
/* コマンド読み取り関連 */
/* ---------------------------------------------------------------------- */

/**
 * @brief  コマンドライン入力コンテキスト
 *
 * @note
 *   コマンドライン入力に関するコンテキスト情報。
 *   コマンドが入力されたら、main()のargc、argvのような形式にトークン分割する。
 */
struct CMDLINE_CTX {
	/** 入力ストリーム */
	FILE *in;

	/** 現在処理中の行番号 */
	unsigned long line_no;

	/** 入力されたコマンドをここに読み込み、直接トークン分割する */
	char line[MAX_INPUT_LINE + 1];

	/** コマンドの各トークンへのポインタ配列。
	 *  末尾にNULLポインタを追加する為、要素を1つ余分に確保している。
	 */
	char *argv[MAX_TOKEN + 1];

	/** コマンドのトークン数 */
	int argc;
};


/* ====================================================================== */
/**
 * @brief  コマンドライン入力コンテキストを初期化する
 *
 * @param[out] *cl  コマンドライン入力コンテキスト
 * @param[in]  *in  使用する入力ストリーム
 */
/* ====================================================================== */
static void init_cmdline_ctx(struct CMDLINE_CTX *cl, FILE *in)
{
	assert(cl != NULL);

	cl->in = in;
	cl->line[0] = '\0';
	cl->argv[0] = NULL;
	cl->argc = 0;
	cl->line_no = 0;
}

/* ====================================================================== */
/**
 * @brief  コマンドライン入力コンテキストをリセットする
 *
 * @param[out] *cl  コマンドライン入力コンテキスト
 */
/* ====================================================================== */
static void reset_cmdline_ctx(struct CMDLINE_CTX *cl)
{
	assert(cl != NULL);

	init_cmdline_ctx(cl, cl->in);
}

/* ====================================================================== */
/**
 * @brief  コマンドの区切り文字かどうかチェックする
 *
 * @param[in] c  チェックする文字
 *
 * @retval true   区切り文字である
 * @retval false  区切り文字ではない
 */
/* ====================================================================== */
static _Bool iscmdsep(int c)
{
	const char *p;

	for (p = CMD_SEP; *p != '\0'; ++p) {
		if (*p == (char) c) {
			return true;
		}
	}
	return false;
}

/* ====================================================================== */
/**
 * @brief  入力ストリームからコマンドの区切り文字を読み飛ばす
 *
 * @param[in,out] *in  入力ストリーム
 *
 * @retval E_OK   正常終了
 * @retval E_EOF  EOF検出
 */
/* ====================================================================== */
static int skip_cmd_seps(FILE *in)
{
	int c;

	assert(in != NULL);

	while ((c = fgetc(in)) != EOF) {
		if (!iscmdsep((unsigned char) c)) {
			(void) ungetc(c, in);
			return E_OK;
		}
	}

	return E_EOF;
}

/* ====================================================================== */
/**
 * @brief  改行かどうかチェックする。CRLFの場合はLFを読み飛ばす。
 *
 * @param[in] c  チェックする文字
 *
 * @retval true   改行である
 * @retval false  改行ではない
 */
/* ====================================================================== */
static _Bool iseol(int c, FILE *in)
{
	assert(in != NULL);

	if (c == '\n') {
		return true;
	}

	if (c == '\r') {
		int ch;

		if (((ch = fgetc(in)) != EOF) && (ch != '\n')) {
			(void) ungetc(ch, in);
		}
		return true;
	}

	return false;
}

/* ====================================================================== */
/**
 * @brief  入力ストリームからコマンドを読み取り、
 *         main()のargc、argvのような形式に変換して返す
 *
 * @param[in,out] *cl  コマンドライン入力コンテキスト
 *
 * @retval E_OK   正常終了
 * @retval E_EOF  EOF検出
 */
/* ====================================================================== */
static int read_cmd(struct CMDLINE_CTX *cl)
{
	size_t i;
	int c;

	assert(cl != NULL);

RETRY:
	++cl->line_no;

	i = 0;
	cl->argc = 0;
	cl->argv[0] = NULL;

	for (;;) {
		if (skip_cmd_seps(cl->in) == E_EOF) {
			if (cl->argc == 0) {
				return E_EOF;
			}
			cl->argv[cl->argc] = NULL;
			break;
		}
		c = fgetc(cl->in);
		assert(c != EOF);

		if (iseol(c, cl->in)) {
			if (cl->argc == 0) {
				goto RETRY;
			}
			cl->argv[cl->argc] = NULL;
			break;
		}

		cl->argv[cl->argc++] = &cl->line[i];
		if (cl->argc >= (int) NELEMS(cl->argv)) {
			eprintf("line %lu: too many token", cl->line_no);
		}

		if (c == '\"') {
			/* TODO  "" で括られた文字列は入れ子には対応していない */
			while ((c = fgetc(cl->in)) != EOF) {
				/* FIXME  ここで改行をカウントすると、パース後の処理で行番号を使うときに困るかも */
				if (c == '\n') {
					++cl->line_no;
				}
				if (c == '\r') {
					int ch;

					if ((ch = fgetc(cl->in)) == EOF) {
						/*EMPTY*/
					} else if (ch == '\n') {
						(void) ungetc(ch, cl->in);
					} else {
						(void) ungetc(ch, cl->in);
						++cl->line_no;
					}
				}
				if (c == '\"') {
					/* TODO  ひとまず "aaa"a のような入力はエラーとする */
					if ((c = fgetc(cl->in)) == EOF) {
						break;
					}
					if (iscmdsep((unsigned char) c) || IS_EOL(c)) {
						(void) ungetc(c, cl->in);
						break;
					}
					eprintf("line %lu: unexpected char `%c\' after `\"\'", cl->line_no, c);
				}
				if (c == '\\') {
					if ((c = fgetc(cl->in)) == EOF) {
						eprintf("line %lu: unexpected EOF", cl->line_no);
					}
					/* TODO  エスケープ文字は 1文字のものだけに対応 */
					switch (c) {
					case 'a': c = '\a'; break;
					case 'b': c = '\b'; break;
					case 'f': c = '\f'; break;
					case 'n': c = '\n'; break;
					case 'r': c = '\r'; break;
					case 't': c = '\t'; break;
					case 'v': c = '\v'; break;
					case '\\': break;
					case '\"': break;
					default:
						eprintf("line %lu: unexpected excape code `\\%c\'", cl->line_no, c);
					}
				}
				cl->line[i++] = c;
				if (i >= sizeof(cl->line)) {
					eprintf("line %lu: too long line", cl->line_no);
				}
			}
		} else {
			/* TODO  "" で括らない場合、エスケープ文字には対応していない */
			for (;;) {
				cl->line[i++] = c;
				if (i >= sizeof(cl->line)) {
					eprintf("line %lu: too long line", cl->line_no);
				}
				if ((c = fgetc(cl->in)) == EOF) {
					break;
				}
				if (iscmdsep((unsigned char) c) || IS_EOL(c)) {
					(void) ungetc(c, cl->in);
					break;
				}
			}
		}
		cl->line[i++] = '\0';
	}

	return E_OK;
}


/* ---------------------------------------------------------------------- */
/* メインルーチン部分 */
/* ---------------------------------------------------------------------- */

/* ====================================================================== */
/**
 * @brief  ファイルパスからファイル名の部分のみを取り出す
 *
 * @param[in] *name  ファイルパス
 *
 * @return  ファイル名部分の開始位置
 */
/* ====================================================================== */
static const char *my_basename(const char * const name)
{
	const char *bn;

	assert(name != NULL);

	bn = strrchr(name, PATH_SEP);
	return (bn == NULL) ? name : bn+1;
}

/* ********************************************************************** */
/**
 * @brief  テスト用メインルーチン
 *
 * @return  常にEXIT_SUCCESS
 */
/* ********************************************************************** */
int main(int argc, char *argv[])
{
	static struct CMDLINE_CTX cl;

	(void) argc;

	program_name = my_basename(argv[0]);

	init_cmdline_ctx(&cl, stdin);
	while (read_cmd(&cl) != E_EOF) {
		int i;

		(void) printf("line %lu\n"
		              "argc == %d\n", cl.line_no, cl.argc);
		for (i = 0; i < cl.argc; ++i) {
			(void) printf("argv[%d] == [%s]\n", i, cl.argv[i]);
		}
		(void) putchar('\n');
	}
	reset_cmdline_ctx(&cl);

	return EXIT_SUCCESS;
}

簡単な構文で十分なので色々と手抜きしているし、仕様もあまり固めていない。まあ、これをベースに改めて考えれば何とかなるか……。