Skip to main content

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 from this blog

1의 개수 세기 - 해답

벌써 어제 말한 내일이 되었는데 답을 주신 분이 아무도 없어서 좀 뻘쭘하네요. :-P 그리고 어제 문제에 O(1)이라고 적었는데 엄밀히 얘기하자면 O(log 10 n)이라고 적었어야 했네요. 죄송합니다. ... 문제를 잠시 생각해보면 1~n까지의 수들 중 1의 개수를 얻기 위해서는 해당 숫자 n의 각 자리의 1의 개수가 모두 몇개나 될지를 구해서 더하면 된다는 사실을 알 수 있습니다. 예를 들어 13이라는 수를 생각해 보면 1~13까지의 수에서 1의 자리에는 1이 모두 몇개나 되는지와 10의 자리에는 모두 몇개나 되는지를 구해 이 값을 더하면 됩니다. 먼저 1의 자리를 생각해 보면 1, 11의 두 개가 있으며 10의 자리의 경우, 10, 11, 12, 13의 네 개가 있습니다. 따라서 2+4=6이라는 값을 구할 수 있습니다. 이번엔 234라는 수에서 10의 자리를 예로 들어 살펴 보겠습니다. 1~234라는 수들 중 10의 자리에 1이 들어가는 수는 10, 11, ..., 19, 110, 111, ... 119, 210, 211, ..., 219들로 모두 30개가 있음을 알 수 있습니다. 이 규칙들을 보면 해당 자리수의 1의 개수를 구하는 공식을 만들 수 있습니다. 234의 10의 자리에 해당하는 1의 개수는 ((234/100)+1)*10이 됩니다. 여기서 +1은 해당 자리수의 수가 0이 아닌 경우에만 더해집니다. 예를 들어 204라면 ((204/100)+0)*10으로 30개가 아닌 20개가 됩니다. 이런 방식으로 234의 각 자리수의 1의 개수를 구하면 1의 자리에 해당하는 1의 개수는 ((234/10)+1)*1=24개가 되고 100의 자리에 해당하는 개수는 ((234/1000)+1)*100=100이 됩니다. 이들 세 수를 모두 합하면 24+30+100=154개가 됩니다. 한가지 추가로 생각해야 할 점은 제일 큰 자리의 수가 1인 경우 위의 공식이 아닌 다른 공식이 필요하다는 점입니다. 예를 들어 123에서 100의 자리에 해당하는 1의 개수는 ((123/1

std::map에 insert하기

얼마전 회사 동료가 refactoring한 코드를 열심히 revert하고 있어서 물어보니 다음과 같은 문제였습니다. 원래 코드와 refactoring한 코드는 다음과 같더군요. nvp[name] = value; // original code nvp.insert(make_pair(name, value)); // refactored 아시겠지만 위의 두 라인은 전혀 다른 기능을 하죠. C++03에 보면 각각 다음과 같이 설명되어 있습니다. 23.1.2/7 Associative containers a_uniq.insert(t): pair<iterator, bool> inserts t if and only if there is no element in the container with key equivalent to the key of t. The bool component of the returned pair indicates whether the insertion takes place and the iterator component of the pair points to the element with key equivalent to the key of t. 23.3.1.2/1 map element access [lib.map.access] T& operator[](const key_type& x); Returns: (*((insert(make_pair(x, T()))).first)).second. 원래 코드는 매번 새 값으로 이전 값을 overwrite했지만 새 코드는 이전에 키가 존재하면 새값으로 overwrite하지 않습니다. 따라서 원래 기능이 제대로 동작하지 않게 된것이죠. 그래서 물어봤죠. "왜 이렇게 했어?" "insert가 성능이 더 좋다 그래서 했지." :-? 사실 Fowler 아저씨는 Refactoring 책에서 refactoring은 성능을 optimizing하기 위한 것이 아니다라

C++ of the Day #9 - Boost.Python 사용하기 #1

Python 은 가장 인기있는 interpret 언어중의 하나입니다. Python의 장점 중 하나는 C/C++ 모듈과 쉽게 연동할 수 있다는 점입니다. 물론 손으로 일일히 wrapper를 만드는 것은 손이 많이 가고 에러를 만들수 있는 작업이나 SWIG 등과 같은 도구를 사용하면 쉽게 python 모듈을 만들 수 있습니다. Boost.Python 은 이런 SWIG와 같이 python 모듈을 쉽게 만들 수 있도록 도와주는 라이브러리로 순수 C++만을 사용한다는 점이 SWIG와 다른 점입니다. 그리고 개인적으로는 Boost 라이브러리에 포함되어 있는 것들이 왠지 좀 더 믿음직스러워서... :-) 이번 글에서는 Boost.Python 문서에 나와 있는 예제 를 가지고 간단하게 python 모듈을 만드는 방법에 대해서 알아보겠습니다. Requirements 리눅스 이 글에서는 리눅스 환경에서의 사용 방법을 설명한다. Boost.Python 라이브러리 (1.33.1) Boost 라이브러리를 다운로드받아 아래와 유사한 명령으로 라이브러리를 빌드한다. bjam -sTOOLS=gcc -with-python install bjam의 --prefix 옵션으로 라이브러리가 설치될 위치를 변경할 수 있다. Python 라이브러리 (2.4.3) Python을 다운로드 받아 빌드하여 설치한다. 위의 경우와 유사하게 configure의 --prefix 옵션으로 설치될 위치를 변경할 수 있다. Write C++ Code 다음과 같이 코드를 작성한다. // greet.cpp #include <stdexcept> char const* greet(unsigned x) { static char const* const msgs[] = { "hello", "Boost.Python", "world!" }; if (x > 2) throw std::range_error("