C++ of the Day #40 - Python의 OptionParser 클래스 따라잡기

텍스트 기반 프로그램을 만들때 필요한 기능중의 하나가 command line option들을 parsing 해주는 것입니다. 원래는 boost::program_options 라이브러리를 소개하는 글을 쓰려고 했습니다. 그런데 마침 Python을 사용하면서 알게 된 OptionParser 클래스가 좀 더 사용하기가 쉬운 것 같아 이 클래스를 C++ 버전으로 만들어 보았습니다.

boost::program_options 라이브러리와는 달리 옵션들의 grouping이나 설정 파일을 사용하는 기능은 구현되어 있지 않습니다.

그리고 옵션들의 타입은 모두 string이나 vector<string>을 사용합니다. boost::program_options과 같이 as<int> 하는 식의 함수를 제공할 수도 있었습니다만 이렇게 하려면 exception들을 정의해야 하기 때문에 생략하였습니다. 필요하다면 다음과 같이 boost::lexical_cast를 사용하면 됩니다.


int blocksize = boost::lexical_cast(opts.get(BlockSizeOpt));


그리고 OptionParser 클래스는 getopt_long을 사용하여 구현되었기 때문에 parsing하는 방법은 GNU coding standards를 따릅니다.

Command line option에는 short form과 long form이 있습니다. 예를 들어 -I는 short form, --include-path는 long form입니다. 또한 어떤 옵션은 여러개의 값을 가질 수도 있습니다. gcc의 -I 옵션은 여러개의 값을 가질 수 있는 옵션의 예입니다.

그럼 이제 OptionParser의 사용법을 하나씩 알아보겠습니다. 먼저 OptionParser를 다음과 같이 생성합니다.


OptionParser op("Usage: %prog [options] arg1 arg2 ...", // usage
"1.0", // version
"Report bugs to ." // message
);


첫번째 인자는 usage 문자열이고 다음으로 version, message 문자열을 넣어 줍니다. usage나 version 문자열에 %prog를 넣으면 이 부분은 argv[0]로 변경되어 출력됩니다. 마지막으로 message는 help 화면의 마지막에 출력될 메시지를 넣습니다. (조금 뒤에 나오는 예제 화면을 보시면 이해가 쉽습니다.)

다음으로 위에서 생성한 OptionParser를 사용하여 parse_args 함수를 호출합니다. 결과는 OptMap과 OptArgs에 각각 저장됩니다.


OptMap optmap;
OptArgs args;
boost::tie(optmap, args) = op.parse_args(argc, argv);


여기까지 작성하고 빌드하여 실행시킨 결과는 다음과 같습니다.


$ ./a.out --help
Usage: ./a.out [options] arg1 arg2 ...

Options:
-V, --version show program's version number and exit
-h, --help show this help message and exit

Report bugs to .

$ ./a.out --version
1.0


위의 결과에서 알 수 있듯이 --help와 --version 옵션은 OptionParser 클래스에 의해 자동적으로 제공됩니다.

그럼 이제 필요한 인자를 하나씩 만들어 보겠습니다. 다음 라인은 -a, --all 옵션을 추가하는 코드입니다. 마지막 문자열은 --help 화면에 출력될 help string입니다.


op.add_option('a', "all", "do not hide entries starting with .");


이 옵션이 입력되었는지 확인하는 방법은 parse_args에서 리턴된 OptMap의 test함수를 사용하는 것입니다.


if (optmap.test('a')) allflags = true;


test함수에서 사용되는 키는 add_option 호출시 short form에 사용된 값입니다. Long form만 제공하는 옵션을 만들기 위해서는 다음과 같이 별도의 값을 지정해야 하는데 이때 OptionParser의 UserOpt 이상의 값중에서 사용해야 합니다. 따라서 이런 옵션이 다수 있는 경우 다음과 같이 enum 을 만들어 사용하는 것이 좋습니다.


enum Opt
{
AuthorOpt = OptionParser::UserOpt,
...
};

add_option(AuthorOpt, "author", "print the author of each file")


이 값이 입력되었는지 확인하기 위해서는 위의 경우와 마찬가지로 이 값을 사용하여 test 함수를 호출하면 됩니다.

다음으로 --block-size=30 이라든지 --color=always와 같이 인자가 필요한 옵션을 추가하는 방법에 대해 알아보겠습니다.


.add_option(BlockSizeOpt, "block-size", "use SIZE-byte blocks", "SIZE")
.add_option(ColorOpt, "color",
"control whether color is used to distinguish file\n"
" types. WHEN may be `never', `always', or `auto'\n"
"[default: %default]",
"WHEN",
"auto")


인자가 필요한 경우 help string 뒤에 metavar 인자를 넘깁니다. 이 인자가 null string이 아닌 경우를 인자가 필요한 옵션으로 간주하여 처리합니다. 추가로 이 값은 --help 화면에 --block-size=SIZE와 같이 출력되는 역할을 합니다.

두번째 --color 옵션의 경우에는 metavar외에 추가로 인자를 하나 더 가지고 있는데 이 인자는 default value를 나타냅니다. Default value가 있는 옵션은 인자가 필요하지만 입력되지 않은 경우에도 에러로 처리되지 않고 default value가 입력된 것처럼 처리됩니다. 이 경우 help string의 %default 부분은 "auto"값으로 변경되어 출력됩니다.

마지막으로 gcc의 -I 옵션처럼 여러개의 값을 가질 수 있는 인자를 만들려면 add_option 함수 대신 add_multi_option 함수를 사용합니다. 사용 방법은 default value를 지정할 수 없다는 점과 metavar가 항상 null string이 아니어야 한다는 점을 제외하곤 동일합니다.

Parsing 결과에서 옵션의 인자를 얻는 방법은 다음과 같습니다. 리턴 값은 string입니다.


if (optmap.test(BlockSizeOpt)) {
blocksize = boost::lexical_cast(optmap.get(BlockSizeOpt));
}


여러개의 인자를 가지는 옵션의 결과는 vector<string>로 리턴됩니다.


template
std::ostream& operator<<(std::ostream& os, std::vector const& cont)
{
std::copy(cont.begin(), cont.end(), std::ostream_iterator(os, " "));
return os;
}

...

if (optmap.test('I')) {
cout << "-I=" << optmap.get_multi('I') << endl;
}


마지막으로 parse_args함수에서 리턴되는 tuple중 두번째 값인 OptArgs에는 옵션 이외의 인자들이 vector<string>의 형태로 리턴됩니다. 예를 들어 "gcc -o a.out 1.cpp 2.cpp" 를 parse_arg하면 OptArgs에는 1.cpp, 2.cpp 두개의 값이 들어 있게 됩니다.

다음 화면은 첨부된 test.cpp를 빌드하여 만든 실행 파일의 --help입니다.


Usage: ./a.out [options] arg1 arg2 ...

Options:
-V, --version show program's version number and exit
-h, --help show this help message and exit
-a, --all do not hide entries starting with .
--author print the author of each file
--block-size=SIZE use SIZE-byte blocks
-c with -lt: sort by, and show, ctime (time of last
modification of file status information)
with -l: show ctime and sort by name
otherwise: sort by ctime
-H, --dereference-command-line
follow symbolic links on the command line
--color=[WHEN] control whether color is used to distinguish file
types. WHEN may be `never', `always', or `auto'
[default: auto]
-I, --include-path=PATH
list of search path

Report bugs to .


.

구현된 내용에 대해 설명을 하려고 했는데 막상 만들고 나니 별로 설명할 내용이 없네요. :roll: 구현을 위해 boost::variant와 static_visitor의 사용하고 있습니다. 참고하실 분들은 한번 보세요.

소스 코드는 여기서 다운로드하시면 됩니다. :-)

Comments

Popular Posts