9장. 자이썬 스크립팅

이 장에서 우리는 자이썬으로 스크립팅하는 것에 대해 알아볼것이다. 우리의 목적을 위하여, ‘스크립팅scripting‘이라는 단어는 일상적인 작업을 돕기 위한 작은 프로그램을 작성함을 뜻하는 것으로 정해두도록 하자. 여기서 작업이라 함은, 디렉토리를 만들고 삭제하기, 파일과 프로그램 관리, 기타 작은 프로그램으로 표현할 수 있을 만한 반복적인 일을 말한다. 하지만 실제로는 스크립트가 커져버려서 스크립트와 풀 사이즈의 프로그램의 경계가 모호해지기도 한다.

자이썬에서 스크립트를 만들 만한 작업에는 어떤 것들이 있는지 감을 잡을 수 있도록 아주 작은 예제 몇 가지를 살펴보도록 하겠다. 그런 다음에는 그러한 기법 몇 가지를 함께 사용하는 중간 크기의 작업도 다루어 볼 것이다.

스크립트로 전달된 인자를 얻기

아주 간단한 스크립트라 하더라도 명령행으로부터 인자를 취하는 것이 많을 것이다. 스크립트로 전달되는 인자를 출력하는 스크립트를 작성해보도록 하겠다.

예제 9-1.

import sys

for arg in sys.argv:
    print arg

우리의 스크립트에 무작위로 인자를 주고 실행시켜보도록 하자.

예제 9-2.

$ jython args.py a b c 1 2 3
args.py
a
b
c
1
2
3

sys.argv의 첫번째 값은 스크립트 자체(args.py)이며, 나머지는 명령행에서 전달된 항목들이다.

파일 탐색

많은 스크립팅 작업은 ‘파일을 한 뭉텅이 찾아서 무언가를 하는’ 형태를 띤다. 그러니 자이썬 프로그램을 가지고 파일을 찾는 방법을 살펴보도록 하자. 전달된 문자열과 일치하는 파일을 현재 디렉토리에서 찾는 간단한 스크립트로 시작해보도록 하겠다.

예제 9-3.

import sys
import os

for f in os.listdir(sys.argv[1]):
    if f.find(sys.argv[2]) != -1:
        print f

스크립트의 첫머리에서는 sys 모듈과 os 모듈을 들여왔다. 다음으로 os.listdir(sys.argv[1])의 결과에 대하여 루핑을 실행했다. 명령행을 통하여 디렉토리에 있는 파일을 나열하고, 파일을 삭제하고, 파일명을 변경하는 등의 작업을 하고자 한다면 os 모듈이 좋은 장소이다. os 모듈의 listdir 함수는 경로를 나타내는 문자열로 된 한 개의 인자를 받는다. 해당 경로에있는 디렉토리의 항목은 목록으로 반환된다. 이 경우에, (현재 디렉토리를 나타내는 ‘.’을 전달함으로써) 자신의 디렉토리에서 수행하면 다음과 같다.

예제 9-4.

$ ls
args.py
search.py
$ jython list.py . py
args.py
search.py
$ jython list.py . se
search.py

list.py에 대하여 처음 호출하였을 때, ‘py’를 포함하는 모든 파일, 즉 ‘args.py’와 ‘search.py’를 나열하였다. 두번째 호출에서는, 문자열 ‘se’를 포함하는 모든 파일을 나열하여, ‘search.py’를 결과로 얻었다.

os 모듈에는 운영 환경에 대한 정보를 얻어내는데에 있어서 유용한 여러 가지 기능과 속성이 있다. 다음으로는 자이썬 프롬프트를 열어서 os 기능을 몇 가지 시험해보도록 하겠다.

예제 9-5.

>>> import os
>>> os.getcwd()
'/Users/frank/Desktop/frank/hg/jythonbook~jython-book/src/chapter8'
>>> os.chdir("/Users/frank")
>>> os.getcwd()
'/Users/frank'

위에서는 os.getcwd()를 사용하여 현재 디렉토리를 출력하고, 디렉토리를 ‘/Users/frank’로 이동하여 다음, os.getcwd()를 한 번 더 호출하여 새로운 위치를 출력하였다. 여기서 JVM은 자이썬 프로세스의 현재 작업 디렉토리를 실제로 변경할 권한을 갖고 있지 않다는 점에 주목할 필요가 있다. 이러한 이유로, 자이썬 자체적으로 현재 작업 디렉토리를 추적한다. 자이썬의 표준 라이브러리를 사용한다면, 현재 작업 디렉토리는 여러분의 예상대로 동작할 것이다(os.chdir()을 통하여 변경된다). 그렇지만, 현재 작업 디렉토리에 대하여 의존적인 자바 라이브러리 함수를 가져올 경우에는 언제나 자이썬이 시작된 디렉토리를 반영하게 된다.

예제 9-6.

>>> import os
>>> os.getcwd()
'/Users/frank/Desktop/frank/hg/jythonbook~jython-book/src/chapter8'
>>> from java.io import File
>>> f = File(".")
>>> for x in f.list():
... print x
...
args.py
search.py
>>> os.chdir("/Users/frank")
>>> os.getcwd()
'/Users/frank'
>>> os.listdir(".")
['Desktop', 'Documents', 'Downloads', 'Dropbox', 'Library', 'Movies', 'Music', 'Pictures', 'Public', 'Sites']
>>> g = File(".")
>>> for x in g.list():
...     print x
...
args.py
search.py

마지막 예제를 차례차례 살펴보자. os 모듈을 들여와서 현재 작업 디렉토리인 chapter8을 출력하였다. 우리는 java.io로부터 File 클래스를 들여왔다. 그 다음으로는 자바 쪽 세계로부터 ‘.’의 내용을 출력하였다. 그런 다음에는 os.chdir()로 홈 디렉토리로 가서, ‘.’의 내용을 자이썬의 입장에서 나열하고, ‘.’을 자바의 입장에서 나열하였다. 중요한 점은, 우리는 자바 프로세스의 실제 작업 디렉토리를 변경할 수 없으므로 자바 쪽에서의 ‘.’은 항상 chapter8 디렉토리가 될 것이라는 점이다. 우리는 작업 디렉토리를 추적함으로써 자이썬의 작업 디렉토리가 파이썬 프로그래머가 예상할 수 있는 방식으로 동작하도록 할 수 있을 따름이다. 너무 많은 파이썬 도구들(distutils이나 setuptools 등)이 작업 디렉토리 변경 능력에 의존하고 있다.

파일 조작하기

파일을 나열하는 것도 훌륭하지만, 스크립팅에 있어 더욱 재미있는 문제는 작업하는 파일에 대하여 뭔가를 실제로 하는 것이다. 필자는 때때로 한 묶음의 파일에 대하여 확장자를 변경할 필요가 있었다. 한두 개의 파일에 대해서는 수작업으로 확장자를 변경하는 것이 어렵지 않다. 하지만 수백 개의 확장자를 변경하고자한다면, 이는 매우 지루한 일이 되어버린다. os.path 모듈의 splitext 함수를 사용하여 확장자를 분리해낼 수 있다. splitext 함수는 파일명을 받아서 파일의 기본 이름과 확장자로 이루어진 튜플을 반환한다.

예제 9-7.

>>> import os
>>> for f in os.listdir("."):
... print os.path.splitext(f)
...
('args', '.py')
('builder', '.py')

('HelloWorld', '.java')
('search', '.py')

이제 확장자를 얻었으므로, 파일의 이름을 변경하는 방법만 알면 된다.os 모듈에 우리가 필요로 하는, 바로 그 rename 함수가 있다.

예제 9-8.

>>> import os
>>> os.rename('HelloWorld.java', 'HelloWorld.foo')
>>> os.listdir('.')
['args.py', 'builder.py', 'HelloWorld.foo', 'search.py']

If you are manipulating any important files, be sure to put the names back!

>>> os.rename('HelloWorld.foo', 'HelloWorld.java')
>>> os.listdir('.')
['args.py', 'builder.py', 'HelloWorld.java', 'search.py']

이제 확장자를 얻는 방법과 파일명을 변경하는 방법을 각각 알았으므로, 확장자를 변경하는 스크립트(chext.py)를 만들 수 있게 되었다.

예제 9-9.

import sys
import os

for f in os.listdir(sys.argv[1]):
    base, ext = os.path.splitext(f)
    if ext[1:] == sys.argv[2]:
        os.rename(f, "%s.%s" % (base, sys.argv[3]))

스크립트 모듈 만들기

chext.py를 다른 모듈에서도 사용할 수 있도록 만들고자한다면, 다음과 같이 코드를 함수에 넣어서 그 사용을 분리할 수 있다.

예제 9-10.

import sys
import os

def change_ext(directory, old_ext, new_ext):
    for f in os.listdir(sys.argv[1]):
        base, ext = os.path.splitext(f)
        if ext[1:] == sys.argv[2]:
            os.rename(f, "%s.%s" % (base, sys.argv[3]))

if __name__ == '__main__':
    if len(sys.argv) < 4:
        print "usage: %s directory old_ext new_ext" % sys.argv[0]
        sys.exit(1)
    change_ext(sys.argv[1], sys.argv[2], sys.argv[3])

이러한 새 버전은 다음과 같은 방법으로 외부 모듈에서 사용할 수 있다.

예제 9-11.

import chext

chext.change_ext(".", "foo", "java")

간단한 오류 점검도 도입하였는데, 인자를 충분히 넣어주지 않을 경우에는 스크립트의 사용법을 알리는 메시지를 출력하도록 하였다.

명령행 선택사항 분석

대다수의 스크립트는 한번 작성하여 사용한 뒤에는 잊혀진다. 하지만 어떤 것은 오랜 시간에 걸쳐서, 매주 혹은 매일 사용하게 되기도 한다. 어떤 스크립트를 반복적으로 사용하고 있다면, 명령행에서 선택사항을 입력할 수 있도록 하면 더욱 유용할 것이라는 것을 종종 깨닫게 될 것이다. 자이썬에는 그러한 일을 할 수 있도록 명령행의 인자를 분석parse하는 세 가지 방법이 있다. 첫번째는 위의 chext.py에서도 사용한 sys.argv이고, 두번째는 getopt 모듈이며, 세번째는 최신의, 보다 유연한 optparse 모듈이다.

인자를 스크립트에 단순히 넣기만 하는 것이 아니라, 그것을 분석하는 것을 일일이 구현하는 것은 꽤 지루한 작업이 될 것이므로, 그러한 경우에는 getopt 또는 optparse를 사용하는 것이 훨씬 나을 것이다. 여기서는 보다 새롭고 유연한 optparse 모듈을 다루도록 하겠다.인자가 간단한 경우에는 좀 더 적은 코드를 필요로 하는 getopt 모듈 또한 여전히​​ 유용하다. 다음은 optparse를 사용하는 기본적인 스크립트이다.

예제 9-12.

# script foo3.py
from optparse import optionparser
parser = optionparser()
parser.add_option("-f", "--foo", help="set foo option")
parser.add_option("-b", "--bar", help="set bar option")
(options, args) = parser.parse_args()
print "options: %s" % options
print "args: %s" % args

위의 코드를 실행하면 다음과 같은 결과를 얻는다.

예제 9-13.

$ jython foo3.py -b a --foo b c d
$ options: {'foo': 'b', 'bar': 'a'}
$ args: ['c', 'd']

위의 예제에서는, optionparser를 생성하고 add_option 메소드를 사용하여 두 개의 선택사항을 추가하였다. add_option 메소드는 최소 한 개 이상의 문자열을 선택 인자(첫번째 경우의 ‘-f’) 및 긴 버전(직전의 ‘–foo’)으로 취한다. 그런 다음 스크립트에 관련된 도움말 문자열을 설정하는 ‘help’ 옵션과 같은 선택적인 키워드 옵션을 전달할 수 있다. 이 장의 뒷부분에서 optparse 모듈에 대한 보다 구체적인 예제를 다루도록 하겠다.

자바 소스 컴파일

자바 소스를 컴파일하는 것은 엄밀히 말하자면 스크립팅 작업이라고 할 수는 없지만, 다음 섹션에서 보여주고자 하는 큰 예제를 위해서 필요한 부분이다. 여기서 다루고자 하는 API는 JDK 6에서 도입되었으며, 구현을 할 지는 JVM 제작자의 선택사항이다. (가장 널리 사용되는) 오라클의 JDK 6 및 맥 OS X에 포함된 JDK 6에서 동작함을 확인하였다. JavaCompiler API에 대해서는 다음의 문서를 참고하기 바란다. http://docs.oracle.com/javase/6/docs/api/javax/tools/JavaCompiler.html

다음은 그 API를 자이썬에서 사용하는 간단한 예제이다.

예제 9-14.

from javax.tools import (ForwardingJavaFileManager, ToolProvider, DiagnosticCollector,)
names = ["HelloWorld.java"]
compiler = ToolProvider.getSystemJavaCompiler()
diagnostics = DiagnosticCollector()
manager = compiler.getStandardFileManager(diagnostics, none, none)
units = manager.getJavaFileObjectsFromStrings(names)
comp_task = compiler.getTask(none, manager, diagnostics, none, none, units)
success = comp_task.call()
manager.close()

javax.tools 패키지로부터 몇몇 자바 클래스를 임포트한다. 그런 다음 ‘HelloWorld.java’라는 문자열을 갖는 목록을 생성한다. 그 다음에는 자바 컴파일러에서 핸들을 얻어서 ‘compiler’라는 이름을 붙인다. 컴파일러가 필요로 하는 두 개의 개체, ‘diagnostics’와 ‘manager’를 생성하였다. 문자열을 갖는 목록을 ‘units’로 탈바꿈시켜서 마침내 컴파일러 작업을 만들고 그것의 call 메소드를 실행한다. 이러한 일을 자주 할 생각이라면, 간단한 메소드를 만들 수도 있을 것이다.

예제 스크립트: Builder.py

지금까지 자이썬에서의 스크립트 작성을 용이하게 하는 몇 가지 모듈에 대하여 논의하였다. 그것들을 어떻게 활용할 수 있는지를 간단한 스크립트를 통하여 확인해보도록 하겠다. 디렉토리 내의 자바 파일을 .class 파일로 컴파일하고, .class 파일을 정리하는 일을 나누어서 처리하는 스크립트를 작성해보려고 한다. 디렉토리 구조를 생성하고, 정리를 위해 디렉토리 구조를 삭제하며, 자바 원천source 파일을 컴파일하는 기능이 필요할 것이다.

예제 9-15.

import os
import sys
import glob

from javax.tools import (forwardingjavafilemanager, toolprovider,
         diagnosticcollector,)

tasks = {}

def task(func):
    tasks[func.func_name] = func

@task
def clean():
    files = glob.glob("\*.class")
    for file in files:
        os.unlink(file)

@task
def compile():
    files = glob.glob("\*.java")
    _log("compiling %s" % files)
    if not _compile(files):
        quit()
    _log("compiled")

def _log(message):
    if options.verbose:
        print message

def _compile(names):
    compiler = toolprovider.getsystemjavacompiler()
    diagnostics = diagnosticcollector()
    manager = compiler.getstandardfilemanager(diagnostics, none, none)
    units = manager.getjavafileobjectsfromstrings(names)
    comp_task = compiler.gettask(none, manager, diagnostics, none, none, units)
    success = comp_task.call()
    manager.close()
    return success

if __name__ == '__main__':
    from optparse import optionparser
    parser = optionparser()
    parser.add_option("-q", "--quiet",
        action="store_false", dest="verbose", default=true,
        help="don't print out task messages.")
    parser.add_option("-p", "--projecthelp",
        action="store_true", dest="projecthelp",
        help="print out list of tasks.")
    (options, args) = parser.parse_args()

    if options.projecthelp:
        for task in tasks:
            print task
        sys.exit(0)

    if len(args) < 1:
        print "usage: jython builder.py [options] task"
        sys.exit(1)

    try:
        current = tasks[args[0]]
    except KeyError:
        print "task %s not defined." % args[0]
        sys.exit(1)
    current()

위의 스크립트는 함수의 이름을 수집하여 디렉토리에 넣는 ‘task’ 장식자를 정의한다. –projecthelp와 –quiet 두 개의 옵션을 정의하는 optionparser 클래스가 있다. 기본적으로 스크립트는 그 행동을 표준 출력에 기록한다. –quiet를 선택하면 기록이 중지되며 –projecthelp는 가능한 작업을 나열한다. ‘compile’과 ‘clean’ 두 개의 작업을 정의하였다. ‘compile’ 작업은 디렉토리 내의 모든 .java 파일을 glob하여 그것들을 컴파일한다. ‘clean’ 작업은 디렉토리 내의 모든 .class 파일을 glob하여 삭제한다. .class 파일을 삭제할 때 물어보지 않으므로 조심해야 한다!

시험해보도록 하자. builder.py와 같은 디렉토리에 생성할 자바 클래스는, 고전적인 ‘Hello World’ 프로그램이다.

HelloWorld.java

예제 9-16.

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World");
    }
}

다음과 같이 builder.py에 명령을 내려서 결과를 볼 수 있을 것이다.

예제 9-17.

[frank@pacman chapter8]$ jython builder.py --help
Usage: builder.py [options]

Options:
    -h, --help show this help message and exit
    -q, --quiet Don't print out task messages.
    -p, --projecthelp Print out list of tasks.
[frank@pacman chapter8]$ jython builder.py --projecthelp
compile
clean
[frank@pacman chapter8]$ jython builder.py compile
compiling ['HelloWorld.java']
compiled
[frank@pacman chapter8]$ ls
HelloWorld.java HelloWorld.class builder.py
[frank@pacman chapter8]$ jython builder.py clean
[frank@pacman chapter8]$ ls
HelloWorld.java builder.py
[frank@pacman chapter8]$ jython builder.py --quiet compile
[frank@pacman chapter8]$ ls
HelloWorld.class HelloWorld.java builder.py
[frank@pacman chapter8]$

요약

이번 장에서는 자이썬으로 스크립트를 만드는 법을 알아보았다. 한두 줄로 이루어진 가장 단순한 스크립트에서부터 여러 개의 선택적인 입력을 받는 큰 스크립트까지 다루어보았다. 여러분이 일상적으로 행하는 반복적인 작업을 자동화하는 데에 도움이 되기를 바란다.