ちょっと事情があって、バッチファイルやシェルの簡易版みたいな構文のパーサが欲しくなった。作りたいツールの内容的に、簡単なコマンド(というかスクリプト)で操作するようにしたいのだ。
多分調べ方が不十分だと思うのだが、この手のパーサのライブラリは見たことがない。既存のツールからソースを持ってくるとなると、各ツールの事情がまとわりついてきそうだ。
今回作りたいツールは仕事絡みだが、多分この手のパーサは仕事以外でも使いたくなると思う(今までの自分の実績的に)。なのでプライベートな時間に試作版をでっち上げてみた。
/* * 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; }
簡単な構文で十分なので色々と手抜きしているし、仕様もあまり固めていない。まあ、これをベースに改めて考えれば何とかなるか……。