Win32アプリのコマンドライン引数を解析するラッパー関数を書く

コードは id:eel3:20090207:1233932887 で既出だけど、一応分けて書いておく。

問題編

普通にC言語のアプリ(Visual C++で言うところのWin32コンソールアプリ)を書く場合、コマンドライン引数は個別にargvに格納される。

Win32アプリでアプリケーションエントリーポイントがWinMain()の場合、コマンドライン引数はlpCmdLineに格納されるのだけど、

  • 実行ファイル名が含まれていない。
  • オプション毎にパースされていない。

という、何というか不便なことになっている。

例えば

$ hoge.exe foo "bar bar"  baz

と実行した場合、lpCmdLineの中身は「foo "bar bar" baz」と、スペース等を含む1つの文字列になっている。

実行ファイル名についてはGetCommandLine()というAPIを使えば取得できる。

LPTSTR WINAPI GetCommandLine(void);

この関数は実行ファイル名を含むコマンドライン引数の文字列へのポインタを返す。上の例だと「hoge.exe foo "bar bar" baz」という文字列へのポインタを返す。

とはいえ、オプション引数等を使いたい場合は何とかして自前で解析する必要がある点に変わりは無い。

解決編

実はコマンドライン引数を解析するAPIがある。Shell32.dllのバージョン6.0以降に含まれるCommandLineToArgvWだ。

LPWSTR *CommandLineToArgvW(
    LPCWSTR lpCmdLine,
    int *pNumArgs
);

但しこのAPIはワイド文字(UNICODE)を使う場合にしか使えない。マルチバイト文字列用にCommandLineToArgvAが用意されてないかと思ったけど、無いらしい。

そこでマルチバイト文字列を使うプロジェクトでも使えるようにラッパーを書いてみた。基本的な考え方はこんな感じ。

  • ワイド文字を使うプロジェクトでは、CommandLineToArgvWをそのまま使う。
  • マルチバイト文字列を使うプロジェクトでは、次の手順。
    1. lpCmdLineはマルチバイト文字列なので、ワイド文字列に変換する。
    2. CommandLineToArgvWで解析する。
    3. 解析結果はワイド文字列なので、マルチバイト文字列に変換する。

インタフェースはこんな感じにしてみた。

/** コマンドライン引数を解析する
 * 
 * @param[in]  args_t  解析する文字列
 * @param[out] *argc   分解された引数文字列の数を返す
 * 
 * @retval !=NULL  解析結果(分解された引数を指すポインタ配列)
 * @retval NULL    何らかの原因で解析に失敗
 */
LPTSTR *parse_args(LPCTSTR args_t, int *argc);

/** parse_args()で取得したコマンドライン引数を削除する
 * 
 * @param[in] argc   コマンドライン引数の数
 * @param[in] *argv  コマンドライン引数
 */
void free_args(int argc, LPTSTR *argv);

以下のコードでは、一つのファイル内に収めるという想定でstatic関数にしている。atowとwtoaはparse_argsで使う下請け関数なので注意。

#include <tchar.h>
#include <windows.h>
#include <shellapi.h>


#ifndef UNICODE

#include <stdlib.h>
#include <winnls.h>


/** char文字列をWCHARに変換する */
static LPWSTR atow(LPCSTR src)
{
	LPWSTR buf;
	int dst_size, rc;

	rc = MultiByteToWideChar(CP_ACP, 0, src, -1, NULL, 0);
	if (rc == 0) {
		return NULL;
	}

	dst_size = rc + 1;
	buf = (LPWSTR) malloc(sizeof(WCHAR) * dst_size);
	if (buf == NULL) {
		return NULL;
	}

	rc = MultiByteToWideChar(CP_ACP, 0, src, -1, buf, dst_size);
	if (rc == 0) {
		free(buf);
		return NULL;
	}
	buf[rc] = L'\0';

	return buf;
}

/** WCHAR文字列をcharに変換する */
static LPSTR wtoa(LPCWSTR src)
{
	LPSTR buf;
	int dst_size, rc;

	rc = WideCharToMultiByte(CP_ACP, 0, src, -1, NULL, 0, NULL, NULL);
	if (rc == 0) {
		return NULL;
	}

	dst_size = rc + 1;
	buf = (LPSTR) malloc(dst_size);
	if (buf == NULL) {
		return NULL;
	}

	rc = WideCharToMultiByte(CP_ACP, 0, src, -1, buf, dst_size, NULL, NULL);
	if (rc == 0) {
		free(buf);
		return NULL;
	}
	buf[rc] = '\0';

	return buf;
}
#endif /* ndef UNICODE */

/** コマンドライン引数を解析する */
static LPTSTR *parse_args(LPCTSTR args_t, int *argc)
{
	LPCWSTR args_w;
	LPWSTR *argv_w;

	if (args_t[0] == _T('\0')) {
		*argc = 0;
		return NULL;
	}

#ifdef UNICODE
	args_w = args_t;
#else
	/* 引数文字列をWCHARに変換 */
	args_w = (LPCWSTR) atow(args_t);
	if (args_w == NULL) {
		return NULL;
	}
#endif

	/* パースする(WCHAR用のAPIしか用意されていない模様) */
	argv_w = CommandLineToArgvW(args_w, argc);

#ifdef UNICODE
	return argv_w;
#else
	free((void *) args_w);
	if (argv_w == NULL) {
		return NULL;
	}

	{
		LPSTR *argv_c = NULL;
		int i, j;

		/* パース結果はWCHARなので、char型に変換する必要がある */
		argv_c = (LPSTR *) malloc(sizeof(argv_c[0]) * (*argc + 1));
		if (argv_c == NULL) {
			goto DONE;
		}
		for (i = 0; i < *argc; ++i) {
			argv_c[i] = wtoa(argv_w[i]);
			if (argv_c[i] == NULL) {
				for (j = 0; j < i; ++j) {
					free(argv_c[j]);
				}
				free(argv_c);
				argv_c = NULL;
				goto DONE;
			}
		}
		argv_c[i] = NULL;

DONE:
		(void) LocalFree((HLOCAL) argv_w);
		return argv_c;
	}
#endif
}

/** parse_args()で取得したコマンドライン引数を削除する */
static void free_args(int argc, LPTSTR *argv)
{
#ifdef UNICODE
	(void) argc;
	(void) LocalFree((HLOCAL) argv);
#else
	int i;

	for (i = 0; i < argc; ++i) {
		free(argv[i]);
	}
	free(argv);
#endif
}

/** テスト用メインルーチン */
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                     LPTSTR lpCmdLine, int nCmdShow)
{
	int i, argc = 0;
	LPTSTR *argv;

	(void) hInstance, (void) hPrevInstance, (void) lpCmdLine, (void) nCmdShow;

#if 0
	MessageBox(NULL , lpCmdLine, _T("before") , MB_ICONINFORMATION);
	argv = parse_args(lpCmdLine, &argc);
#else
	MessageBox(NULL , GetCommandLine(), _T("before") , MB_ICONINFORMATION);
	argv = parse_args(GetCommandLine(), &argc);
#endif
	if (argv == NULL) {
		MessageBox(NULL , _T("parse_args() return NULL"), _T("before") , MB_ICONINFORMATION);
		return 1;
	}
	for (i = 0; i < argc; ++i)
		MessageBox(NULL , argv[i], _T("after") , MB_ICONINFORMATION);

	free_args(argc, argv);
	return 0;
}

結構、力技的なコードで、気になる点もある。例えばパースした結果をchar型に変換する所で、個別にメモリを割り当てている所なんかが気になる。