예제로 알아보는 argparse 사용법

몇 년 전에 argparse의 사용법을 간단하게 정리한 글을 발행했는데, 우연히 몇 가지 찾아보다 보니 기능설명의 나열만 읽어봐서는 애매한 부분도 많고 원하는 형태로 설정하는 것도 계속 헷갈려서 큰 맘 먹고 총정리 하는 마음으로 새로운 글을 하나 써보기로 마음 먹었다. 오늘은 명령줄 인자를 파싱하는 도구인 argparse를 어떻게 사용하는지, 그리고 몇몇 경우에 있어서 옵션의 동작을 어떻게 설정하는지를 좀 더 자세히 살펴보고자 한다.

명령줄 인자를 파싱하는 절차

명령줄 옵션을 처리하는 기본적인 절차는 파서 객체를 생성하여 초기화하고, 생성된 파서 객체에 각각의 인자에 관한 설정을 추가한다. 이렇게 설정을 추가한 파서에게 문자열을 넘겨주면 각 인자별 설정값들을 파싱하여 하나의 객체로 생성하여 되돌려준다.

여기서 파서 객체는 argparse.ArgumentParser 클래스의 인스턴스이고, 이 파서를 설정하기 위해서는 add_argument() 메소드를 사용한다. 설정을 마친 파서에 문자열의 리스트 형태로 인자들을 넘겨주면, 이를 파싱하여 객체의 형식으로 결과를 돌려주게 된다.

인자의 종류와 특성

프로그램이 명령줄을 통해서 받을 수 있는 인자는 크게 두 종류로 나뉠 수 있는데 하나는 프로그램이 제공하는 인자, 다른 하나는 사용자가 제공하는 인자이다. 전자의 경우 보통 하이픈이 앞에 붙어서 -o 라든지 -c 와 같은 형태를 취하거나 (이런 경우를 보통 ‘스위치’라고도 한다.) --data 처럼 하이픈이 두 개 붙은 단어의 형태로 된 경우가 있다. 이런 것들 ‘옵션 인자’라 한다. 다른 인자로는 이러한 약속 없이 주로 파일이나 URL 같이 매번 변하는 값을 입력하는 경우가 있다. 이를 ‘위치 인자’라 한다.

이는 옵션 인자들은 같은 옵션 인자들 내에서는 그 순서에 영향을 받지 않는 경우가 많지만, 위치 인자는 인자의 순서에 따라 의미나 역할이 달라지는 경우가 있기 때문에 붙이는 구분이다.


아주 간단한 예제

간단하게 단일 파일로 구성된 c 소스를 컴파일 하는 명령을 생각해보자. gcc 를 사용하는 경우 다음과 같이 실행할 수 있다. 이 때 gcc는 명령이고 이 이후에 오는 모든 내용은 인자이다.

$ gcc test.c
  ^~~ ^~~~~~
  |    \- 인자
   \ 명령

gcc는 test.c를 컴파일하여 test라는 결과물을 만들어 낼 수 있다. gcc는 결과물의 이름을 지정하는 -o 옵션이 있지만, 이는 선택적이며 주어지지 않는 경우, 입력된 소스 파일이 한 개라면 이름이 결과물을 만들기로 약속이 되어 있기 때문이다.

위치 인자는 2개를 써야 하는 명령도 있다. 예를 들면 파일을 복사하는 cp 나 윈도 명령줄의 copy 명령 같은 것이다. 이 명령은 원본과 대상에 대한 지정이 필요하므로 2개의 인자를 받아야 한다.

사실 이런식으로 정해진 인자만 있는 경우라면 딱히 파서가 필요하지 않다고 느낄 수도 있다. 하지만 argparse는 사실 훨씬 더 많은 기능들을 쉽게 구현할 수 있도록 도와준다.


인자의 구성

프로그램이 실행되면 실행된 프로세스는 어떤 식으로든 자신을 실행했을 때의 명령줄 인자를 전달받는다. 파이썬의 경우에는 sys.argv 라는 리스트로 전달 받을 수 있다. 명령줄 인자에는 프로그램 자신이 포함되기 때문에 sys.argv[0]은 보통 스크립트 자신의 이름이다.

명령줄에서 python myscript.py arg1 arg2 와 같은 식으로 실행할 수도 있을 것이다. 이 경우 python은 실행되는 스크립트가 주 명령인 것으로 가정해서 python을 제외한 명령줄을 스크립트로 넘겨준다.

cp 명령과 같이 두 개의 위치 인자만 가지고 있다면 굳이 argparse를 쓰지 않더라도 다음과 같이 파싱하면 되지 않느냐고 생각할 수 있다. 게다가 간단하다.

source, dest = sys.argv[1:]

이 코드는 인자가 2개만 있을 것이라 가정하기 때문에, 한 개만 주어지거나 세 개 이상이 되면 예외가 발생할 것이다. 이러한 예외 처리에 관한 코드를 따로 작성해야 할 텐데, argparse를 쓰면 잘못된 인자, 부족한 인자에 대한 처리를 자동으로 해준다. argparse는 인자 파싱에 실패할 경우, 설정된 인자를 통해 자동으로 사용법에 대한 안내를 출력하면서 비정상 코드로 종료한다.

위치 인자 지정

2개의 다른 위치 인자를 받는 프로그램을 작성하려는 경우, 다음과 같이 argparse를 사용할 수 있다. 파서 인스턴스를 생성한 다음, add_argument() 메소드를 호출하면서 각각의 인자를 추가해주면 된다. 이후 파서의 parse_args()에 인자들을 넣어주면 파싱된 결과를 하나의 객체로 담아서 돌려준다. 실제 사용시에는 인자를 주지 않는데, 인자 없이 parse_args()를 호출하면 자동으로 sys.argv[1:] 을 사용하게 된다.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('infile')
parser.add_argument('outfile')

cmds = 'prog file1 file2'
args = parser.parse_args(cmds.split()[1:]))
print(args.infile, args.outfile)
# 'file1' 'file2'

코드가 제법 번거로워 보인다. 대신에 이 경우 인자를 누락하거나 너무 많이 넣은 경우에 별다른 처리를 하지 않았지만 아래와 같이 멋지게 처리되는 것을 볼 수 있다. 즉 argparse는 단순히 인자를 파싱하는 것 외에 여러 명령줄 도구들과 비슷한 일관성 있는 사용자 인터페이스를 구축하는데 도움이 된다.

usage: a.py [-h] infile outfile
a.py: error: the following arguments are required: outfile

옵션 인자 설정하기

이번에는 옵션 인자를 추가해보자. gcc의 경우 위치 인자로는 소스 파일’들’이 적용된다. 즉 1개 이상의 위치 인자를 가지고 있다는 이야기이다. (같은 인자가 여러 개의 값을 갖는 경우도 아래에서 살펴볼 것이다.) 그리고 기본적으로는 두 개의 옵션 인자를 쓸 수 있다.

  • -c : 소스 파일을 컴파일만 하여 오브젝트 파일을 생성
  • -o, --output : 생성될 파일의 경로

일단 다음과 같이 옵션 인자를 구성할 수 있다. 옵션 인자는 이름이 하이픈으로 시작하는 것으로 정의되며, 같은 역할을 하는 인자의 이름을 여러 개 만들 수 있다.

parser.add_argument('-o', '--ouput', dest='outfile')

위치 인자인지 옵션 인자인지를 결정하는 것은 인자의 이름이 하이픈으로 시작하는지로 판단한다. 그리고 추가적인 옵션인 dest= 를 썼는데, 앞서 위치 인자의 예에서 보면 기본적으로 인자의 이름이 파싱 결과의 키가 되는 것을 볼 수 있다. 이 예에서는 '-o' 가 옵션의 첫 이름이므로 args.o 로 나오게 되기 때문에 dest=를 통해서 내부에서 사용할 별도의 이름을 주었다.

여러 개의 값을 갖는 인자

gcc와 같은 프로그램은 소스 파일 여러 개를 한 번에 컴파일 할 수 있고 이것은 ‘infile’ 위치 인자의 값이 1개 이상이 될 수 있음을 의미한다. 또한 보통 위치인자는 옵션 인자의 상대적인 개념으로 필수값으로 생각하는데, 경우에 따라서는 위치인자가 선택적인 경우도 있다. 이렇게 인자의 개수를 조절할 필요가 있을 경우 nargs= 옵션을 줄 수 있다. 사용가능한 값은 다음과 같다.

  • 1, 2, 3 … 등 정수값 : 고정된 개수의 값을 요구한다.
  • ? : 0 혹은 1개로 1개만 쓰거나 쓰지 않을 수 있다.
  • + : 1개 이상, 최소 1개는 반드시 있어야 한다.
  • * : 0개 이상, 쓰지 않아도 좋으며 여러 개 일 수 있다.

위 예에서의 위치 인자의 경우에는 + 이 올바른 선택이겠다. 참고로 nargs= 로 설정하면 파싱된 결과에서 해당 옵션의 값은 nargs=1 이더라도 모두 리스트로 만들어지니 유의하자.

도움말에 표시되는 이름 : metavar

프로그램이 실행됐을 때 요구하는 인자가 올바르게 들어오지 않았다면 argparse는 이에 관한 표준 형식의 안내 (사용법) 를 출력하면서 프로그램을 종료시킨다. 이때 표시되는 사용법 텍스트에는 필수적으로 입력해야 할 인자들이 표시된다. 이때 각 인자의 이름이 내부에서 사용하는 이름 그대로 표시될 필요는 없을 것이다.

예를 들어 앞서 작성한 프로그램의 안내문이 다음과 같은 식이면 어떨까

Usage: myapp FILE [FILE...] -o OUTPUT
#      ^ 프로그램 이름도 변경됐음에 유의

이렇게 안내문에서 해당 인자를 표현하려고 할 때, add_argument()metavar= 파라미터를 넘겨주면 된다.

parser = argparse.ArgumentParser(prog='myapp')
parser.add_argument('infiles', nargs='+', metavar='SOURCE_FILE')
parser.add_argument('outfile', metavar='OUTFILE')
cmds = 'myapp file1'.split()[1:]
print(parser.parse_args(cmds))

# usage: myapp [-h] SOURCE_FILE [SOURCE_FILE ...] OUTFILE
# myapp: error: the following arguments are required: OUTFILE

결과로 출력되는 안내문을 보면 SOURCE_FILE을 한 개 이상 입력하고 , 끝으로 OUTFILE을 입력해주어야 동작한다는 것을 알 수 있다.

참고로 안내문에서 표시되는 앱 이름은 ArgumentParser()를 생성할 때 prog= 파라미터로 문자열을 넘겨주면 된다.


스위치

앞서 예를 들어본 선택인자 -o의 경우, 인자 자체는 선택적이지만, 인자를 사용하는 경우 인자에 해당하는 값을 넣어야 했다. 하지만 ls 명령의 -l 옵션과 같이 인자를 주는 것 만으로 동작에 영향을 줄 수 있는 옵션들이 있다. 이러한 옵션들은 정해진 기능을 껐다 켰다하는 역할을 수행하므로 ‘스위치’라고 부르기도 한다. 프로그램 내에서는 해당 인자의 값 보다는 인자가 켜졌는지 여부를 사용할 것이므로 True/False 중 하나의 값을 지정해주면 되겠다.

이러한 동작을 수행하려면 add_argument()에서 action= 파라미터를 사용한다. 액션 값으로는 미리 정해져있는 액션의 이름(문자열)을 넘겨주거나, 따로 작성한 커스텀 액션 클래스를 넘겨줄 수 있다. 이 액션은 기본적으로 "store"로 정해지며 dest에 인자의 값을 저장한다는 개념이다. 스위치를 사용할 때에는 "store_true" 를 사용한다. 반대로 스위치가 주어졌을 때 False가 되게 하려면 "store_false"를 쓸 수 있다. -u, --uppercase 옵션이 있을 때 대문자로 출력한다고 가정하면 다음과 같이 코드를 작성할 수 있을 것이다.

cmds = 'app -u'.split()[1:]
ps = argparse.ArgumentParser(prog='app')
ps.add_argument('-u', '--uppercase', dest='print_uppercase',
                action='store_true')
ps.add_argument('-i', '--ignore-case', dest='case_sensitive',
                action='store_false')
args = ps.parse_args(cmds)
print(args)

# print_uppercase == True
# case_sensitive == True

-u 옵션만 주어졌을 경우, print_uppercase는 켜지게 된다. case_sensitive-i 옵션에 의해서 꺼지기 때문에 "store_false"를 적용했고 실행 시 주어지지 않아서 True가 되었다.

스위치를 사용하는 경우 True/False가 아닌 별도의 값을 갖도록 할 수 있다. 이는 "store_const" 액션을 사용하는데, 이 때는 반드시 const=로 사용될 값을 지정해 주어야 한다.

parser.add_argument('-h', '--head', dest='max_lines', action='store_const', const=10)

여러 번 쓰여지는 인자

앞서서 인자의 값이 여러 개인 경우 nargs='+' 옵션을 써서 정의할 수 있다고 했다. 그런데 이것은 인자 하나에 대한 값이 여러 개라는 의미이다. 동일한 인자를 여러 번 쓰면 어떨까? 예를 들어 grep 명령은 -e 옵션을 여러 번 사용할 수 있다. 일반적으로 옵션인자를 여러 번 사용하는 경우, 해당 옵션의 값은 계속해서 덮어써 지므로 마지막에 사용된 값만 남게 된다. 비슷하게 gcc의 경우에도 -H, -L 과 같은 옵션을 여러 번 사용해서 여러 값을 모두 누적해서 사용한다.

이 동작은 action="append" 를 사용해서 구현할 수 있다.

cmds = 'app -e 1 -e 2 -e 3'.split()[1:]
ps = argparse.ArgumentParser(prog='app')
ps.add_argument('-e', dest='num', type=int, action='append')
args = ps.parse_args(cmds)

# args.num = [1, 2, 3] 

"append""store_const"와 비슷하게 "append_const" 버전이 있어서 인자의 값을 받지 않고 const=로 주어진 값을 계속해서 담을 수도 있다. "count" 액션은 인자가 쓰여진 갯수만 세도록 할 수도 있다.

참고로 nargs=1로 설정한 인자인 경우, 원소가 하나인 리스트로 파싱되기 때문에, action="append"를 쓴다면 리스트의 리스트로 파싱되니 주의하자.

들어왔을 때에만 기본값을 가질 수 있는 옵션 인자

이제 어지간한 종류의 인자는 다 만들어본 것 같다. 이번에는 조금 복잡한 인자를 구현해보도록 하자. grep에는 독특한 인자가 있다. 바로 -A, --after, -B, --before, -C, --context 인자이다. 이들은 기본적으로 스위치처럼 동작하지만, 인자로 주어질 때 선택적으로 LINE 값을 받을 수 있다. -A의 경우 매칭된 라인 아래로 몇 줄을 더 표시하는 옵션인데, 주어지지 않으면 동작하지 않는다. (LINE = 0) 만약 -A 만 주어진다면 기본값인 2가 사용된다. 여기에 -A4와 같이 다른 값을 지정할 수도 있다.

일종의 기본값이기 때문에 default=를 사용하면 될거라 생각하는데, `default=`는 옵션에 대한 값이 주어지지 않을 때 뿐 아니라 옵션 자체가 없을 때에도 사용되는 값이기 때문에 이 동작을 구현할 수 없다. 따라서 커스텀 액션을 만들어야 한다.

커스텀 액션은 argparser.Action을 서브클래싱하여 만들게 되는데, __init__(*), __call__(*) 두 개의 메소드를 정의해야 한다. __init__()add_argument() 에서 action=으로 지정될 때 호출된다. (즉 항상 호출된다.) 바대로 __call__()은 지정한 인자가 실제로 들어왔을 때에만 호출된다. 인자 자체가 입력으로 들어오지 않았다면 호출되지 않는다.

따라서 default 값을 별도로 저장해두었다가, 인자가 들어왔을 때 값이 없으면 저장된 별도의 값을 사용하면 될 것이다.

__init__()은 옵션 문자열의 리스트와 dest를 포함하여 add_argument()에 쓰이는 옵션 중 action=을 제외한 모든 인자를 받는다. __call__()은 parser, namespace, value, option_string을 인자로 받는데, 그 내부에서 namespace에 대해서 속성값을 조작해주면 되겠다. 다음과 같이 커스텀 액션을 만든다.

class LineAction(argparse.Action):
  def __init__(self, option_strings, dest, *, default=2, **kwds):
    self.line_default = default
    kwds['nargs'] = '?'
    super().__init__(option_strings, dest,default=0, **kwds)

  def __call__(self, parser, namespace, value, option_string=None):
    if option_string in ('-A --after -B --befor -C --context'.split()):
      setattr(namespace, self.dest, value or self.line_default)

디폴트값은 2인데, 이를 action 객체의 line_default라는 속성으로 따로 기록한다. 그리고 default 속성으로는 스위치가 없을 때 설정될 값인 0이 들어가도록한다.

옵션 값이 있을 수도 없을 수도 있기 때문에 nargs= 가 반드시 '?'로 들어가야 한다. 이를 액션 내에서 강제하도록 했다. 그리고 __init__() 내에서는 부모 클래스의 __init__()을 호출하여, 그외 다른 동작들은 기본동작과 같이 수행되도록 해야 한다.

__call__() 에서는 value 값을 체크해서 없으면 따로 지정해둔 line_default 값으로 namespace 객체의 속성을 덮어써준다. 이로써 원하는 동작이 가능하게 되었다.

cmds = 'app -A -B4'.split()[1:]
ps = argparse.ArgumentParser(prog='app')
ps.add_argument('-A', '--after',
                dest='line_after',
                metavar='LINE',
                type=int,
                default=2,
                action=LineAction)

ps.add_argument('-B', '--before',
                dest='line_before',
                metavar='LINE',
                type=int,
                default=2,
                action=LineAction)

ps.add_argument('-C', '--context',
                dest='line_context',
                metavar='LINE',
                type=int,
                default=2,
                action=LineAction)

args = ps.parse_args(cmds)
# line_after   == 2
# line_before  == 4
# line_context == 0
  

이상으로 argparse를 사용하는 기본적인 방법부터 상황에 따른 옵션들을 어떻게 구현하는지를 살펴보았다. argparse는 전통적인 유닉스 명령줄 도구를 만드는 방식을 재현하고 있고, 사용자와 커뮤니케이션하는 컨벤션을 내장하고 있기 때문에 다소 번거로워 보이지만, 단순히 sys.argv를 쪼개어 사용하는 것보다도 훨씬 더 정교하고 편리하게 활용할 수 있다. 이 글에서 소개한 것보다 더 자세한 내용이 파이썬 argparse 자습서에 나와 있으니 참고해보면 도움이 될 것이다.