소스 필터

소스 필터 (perlfilter)

이 문서는 Perl의 잘 알려지지 않은 기능인 소스 필터(source filter) 를 다뤄요. 소스 필터는 Perl이 코드를 보기 전에 모듈의 프로그램 텍스트를 바꿔줘요. C 전처리기가 C 컴파일러가 소스를 보기 전에 C 프로그램 소스 텍스트를 바꾸는 것과 비슷하죠. 소스 필터가 무엇인지, 어떻게 동작하는지, 직접 만들려면 어떻게 해야 하는지 알려드릴게요.

소스 필터의 원래 목적은 프로그램 소스를 암호화해서 별 생각 없이 읽는 것을 막는 거였어요. 곧 알게 되겠지만 그것만 할 수 있는 건 아니에요. 하지만 먼저 기초부터 볼게요.

출처: perlfilter - Source Filters

본문

CONCEPTS

Perl 인터프리터가 Perl 스크립트를 실행하려면 먼저 파일에서 메모리로 읽어 파싱하고 컴파일해야 해요. 그 스크립트 자체가 userequire 문으로 다른 스크립트를 포함한다면, 그 각각의 스크립트들도 각자의 파일에서 읽어야 해요.

이제 Perl 파서와 개별 파일 사이의 각 논리적 연결을 소스 스트림(source stream) 이라고 생각해 봐요. 소스 스트림은 Perl 파서가 파일을 열 때 만들어지고, 소스 코드가 메모리로 읽히는 동안 계속 존재하다가, Perl이 파일 파싱을 끝내면 파괴돼요. 파서가 소스 스트림에서 requireuse 문을 만나면 그 파일을 위해 새롭고 별개의 스트림이 만들어져요.

아래 다이어그램은 단일 소스 스트림을 나타내요. 왼쪽의 Perl 스크립트 파일에서 오른쪽의 Perl 파서로 소스가 흐르죠. 이게 Perl이 정상적으로 동작하는 방식이에요.

file -------> parser

기억해야 할 중요한 점이 두 가지 있어요:

  1. 어떤 순간에도 존재하는 소스 스트림은 여러 개일 수 있지만, 활성 상태인 것은 하나뿐이에요.

  2. 모든 소스 스트림은 파일 하나에만 연관돼요.

소스 필터는 파서에 도달하기 전에 소스 스트림을 가로채서 수정하는 특별한 종류의 Perl 모듈이에요. 소스 필터는 다이어그램을 이렇게 바꿔요:

file ----> filter ----> parser

그게 잘 이해가 안 된다면 명령 파이프라인에 비유해 보세요. 압축된 파일 trial.gz에 셸 스크립트가 저장돼 있다고 해 봐요. 아래의 간단한 파이프라인 명령은 압축 해제된 파일을 담을 임시 파일을 만들 필요 없이 스크립트를 실행해요.

gunzip -c trial.gz | sh

이 경우 파이프라인의 데이터 흐름은 이렇게 나타낼 수 있어요:

trial.gz ----> gunzip ----> sh

소스 필터로는 스크립트 텍스트를 압축해서 저장하고, 소스 필터를 써서 Perl의 파서를 위해 압축을 풀 수 있어요:

 compressed           gunzip
Perl program ---> source filter ---> parser

USING FILTERS

그럼 Perl 스크립트에서 소스 필터를 어떻게 쓰는 걸까요? 위에서 소스 필터가 그냥 특별한 종류의 모듈이라고 했죠. 다른 모든 Perl 모듈처럼 소스 필터도 use 문으로 호출해요.

실행 전에 Perl 소스를 C 전처리기에 통과시키고 싶다고 해 봐요. 마침 소스 필터 배포판에 Filter::cpp라는 C 전처리기 필터 모듈이 들어 있어요.

아래는 이 필터를 쓰는 예시 프로그램 cpp_test예요. 특정 줄을 쉽게 참조하도록 줄 번호를 붙였어요.

1: use Filter::cpp;
2: #define TRUE 1
3: $a = TRUE;
4: print "a = $a\n";

이 스크립트를 실행하면 Perl은 파일을 위한 소스 스트림을 만들어요. 파서가 파일의 어떤 줄도 처리하기 전에 소스 스트림은 이렇게 생겼어요:

cpp_test ---------> parser

1줄의 use Filter::cppcpp 필터 모듈을 포함하고 설치해요. 모든 소스 필터가 이렇게 동작해요. use 문은 파일의 나머지가 읽히기 전인 컴파일 시간에 컴파일·실행되고, 뒤에서 소스 스트림에 cpp 필터를 부착해요. 이제 데이터 흐름은 이렇게 돼요:

cpp_test ----> cpp filter ----> parser

파서가 소스 스트림에서 두 번째 이후 줄을 읽으면서, 처리 전에 그 줄들을 cpp 소스 필터에 통과시켜요. cpp 필터는 각 줄을 그냥 진짜 C 전처리기에 통과시켜요. C 전처리기의 출력은 필터가 다시 소스 스트림에 되돌려 넣어요.

               .-> cpp --.
               |         |
               |         |
               |       <-'
cpp_test ----> cpp filter ----> parser

그러면 파서는 다음 코드를 보게 돼요:

use Filter::cpp;
$a = 1;
print "a = $a\n";

필터링된 코드가 use로 다른 모듈을 포함할 때 무슨 일이 생기는지 생각해 볼게요:

1: use Filter::cpp;
2: #define TRUE 1
3: use Fred;
4: $a = TRUE;
5: print "a = $a\n";

cpp 필터는 Fred 모듈의 텍스트에는 적용되지 않아요. 그것을 쓴 파일(cpp_test)의 텍스트에만 적용되죠. 3줄의 use 문은 cpp 필터를 통과하지만, 포함되는 모듈(Fred)은 통과하지 않아요. 3줄이 파싱된 뒤, 4줄이 파싱되기 전의 소스 스트림은 이래요:

cpp_test ---> cpp filter ---> parser (INACTIVE)

Fred.pm ----> parser

보시다시피 Fred.pm에서 소스를 읽기 위한 새 스트림이 만들어졌어요. 이 스트림은 Fred.pm이 모두 파싱될 때까지 활성 상태로 남아요. cpp_test의 소스 스트림은 여전히 존재하지만 비활성이에요. 파서가 Fred.pm 읽기를 끝내면 그것과 연관된 소스 스트림은 파괴돼요. 그러면 cpp_test의 소스 스트림이 다시 활성화되고 파서는 cpp_test에서 4번 이후 줄을 읽어요.

단일 파일에 소스 필터를 둘 이상 쓸 수 있어요. 마찬가지로 같은 필터를 원하는 만큼 많은 파일에서 재사용할 수 있어요.

예를 들어 uuencode되고 압축된 소스 파일이 있다면, uudecode 필터와 압축 해제 필터를 이렇게 쌓을 수 있어요:

use Filter::uudecode; use Filter::uncompress;
M'XL(".H<US4''V9I;F%L')Q;>7/;1I;_>_I3=&E=%:F*I"T?22Q/
M6]9*<IQCO*XFT"0[PL%%'Y+IG?WN^ZYN-$'J.[.JE$,20/?K=_[>
...

첫 줄이 처리되고 나면 흐름은 이렇게 돼요:

file ---> uudecode ---> uncompress ---> parser
           filter         filter

데이터는 소스 파일에 나타난 순서대로 필터를 통과해요. uudecode 필터가 uncompress 필터보다 먼저 나타났으므로, 압축이 풀리기 전에 소스 파일이 uudecode되죠.

WRITING A SOURCE FILTER

소스 필터를 직접 만드는 방법은 세 가지예요. C로 작성하거나, 외부 프로그램을 필터로 쓰거나, Perl로 필터를 작성할 수 있어요. 처음 두 가지는 자세히 다루지 않을 거라 먼저 치워 둘게요. Perl로 필터를 작성하는 게 가장 편리해서 가장 많은 부분을 할애할 거예요.

WRITING A SOURCE FILTER IN C

세 가지 방법 중 첫 번째는 필터를 전적으로 C로 작성하는 거예요. 만드는 외부 모듈은 Perl이 제공하는 소스 필터 훅과 직접 인터페이스해요.

이 방식의 장점은 필터 구현을 완전히 제어할 수 있다는 거예요. 큰 단점은 필터 작성에 필요한 복잡도가 늘어난다는 거죠. 소스 필터 훅을 이해해야 할 뿐 아니라 Perl 내부에 대한 합리적인 지식도 필요해요. 이런 수고를 들일 가치가 있는 몇 안 되는 경우 중 하나가 소스 스크램블러를 작성할 때예요. 소스 필터 배포판에 포함된 decrypt 필터(Perl이 파싱하기 전에 소스를 풀어주는)가 C 소스 필터의 예시예요(아래 Decryption Filters 참고).

Decryption Filters

모든 복호화 필터는 "보안은 난독성에 의존"(security through obscurity) 원칙에 기반해 동작해요. 복호화 필터를 아무리 잘 쓰고 암호화 알고리즘이 아무리 강해도, 결심한 사람은 원본 소스 코드를 되찾을 수 있어요. 이유는 아주 간단해요. 프로그램을 실행하려면 Perl이 소스 코드를 파싱해야 하니까요. 이는 Perl이 프로그램을 복호화하는 데 필요한 모든 정보를 가져야 한다는 뜻이고, 즉 그 정보는 프로그램을 실행할 수 있는 누구에게나 이용 가능하다는 뜻이에요.

그렇긴 해도 잠재적 독자의 삶을 어렵게 만들 단계를 몇 가지 밟을 수 있어요. 가장 중요한 것: 복호화 필터를 C로 작성하고 복호화 모듈을 Perl 바이너리에 정적으로 링크하세요. 잠재적 독자의 삶을 어렵게 만드는 더 많은 팁은 소스 필터 배포판의 decrypt.pm 파일을 보세요.

CREATING A SOURCE FILTER AS A SEPARATE EXECUTABLE

C로 필터를 작성하는 대안은 원하는 언어로 별도의 실행 파일을 만드는 거예요. 그 별도 실행 파일은 표준 입력에서 읽어 필요한 처리를 하고 필터링된 데이터를 표준 출력에 써요. Filter::cpp는 별도 실행 파일로 구현된 소스 필터의 예시예요. 그 실행 파일이 바로 C 컴파일러에 딸린 C 전처리기죠.

소스 필터 배포판에는 이 작업을 단순화하는 모듈 두 개가 포함돼 있어요: Filter::execFilter::sh예요. 둘 다 외부 실행 파일을 실행할 수 있게 해줘요. 둘 다 코프로세스(coprocess)를 써서 외부 실행 파일 안팎의 데이터 흐름을 제어해요.(코프로세스에 대한 자세한 내용은 Stephens, W.R., "Advanced Programming in the UNIX Environment." Addison-Wesley, ISBN 0-210-56317-7, 441-445쪽을 보세요.) 둘의 차이는 Filter::exec가 외부 명령을 직접 띄우는 반면, Filter::sh는 외부 명령을 실행할 셸을 띄운다는 거예요.(Unix는 Bourne 셸, NT는 cmd 셸을 써요.) 셸을 띄우면 셸 메타문자와 리다이렉션 기능을 쓸 수 있어요.

Filter::sh를 쓰는 예시 스크립트가 있어요:

use Filter::sh 'tr XYZ PQR';
$a = 1;
print "XYZ a = $a\n";

스크립트가 실행되면 얻는 출력이에요:

PQR a = 1

소스 필터를 별도 실행 파일로 작성하는 건 잘 동작하지만, 약간의 성능 손실이 발생해요. 예를 들어 위의 작은 예시를 실행하면 Unix tr 명령을 실행할 별도 서브프로세스가 생성돼요. 필터를 쓸 때마다 자기 서브프로세스가 필요해요. 시스템에서 서브프로세스 생성이 비싸다면 소스 필터를 만드는 다른 옵션을 고려해 보는 게 좋아요.

WRITING A SOURCE FILTER IN PERL

자신의 소스 필터를 만드는 데 가장 쉽고 이식성 좋은 옵션은 전적으로 Perl로 작성하는 거예요. 앞의 두 방식과 구분하려고 Perl 소스 필터라고 부를게요.

Perl 소스 필터를 어떻게 쓰는지 이해하려면 공부할 예제가 필요해요. 여기 rot13 복호화를 수행하는 완전한 소스 필터가 있어요. (Rot13은 Usenet 게시물에서 불쾌한 게시물의 내용을 숨기는 데 쓰는 아주 간단한 암호화 방식이에요. 각 글자를 13칸 앞으로 옮겨서 A는 N, B는 O, Z는 M이 돼요.)

package Rot13;

use Filter::Util::Call;

sub import {
   my ($type) = @_;
   my ($ref) = [];
   filter_add(bless $ref);
}

sub filter {
   my ($self) = @_;
   my ($status);

   tr/n-za-mN-ZA-M/a-zA-Z/
      if ($status = filter_read()) > 0;
   $status;
}

1;

모든 Perl 소스 필터는 Perl 클래스로 구현되고 위 예시와 같은 기본 구조를 가져요.

먼저 Filter::Util::Call 모듈을 포함하는데, 이 모듈이 필터의 네임스페이스로 여러 함수를 export해요. 위 필터는 그중 filter_add()filter_read() 두 함수를 써요.

다음으로 import 함수를 정의해 필터 객체를 만들고 소스 스트림에 연관시켜요. Perl을 잘 안다면, 모듈이 use 문으로 포함될 때마다 import가 자동으로 호출된다는 걸 알 거예요. 그 덕분에 import는 필터 객체를 만들고 설치하기에 이상적인 위치예요.

예시 필터에서 객체($ref)는 다른 Perl 객체처럼 bless돼요. 우리 예시는 익명 배열을 쓰지만, 필수는 아니에요. 이 예시는 어떤 컨텍스트 정보도 저장할 필요가 없으니 스칼라나 해시 참조를 써도 똑같아요. 다음 절에서 컨텍스트 데이터를 보여드릴게요.

필터 객체와 소스 스트림의 연관은 filter_add() 함수로 만들어져요. 이 함수는 필터 객체를 매개변수(이 경우 $ref)로 받아 소스 스트림에 설치해요.

마지막으로 실제로 필터링을 하는 코드가 있어요. 이런 유형의 Perl 소스 필터에서는 모든 필터링이 filter()라는 메서드에서 수행돼요.(클로저를 써서 Perl 소스 필터를 작성하는 것도 가능해요. 자세한 내용은 Filter::Util::Call 매뉴얼 페이지를 보세요.) Perl 파서가 처리할 소스 줄이 하나 더 필요할 때마다 호출돼요. 그리고 filter() 메서드는 filter_read() 함수로 소스 스트림에서 줄을 읽어요.

소스 스트림에서 줄을 읽을 수 있었다면 filter_read()는 0보다 큰 상태 값을 반환하고 그 줄을 $_에 덧붙여요. 상태 값이 0이면 파일 끝, 0보다 작으면 오류를 뜻해요. 필터 함수 자체도 같은 방식으로 상태를 반환하고, 소스 스트림에 쓰고 싶은 필터링된 줄을 $_에 넣는 게 기대돼요. $_의 사용이 대부분의 Perl 소스 필터가 짧은 이유예요.

rot13 필터를 사용하려면 소스 파일을 rot13 형식으로 인코딩할 방법이 필요해요. 아래 스크립트 mkrot13이 바로 그걸 해요.

die "usage mkrot13 filename\n" unless @ARGV;
my $in = $ARGV[0];
my $out = "$in.tmp";
open(IN, "<$in") or die "Cannot open file $in: $!\n";
open(OUT, ">$out") or die "Cannot open file $out: $!\n";

print OUT "use Rot13;\n";
while (<IN>) {
   tr/a-zA-Z/n-za-mN-ZA-M/;
   print OUT;
}

close IN;
close OUT;
unlink $in;
rename $out, $in;

이걸 mkrot13으로 암호화하면:

print " hello fred \n";

결과는 이렇게 돼요:

use Rot13;
cevag "uryyb serq\a";

실행하면 이 출력이 나와요:

hello fred

USING CONTEXT: THE DEBUG FILTER

rot13 예시는 사소한 예였어요. 여기 더 많은 기능을 보여주는 또 다른 데모가 있어요.

개발 중에는 Perl 스크립트에 디버깅 코드를 많이 넣고 싶은데 출시 제품에는 포함하고 싶지 않다고 해 봐요. 소스 필터가 해결책을 줘요. 예시를 단순하게 유지하려고 디버깅 출력이 환경 변수 DEBUG로 제어되길 원한다고 해 봐요. 변수가 있으면 디버깅 코드가 활성화되고, 없으면 비활성화돼요.

디버깅 코드를 감싸는 특별한 마커 줄 두 개가 있을 거예요:

## DEBUG_BEGIN
if ($year > 1999) {
   warn "Debug: millennium bug in year $year\n";
}
## DEBUG_END

필터는 DEBUG 환경 변수가 존재할 때만 Perl이 <DEBUG_BEGIN>과 DEBUG_END 마커 사이의 코드를 파싱하도록 보장해요. 즉 DEBUG가 존재할 때는 위 코드가 필터를 그대로 통과해야 해요. 마커 줄도 그대로 통과할 수 있는데, Perl 파서가 그것들을 주석 줄로 보기 때문이에요. DEBUG가 설정되지 않았을 때는 디버그 코드를 비활성화할 방법이 필요해요. 간단한 방법은 두 마커 사이의 줄을 주석으로 바꾸는 거예요:

## DEBUG_BEGIN
#if ($year > 1999) {
#     warn "Debug: millennium bug in year $year\n";
#}
## DEBUG_END

여기 완전한 Debug 필터가 있어요:

package Debug;

use v5.36;
use Filter::Util::Call;

use constant TRUE => 1;
use constant FALSE => 0;

sub import {
   my ($type) = @_;
   my (%context) = (
     Enabled => defined $ENV{DEBUG},
     InTraceBlock => FALSE,
     Filename => (caller)[1],
     LineNo => 0,
     LastBegin => 0,
   );
   filter_add(bless \%context);
}

sub Die {
   my ($self) = shift;
   my ($message) = shift;
   my ($line_no) = shift || $self->{LastBegin};
   die "$message at $self->{Filename} line $line_no.\n"
}

sub filter {
   my ($self) = @_;
   my ($status);
   $status = filter_read();
   ++ $self->{LineNo};

   # deal with EOF/error first
   if ($status <= 0) {
       $self->Die("DEBUG_BEGIN has no DEBUG_END")
           if $self->{InTraceBlock};
       return $status;
   }

   if ($self->{InTraceBlock}) {
      if (/^\s*##\s*DEBUG_BEGIN/ ) {
          $self->Die("Nested DEBUG_BEGIN", $self->{LineNo})
      } elsif (/^\s*##\s*DEBUG_END/) {
          $self->{InTraceBlock} = FALSE;
      }

      # comment out the debug lines when the filter is disabled
      s/^/#/ if ! $self->{Enabled};
   } elsif ( /^\s*##\s*DEBUG_BEGIN/ ) {
      $self->{InTraceBlock} = TRUE;
      $self->{LastBegin} = $self->{LineNo};
   } elsif ( /^\s*##\s*DEBUG_END/ ) {
      $self->Die("DEBUG_END has no DEBUG_BEGIN", $self->{LineNo});
   }
   return $status;
}

1;

이 필터와 이전 예시의 큰 차이는 필터 객체에서 컨텍스트 데이터를 쓴다는 거예요. 필터 객체는 해시 참조를 기반으로 하고, filter 함수 호출 사이에 여러 컨텍스트 정보를 보관하는 데 쓰여요. 해시 필드 중 두 개를 제외한 전부가 오류 보고에 쓰여요. 그중 첫 번째 Enabled는 필터가 디버깅 코드를 Perl 파서에 줄지 여부를 정하는 데 써요. 두 번째 InTraceBlock은 필터가 DEBUG_BEGIN 줄을 만났지만 아직 이어지는 DEBUG_END 줄을 만나지 못했을 때 참이에요.

대부분의 코드가 하는 오류 검사를 무시하면 필터의 본질은 이래요:

sub filter {
   my ($self) = @_;
   my ($status);
   $status = filter_read();

   # deal with EOF/error first
   return $status if $status <= 0;
   if ($self->{InTraceBlock}) {
      if (/^\s*##\s*DEBUG_END/) {
         $self->{InTraceBlock} = FALSE
      }

      # comment out debug lines when the filter is disabled
      s/^/#/ if ! $self->{Enabled};
   } elsif ( /^\s*##\s*DEBUG_BEGIN/ ) {
      $self->{InTraceBlock} = TRUE;
   }
   return $status;
}

주의하세요. C-전처리기가 C를 모르듯, Debug 필터도 Perl을 몰라요. 꽤 쉽게 속을 수 있어요:

print <<EOM;
##DEBUG_BEGIN
EOM

그런 것들은 제쳐두고, 적당한 양의 코드로 많은 것을 이룰 수 있음을 알 수 있을 거예요.

CONCLUSION

이제 소스 필터가 무엇인지 더 잘 이해했고, 아마 쓸모있는 방법도 떠올랐을 거예요. 소스 필터를 가지고 놀고 싶은데 영감이 좀 필요하다면, Debug 필터에 추가할 수 있는 기능이 여기 몇 가지 있어요.

먼저 쉬운 것부터요. 전부 아니면 전무인 디버깅 코드 대신, 특정 디버그 블록들만 포함되도록 제어할 수 있으면 훨씬 유용할 거예요. 디버그 블록 구문을 확장해서 각각 식별되도록 해 보세요. 그러면 DEBUG 환경 변수의 내용으로 어떤 블록을 포함할지 제어할 수 있어요.

개별 블록을 식별할 수 있게 되면, 그 블록들이 중첩되도록 해 보세요. 그것도 어렵지 않아요.

Debug 필터와 무관한 재미있는 아이디어가 있어요. 현재 Perl 서브루틴은 형식 매개변수 목록에 대한 지원이 상당히 제한적이에요. 매개변수 수와 타입을 지정할 수는 있지만, 여전히 @_ 배열에서 직접 꺼내야 해요. 이름 붙은 매개변수 목록을 가질 수 있게 해주는 소스 필터를 작성해 보세요. 그런 필터는 이걸:

sub MySub ($first, $second, @rest) { ... }

이렇게 바꿀 거예요:

sub MySub($$@) {
   my ($first) = shift;
   my ($second) = shift;
   my (@rest) = @_;
   ...
}

마지막으로 진짜 도전을 원한다면, 소스 필터로 본격적인 Perl 매크로 전처리기 작성에 도전해 보세요. C 전처리기와 아는 다른 매크로 프로세서에서 유용한 기능을 빌려오세요. 까다로운 부분은 필터가 Perl 구문에 대해 얼마나 많은 지식을 가질지 선택하는 거예요.

LIMITATIONS

소스 필터는 문자열 수준에서만 동작하므로, 소스 코드를 즉석에서 바꾸는 능력이 크게 제한돼요. 주석, 따옴표로 묶인 문자열, heredoc을 감지할 수 없고, 진짜 파서의 대체물이 아니에요. 소스 필터의 유일하게 안정적인 용도는 암호화, 압축, 또는 바이트코드를 소스 코드로 되돌리는 byteloader예요.

예를 들어 Switch의 한계를 보세요. Switch는 소스 필터를 쓰기 때문에 문자열 eval 안에서는 동작하지 않아요. 또 raw /.../ 구분자로 지정되고 //x 수정자가 없는 내장 줄바꿈이 있는 정규 표현식의 존재는, 나눗셈 연산자 /로 시작하는 코드 조각과 구분할 수 없어요. 해결책으로 그런 패턴은 m/.../이나 m?...?를 써야 해요. 또한 raw ?...? 구분자로 지정된 정규 표현식은 수수께끼 같은 오류를 일으킬 수 있어요. 해결책은 m?...?를 쓰는 거예요. https://metacpan.org/pod/Switch#LIMITATIONS를 보세요.

현재 __DATA__ 블록의 내용은 필터링되지 않아요.

현재 내부 버퍼 길이는 32비트로만 제한돼요.

THINGS TO LOOK OUT FOR

일부 필터가 DATA 핸들을 망가뜨린다

일부 소스 필터는 DATA 핸들을 써서 호출 프로그램을 읽어요. 이런 소스 필터를 쓸 때는 이 핸들에 의존할 수 없고, 그것을 조작할 때 어떤 특정한 동작도 기대할 수 없어요. Filter::Util::Call 기반(따라서 Filter::Simple도) 필터는 DATA 파일핸들을 바꾸지 않지만, 반대로 __DATA__ 이후의 텍스트를 완전히 무시해요.

REQUIREMENTS

Source Filters 배포판은 CPAN에서 구할 수 있어요.

CPAN/modules/by-module/Filter

Perl 5.8부터 Filter::Util::Call(Source Filters 배포판의 핵심 부분)은 표준 Perl 배포판의 일부예요. 또한 Damian Conway가 만든 더 친근한 인터페이스 Filter::Simple도 포함돼 있어요.

AUTHOR

Paul Marquess [email protected]

Reini Urban [email protected]

Copyrights

이 글의 첫 버전은 원래 The Perl Journal #11에 실렸고, 1998 The Perl Journal의 저작권이에요. Jon Orwant와 The Perl Journal의 호의로 게재됐어요. 이 문서는 Perl 자체와 같은 조건으로 배포될 수 있어요.

더 알아보기 (Learn more)