열두번째 날: 펄에서 외부명령어 실행 시키기

저자

John_Kang - SE, Seoul.pm의 철권 2번 타자

시작하며

펄에서 외부 명령어를 실행시키는 방법은 여러 가지가 있습니다. system 내장 함수, ...처럼 역따옴표, open 내장 함수로 파이프(pipe)를 생성하는 방법 등이 있습니다. 이러한 방법의 동작과 차이점을 살펴보고 Capture::Tiny 모듈을 이용해 외부 명령어를 손쉽게 실행시키는 방법에 대해 알아봅니다.

외부 명령어를 펄을 통해 실행시킬 때는 표준 출력/오류의 방향과 외부 명령어가 반환하는 종료값을 인지해야 할 경우가 많습니다. 간단하게 기본적으로 제공하는 방법을 이용하지 않고 복잡하게 모듈까지 설치해서 외부 명령어를 실행하는지에 대한 의문은 각각의 차이점을 먼저 비교해보고 설명하겠습니다.

준비물

필요한 모듈은 다음과 같습니다.

직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.

$ sudo cpan Capture::Tiny

사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.

$ cpan Capture::Tiny

system 내장 함수

system 함수는 펄의 내장 함수로써 외부 명령어를 실행시킬 수 있습니다. 사용방법은 다음과 같습니다.

#
# system LIST
# system PROGRAM LIST
#
my $exit_status = system('ls');
#
# return : $exit_status, $?
# stdout : STDOUT
# stderr : STDERR
#

system 함수의 반환값은 외부 명령어가 반환한 종료값입니다. 따라서 $exit_statusls 명령어의 종료값을 담습니다. 대부분의 운영제체에서 외부 명령어의 정상적인 종료 코드는 0이기 때문에 system 함수에서 실행한 외부 명령이 성공적으로 종료되면 system의 반환값은 0이 됩니다.

system 함수는 fork를 이용해 자식 프로세스를 생성하고 자식 프로세스에서 외부 명령어를 실행합니다.

init --> perl --> ls

이 때 사용자가 임의로 표준 출력과 표준 오류의 파일 핸들을 수정하지 않았다면, ls 명령어의 표준 출력은 펄의 표준 출력(1)으로, 표준 오류는 펄의 표준 오류(2)를 상속 받습니다. fileno 내장 함수를 이용하면 해당 파일 핸들의 파일 디스크립터 값을 알 수 있습니다.

print fileno STDOUT ## 1

역따옴표(`)

역따옴표으로 외부 명령어를 실행하는 방법은 쉘에서의 그것과 동일합니다! 펄에는 크게 스칼라 문맥(scalar context)과 목록 문맥(list context) 두 가지가 존재하며 영어의 단수와 복수 개념과 비슷합니다. 역따옴표의 결과는 펄의 두 문맥에 따라 달라집니다.

다음은 스칼라 문맥에서의 역따옴표를 사용한 경우입니다.

my $output_string = `ls`;
- return : $?
- stdout : $out_string
- stderr : STDERR

외부 명령어의 종료값은 $? 변수에 저장되며 역따옴표는 외부 명령어가 표준 출력에 출력한 모든 내용을 반환하기 때문에 앞의 예제의 경우 디렉토리와 파일의 목록이 $output_string 변수에 저장됩니다. 표준 오류는 역따옴표를 실행하기 전의 펄의 표준 오류와 동일하기 때문에 오류를 확인하고 싶다면 외부 명령어를 실행할 때 표준 오류의 방향을 변경(재지향, redirect)해야 합니다.

my $output_string = `ls 2 > &1`;

다음은 목록 문맥에서의 역따옴표를 사용한 경우입니다.

my @output_string = `ls`;
- return : $?
- stdout : @out_string line terminated by $/;
- stderr : STDERR

아니 똑같아 보이는데 왜 목록 문맥이냐고요? = 연산자 왼쪽의 값이 $output_string이 아닌 @output_string이기 때문에 펄은 이 구문을 목록 문맥으로 처리합니다. 목록 문맥에서 역따옴표는 외부 명령어의 실행 결과를 $/ 변수의 값(기본 값은 해당 운영체제의 줄바꿈 문자)을 구분자로 나누어 각각의 줄 단위로 실행 결과를 반환합니다. 따라서 @output_string은 한 줄 단위로 구분된 문자열 목록을 가집니다.

qx()는 역따옴표의 또 다른 표현으로 둘은 완전히 동일합니다.

my $output_string = qx(ls);

실행할 구문에 쉘이 해석해야 할 메타 문자가 있다면 다음처럼 작은 따옴표를 괄호 대신 사용하세요.

my $output_string = qx'ps $$';

open 함수로 파이프 열기

전통적으로 파이프를 이용하면 외부 명령어와 데이터를 주고받을 수 있습니다. open 내장 함수로 파이프를 열 수 있는데 이때 두 번째 인자에 따라 파이프 생성 및 모드를 지정할 수 있습니다. 두번째 인자의 앞 부분에 | 기호를 사용하면 쓰기 모드로 파이프를 생성하며 뒷 부분에 | 기호가 오면 읽기 모드로 파이프를 생성합니다.

다음은 현재 디렉터리에서 30일이 지난 로그 파일을 삭제하는 간단한 프로그램입니다.

#!/usr/bin/env perl

use strict;
use warnings;

open( my $pipe, 'ls |' ) or die "Can't open a pipe : $!\n";
while (<$pipe>) {
    chomp;                    ## 파일명뒤의 개행(\n) 삭제

    next unless -f;           ## 파일만 추출
    next unless /\.log$/i;    ## *.log 파일만 추출

    unlink if -M > 30;        ## -M 으로 파일변경시간 확인(in day)후 삭제
}

open 함수를 실행하는 중에 오류가 발생할 경우 $! 변수를 확인해 어떤 오류가 발생했는지 확인할 수 있습니다.

다음은 /bin/mail 명령을 이용해 메일을 보내는 예제입니다.

#!/usr/bin/env perl

use strict;
use warnings;
use autodie;

open( my $body, '| /bin/mail -s Subject mail@domail.com' );
print {$body} 'Hi There, I hope you are doing well :)';

예제는 $body 파이프를 통해 입력 결과를 /bin/mail에게 전달합니다. autodie 프라그마를 통해 open 함수의 오류 제어를 자동화하면 편리합니다.

Capture::Tiny

사실 위의 방법만으로도 외부 명령어를 실행 시키기에 충분하며 별 무리는 없습니다. 하지만 어떤 외부 명령어는 실행 중 표준 출력과 표준 오류가 동시에 발생하며 이것을 각각 별도로 처리해야 할 경우도 있죠. 지금까지 설명한 방법으로 표준 출력과 표준 오류 및 반환값까지 각각 처리하려면 꽤 세부적인 이해와 많은 양의 코드가 필요(불가능하다는 뜻이 아님)합니다.

하지만 이런 류의 일은 꽤나 전형적이죠. 이미 이런 상황을 대비한 모듈이 CPAN에는 있지 않을까요? :-)

지금부터 살펴 볼 모듈은 CPAN의 Capture::Tiny 모듈입니다. 2013년 펄 크리스마스 달력의 셋째 날 기사를 참고해보면 Capture::Tiny43위에 랭크 되어있습니다. 수없이 많은 모듈 중 100위권 안의 모듈은 어느 정도 안심하고 사용할 수 있겠죠?

Capture::Tiny 모듈은 다음과 같은 장점을 가집니다.

다음은 Capture::Tiny 모듈을 사용한 간단한 예제입니다.

#!/usr/bin/env perl

use strict;
use warnings;
use autodie;
use Capture::Tiny ':all';

my @cmd = qw( find /proc -type f );
my ( $stdout, $stderr, $exit ) = capture {
    system @cmd;
};

capture함수가 system 함수의 표준 출력, 표준 오류 그리고 외부 명령어의 종료값을 반환합니다.

심지어 capture 함수에 stdoutstderr 인자를 넘겨주면 표준 출력과 표준 오류를 구분해서 사용할 수 있습니다.

my $out_fh = IO::File->new("out.log", "w+");
my $err_fh = IO::File->new("err.log", "w+");

my @cmd = qw( find /proc -type f );
capture { system @cmd } stdout => $out_fh, stderr => $err_fh;

어떤 명령어는 실행 도중에 출력 결과를 변수에도 저장하고 기존의 표준 출력과 표준 오류로 계속 출력시켜 실시간으로 확인하고 싶을 수도 있습니다.

my @cmd = qw( find /proc -type f );
my ( $stdout, $stderr, @result ) = tee {
    system @cmd;
};

tee 함수는 각각의 표준 출력과 오류를 $stdout$stderr에 저장함과 동시에 기존의 출력 방향으로도 그대로 출력해줍니다.

그 밖의 주의 사항

보안

system 함수에 인자를 전달할 때는 명령어와 각각의 인자를 배열에 담아 목록 형태로 제공해야 추가적인 쉘의 실행을 막을 수 있기 때문에 보안상 유리합니다. 다음은 흔히 볼 수 있는 system 함수를 사용하는 평범한 예제입니다.

#!/usr/bin/env perl

use strict;
use warnings;

my $cmd = 'find / -name' . q{ };
print "Please input what file do you want to find : ";
chomp( my $input = <> );

my $exit_status = system( $cmd . $input );

사용자 입력에 ;rm -rf 문자열이 있다면 어떻게 될까요? find / -name ;rm -rf 명령이 어떤 상황을 만들어 낼까요? 목록 형태로 system에게 인자를 전달하면 각각의 인자를 단순 문자열로 간주합니다. 이때는 각각의 인자 중에 ;rm -rf가 있다면 ;rm -rf 그 자체를 문자열로만 인식합니다.

Windows 환경에서의 반환값

리눅스, 유닉스 계열에 익숙한 관리자라면 윈도우에서 외부 명령어를 실행한 후 그 반환값을 확인해보고는 당황할 수 도 있습니다. 유닉스에서는 1 바이트인데 윈도우에서는 반환값의 크기가 2Byte이기 때문입니다.

hhhhhhhhllllllll의 하위 8비트는 프로그램을 종료한 시그널이며 상위 8비트가 실제 프로그램의 종료 값입니다. 실제 종료 값을 얻기 위해서는 8비트 만큼 우측으로 쉬프팅하면 그 값을 얻을 수 있습니다.

my @cmd = 'dir asdf'    # asdf is not existing
my $ret = system @cmd;  # $ret = 512
$ret >>= 8;             # $ret = $ret >> 8;
print "$! : [$ret]";    # No such file or directory : [2]

SunOS, Solaris, HP-UX과 system()함수

외부 명령어를 실행시킬 때는 내부적으로 외부 명령어를 위한 자식프로세스를 생성(fork)합니다. fork로 생성된 자식 프로세스는 부모의 파일 디스크립터는 물론 아직 소모되지 출력 버퍼도 물려받습니다. 안전하게 system 함수를 사용하기 위해서는 출력 버퍼를 비워(flush)주고 사용해야 합니다. 리눅스와 윈도우 계열의 system 함수는 이를 알아서 처리하기 때문에 신경쓰지 않아도 되지만 SunOS, Solaris, HP-UX 등의 운영체제에서는 이를 고려해야 합니다.

다행히 처리하는 방법이 어렵지는 않습니다. :)

local $| = 1;    ## autoflushing
.
.
coding...
.
my $ret = system ('command');
.

정리하며

전반적으로 펄을 이용해 외부 명령어를 실행하는 방법을 알아보았습니다. 더불어 Capture::Tiny 모듈을 이용해 표준 출력과 표준 오류를 자유롭게 다루어 보았습니다. 아마 이런 모듈이 없었다면 이런 기능을 복잡하게 직접 구현하기 보다는 표준 출력으로 통합하거나 또는 표준 출력만으로 문제를 해결하는 등의 선에서 적절히 타협을 보았을것 같습니다. 다시 한번 CPAN이라는 저장소에 놀라며 모듈 개발자에게 감사를 표합니다 :)

EOT

blog comments powered by Disqus