리눅스와 함께 꾸준히 발전해온 Perl.
    “CGI 최적언어”라는 평가를 받은 지는 이미 오래지만 국내에 Perl 전문개발자가 의외로 적고 Perl의 쓰임세 또한 가볍게 여기는 경향이 있는 게 현실이다.
    단순히 Perl은 C와 문법이 비슷하고 CGI를 제작하기에 적당하다는 정도로 인식하고 있는 독자라면 연재될 글들을 주의 깊게 살피기를 권하고 싶다.  필자가 연재하게 될 내용은 이미 많은 책자에서 거론되었을 법한 것도 있고 전혀 색다른 시도라고 느껴지는 것도 있을 수 있을 것이다.
    지면 관계상 원하는 모든 내용을 실을 수는 없겠지만 적어도 C보다는 간결하고 쉽게 구현되는 결과물을 접할 수 있도록 구성할 계획이다.

 

Ⅰ. Perl 입문

    필자는 Linux를 접하기 전부터 Perl을 이용해 배포용 CGI(http://way.co.kr)를 위시로 하여 개발해왔으며, Linux를 접하게 된 후부터는 Linux가 Perl 프로그램 개발에 더없이 편리한 환경을 제공한다는 것을 알 수 있었다.
    Linux 설치를 끝냈고 C나 기타 언어를 학습중이거나 학습할 계획인 리눅서라면 Perl을 공부해보라고 권하고 싶다.

    Perl의 기능은 C에서 많이 빌려왔으며 sed나 awk 등의 문자열 처리용 프로그램에서도 많은 기능을 계수하였다.  그래서 C를 어느 정도 알고 있는 사람이 Perl 소스를 접하게 되면 유사한 문법 때문에 대략적인 윤곽을 파악할 수 있게 된다. 그렇다고 Perl을 배우기 위해 일부러 C를 먼저 배우는 어리석음은 범하지 말기를 바란다.

    요즘 필자는 Linux에서 vi에디터를 이용해 Perl 프로그램을 개발하고 있다.  대부분의 Linux 배포판은 Perl이 기본적으로 설치되고 레드햇 등의 배포판은 rpm과 같은 바이너리 패키지 형태로 제공되어 설치 및 관리가 간편하다.  만약 Perl을 직접 컴파일하여 설치하려거든 http://www.perl.com/등에서 파일을 다운로드하면 되지만 구체적인 설치방법은 지면관계상 생략하기로 한다.  만약 윈도우즈 계열의 서버에서 테스트해보고 싶다면 http://way.co.kr의 “CGI 강좌” 게시판을 참고하기 바란다.

    먼저 Perl이 설치되어있는지 확인해보자.  대부분의 경우 /usr/bin 경로에 perl 프로그램이 위치할 것이다.  레드햇의 경우 $ rpm -qa | grep perl 명령으로도 설치여부를 확인할 수 있다.  perl -v를 입력하면 설치된 Perl의 버전정보를 얻을 수 있으며 perl -V는 좀더 상세한 정보를 얻을 수 있다.

    $ perl -v

    This is perl, version 5.004_05 built for i386-linux
    (with 1 registered patch, see perl -V for more detail)

    Copyright 1987-1998, Larry Wall
    < 하략 >

    프로그래밍 언어 중에는 C 등과 같이 컴파일을 통해 독자적인 파일 하나로도 프로그램 실행이 가능한 언어가 있다. 하지만 Perl은 프로그램 소스를 해석하여 실행해주는 프로그램(인터프리터, interpreter)이 설치되어 있어야만 하고 그 때문에 Perl이나 shell 스크립트는 소스의 첫 번째 줄에 #!/usr/bin/perl 등과 같이 해석용 프로그램의 위치를 적어주어야 하는 것이다.  그러니까 Perl이 설치되어있지 않은 서버에서는 Perl 소스 프로그램을 실행할 수 없다고 보면 되는 것이다.(최근에는 소스를 컴파일 하는 방법도 제공되고 있다.)

 

1. 실행방법

    Perl 소스 프로그램에 실행권한(chmod +x)을 부여하고 첫 번째 줄에 Perl의 위치를 적어줄 경우 다음과 같이 프로그램명만으로 직접 실행시킬 수 있다.  
    특히 웹상에서 실행하고자 할 경우 이와 같이 해주어야 한다.

    $ program(프로그램명)

    현재 경로가 PATH에 걸려있지 않다면 ./program과 같이 실행하여야 한다.
    만약 shell 상태에서 다음과 같이 실행한다면 소스 첫 번째 줄에 Perl의 위치를 적어주거나 실행권한을 부여할 필요가 사라진다.

    $ perl 프로그램명

    작성된 소스코드의 오류를 점검하기 위해서는 perl -c 옵션을 이용한다.  
    소스코드에 이상이 없을 경우 “syntax OK”라는 메시지가 나타난다.

    $ perl -c 프로그램명

 

2. 프로그램 맛보기

    Perl이 시스템에 설치되어있다면 이제부터 간단한 Perl 문법부터 익혀보도록 하자.

    print “Hello, Way!\n”;

    간단하면서도 완벽한 하나의 프로그램이다. vi 등의 편집기에 소스내용을 입력하여 저장한 후 실행해 보자.

    $ perl hello(파일명)
    Hello, Way!

    소스를 분석해보면, “Hello, Way!”라는 문자열과 함께 줄 바꿈 문자(\n)를 출력하는 것이며 각 줄의 끝은 ;(세미콜론) 기호로 마무리한다.

 

3. 변수와 기본연산

    다른 언어들과는 달리 변수형을 지정할 필요 없이 대입과 동시에 변수형이 결정된다.  변수는 $기호로 시작되며 배열의 경우 @, 조합배열(hash)은 %기호로 시작된다.  $bom = 123; 과 같이 $bom 변수에 123이라는 정수를 대입하게 되면 별도의 조치 없이 정수형을 갖게 된다.  변수간의 연산도 매우 자유롭다.  예를 들어 다음과 같은 경우 결과값은 정상적인 수치 연산결과인 100이 출력된다.

    $bom = 49;   # 정수
    $danvi = “51”;   # 문자열
    print $bom + $danvi;  # 100 출력

    perl 문법상 # 기호 이후부터는 주석으로 인식한다.  예상하듯이 Perl에서는 사칙연산에 +, -, *, / 기호가 이용되며 나머지(계수)에는 % 기호가 사용된다.
    문자열 연산자는 .(마침표, dot)를 사용하며, 연산방법 또한 앞에서와 같이 간단하다.

    $bom = 100;  # 정수
    $danvi = “200”;  # 문자열

    print $bom . $danvi; 와 같이 문자열 연산자를 이용하면 “100200”이 출력되고 print $bom + $danvi; 와 같이 수치연산자를 이용하면 300이 출력된다.

 

4. 조건문

    if (조건) { 참일 때; }
    else { 거짓일 때; }

    두 개 이상의 조건문은 elsif(“elseif”가 아님에 주의)를 이용하여 다음과 같이 구성할 수 있다.

    if (조건) { 참일 때; }
    elsif (조건) { 새로운 조건이 참일 때; }
    elsif (조건) { 새로운 조건이 참일 때; }
    . . . .
    else { 모두 거짓일 때; }

    조건판별을 위한 연산자로서 ==(eq)는 일치, !=(ne)는 불일치로 사용되며 <, >, <=, >= 등의 기호를 사용할 수 있다.  또한 논리연산자로서 &&(and), ||(or), !(not)도 사용할 수 있다.(기호와 영문 모두 사용할 수 있다.)

    $bom = 5;
    if ($bom < 10 and $bom > 3) {
      print “10보다는 작고 3보다는 크다. \n”; }

    소스를 실행해보면 당연히 “10보다는 작고 3보다는 크다.”가 출력될 것이다.

    if와는 반대로 사용되는 조건문으로는 unless가 있다.

    unless (조건) { 거짓일 때; }
    else { 참일 때; }

 

5. 반복문

    while (조건) { 조건이 참인 동안 실행; }
    until (조건) { 조건이 거짓인 동안 실행; }
    무한루프를 원치 않는다면 당연히 중괄호({})를 실행하는 동안 조건과 관련한 값이 변화되어야 할 것이다.

    $bom = 1;
    while($bom <= 10) {
       print $bom, “\n”;
       $bom++; }

    초기의 $bom의 값은 1이었고 해당 값이 10보다 작기 때문에 while문의 조건에 일치하므로 중괄호({}) 가 실행된다.  중괄호로 처리방향이 넘겨진 후 print 문에 따라 $bom의 값인 1이 출력된다. 만약 $bom++; 와 같이 $bom 값을 변화시켜주지 않는다면 $bom 값은 영원히 1일 것이므로 무한루프가 되어버릴 것이다. $bom++ 는 자동증가를 뜻하는 것으로서 원래의 $bom 값에 1을 가산한 후 다시 $bom에 값을 저장하게 되는 것이다. $bom-- 는 그 반대이다. 루프를 한번 돌게되면 $bom 값은 2가 되고 한번씩 더 돌 때마다 3, 4, 5, 6, 7, 8, 9, 10이 된다.  $bom 값이 10일 경우 10을 출력하고 $bom++ 에 따라 $bom 값이 11이 되면서 while문의 조건에 맞지 않아 루프는 결국 종료된다.

    또다른 반복문으로는 for문이 있다.

    for (초기값; 조건; 변화값) { 조건이 참인 동안 실행; }

    다음 소스는 앞서 설명한 while문의 예제와 동일한 결과를 보이게 된다.

    for ($bom = 1; $bom <= 10; $bom++) {
       print $bom, “\n”; }

    반복문중에는 foreach문도 있으나 배열과 관련하여 주로 사용되므로 설명을 뒤로 미루기로 하겠다.

 

6. 파일 핸들링

    파일을 읽거나 저장하기 위해서는 파일을 열어야하며 일반적으로 다음과 같은 규칙에 의해 사용되어진다.

    open(파일핸들, “[조작방법] 파일명”);

    ‘파일핸들’은 사용자의 식별을 용이하게 하기 위해 임의로 지정하여 사용한다. ‘조작방법’에는 읽기전용(< 또는 생략), 쓰기(>), 덧붙이기(>>) 등이 있다.

    open(FILE, “>bom”);
      print FILE “Way\n”;
    close(FILE);

    분석해보면 다음과 같다.

    open(FILE, “>bom”);

    bom이라는 파일명으로 파일을 쓰기모드로 열되 파일핸들은 FILE로 지정한다.

    print FILE “Way\n”;

    FILE이라는 파일핸들(실제로는 bom이라는 파일)에 “Way\n”라는 문자열을 기록한다.

    close(FILE);

    FILE 이라는 핸들의 파일을 닫는다.  조작이 끝나는 즉시 닫는 게 바람직하다.

    앞에서 알 수 있듯이 파일핸들을 사용함으로 해서 특정 파일에 print(기록)하고, 손쉽게 파일을 닫을 수 있게 된다.
    소스가 실행되게 되면 bom이라는 파일이 생성되어지고 파일에는 Way라는 내용이 들어가게 될 것이다.
    쓰기모드일 경우 이미 동일한 파일이 존재하면 내용을 덮어쓰게 되므로 종전내용을 잃게 된다. 하지만 덧붙이기(>>)모드로 파일을 열게 되면 종전내용의 하단에 새로운 내용이 추가된다.(대부분의 log파일 저장방식과 동일하다.)

    이제 다음과 같은 소스를 작성하여 실행해보자.

    open(FILE, “>>bom”);
      print FILE “Way2\n”;
    close(FILE);

    특별한 문제가 없는 한 bom이라는 파일내용은 다음과 같을 것이다.

    $ cat bom
    Way
    Way2
    $

    저장하는 방법을 알았으니 이제 읽어 오는 방법을 알아보자.
    open(FILE, “bom”);
      $line1 = <FILE>;
      $line2 = <FILE>;
    close(FILE);

    <FILE>이 어떤 기능을 하는지 궁금할 것이다.  <FILE>에서 FILE은 앞에서와 마찬가지로 파일핸들이며 <>(‘줄입력 연산자’라고도 한다.)로 묶이므로 해서 한 줄을 읽어 온다는 뜻이다.

    $line1= <FILE>;
    bom파일의 첫 번째 줄 내용을 $line1 이라는 변수에 저장(대입)한다.

    $line2 = <FILE>;
    한번 더 <FILE>을 사용하였으므로 두 번째 줄을 읽어 $line2에 저장하는 것이다.  그런데 모든 파일의 내용을 하나의 변수에 넣어 조작하고자 한다면 몇 줄인지 알 수 없기 때문에 앞의 방법보다는 다음과 같이 while문을 이용하는 것이 편리하다.

    open(FILE, “bom”);
      while($line = <FILE>) {
            $all_line = $all_line . $line; }
    close(FILE);

    약간 복잡하게 느껴질 수도 있을 것이다.  첫줄과 마지막 줄은 설명이 필요 없을 듯 하여 가운데부분만 설명하도록 하겠다.  while문은 참일 경우 바로 뒤에 위치한 중괄호({})가 실행된다.  Perl에서는 아무런 값이 없을 경우 ‘거짓’을 돌려주므로 bom 이라는 파일을 모두 읽어오고 나면 while 루프가 종료되는 것이다.  약간 더 설명을 덧붙이자면, $line = <FILE> 이 참일 경우(읽어올 줄이 있을 경우는 참이 된다.) $line 변수에 읽어온 줄의 내용을 저장하고 문자열연산자(.)에 의해 $all_line 변수에 추가되며, 파일 끝까지 읽고 나면 읽어올 줄이 없어 거짓을 리턴 하므로 while 루프가 종료되는 것이다.

    다음과 같이 간단히 나타낼 수도 있다.

    open(FILE, “bom”);
      while(<FILE>) { $all_line .= $_; }
    close(FILE);

    $all_line .= $_ 부분은 $all_line = $all_line . $_와 동일한 결과를 보인다.

    이제 특정파일의 내용을 읽어 변수에 저장하였으며 이로써 조작이 가능하게 되었다.

    파일을 열거나 조작하고자 할 때 발생할 수 있는 오류를 미리 예방하고 파일(또는 디렉토리)에 관한 정보를 얻어 활용하기 위해 Perl은 다양한 파일테스트 연산자를 제공한다.

    주요 연산자는 다음과 같다.

     

    - e

      존재 여부

    - r

      읽기 가능 여부

    - w

      쓰기 가능 여부

    - x

      실행 가능 여부

    - d

      디렉토리 여부

    - T

      텍스트파일 여부

    - B

      이진파일(binary)  여부

    - s

      파일크기

     

    if (-e “bom”) { print “파일 있음\n”; }
    else { print “파일 없음\n”; }

    print “bom 파일의 크기는 “, -s “bom”, “ byte입니다.\n”;

 

7. 함수

    구조적인 프로그램을 위해, 특히 빈번히 사용되는 특정 기능이나 처리절차를 하나의 함수로 생성하여 활용할 수 있다.

    함수는 다음과 같이 구성되며 &함수명과 같이 호출한다. 함수는 중괄호({})로 묶이며 함수영역 내외를 불문하고 사용되어지는 문법은 동일하다.

    sub 함수명 { 처리내용; }

    앞서 설명했던 for문을 이용해 함수를 적용해보겠다.

    for ($bom = 1; $bom <= 10; $bom++) {
       print $bom, “\n”;
       &job; }

    sub job { print “===============\n”; }

    소스를 실행해보면 1부터 10까지 출력되는 과정중 숫자 사이사이에 구분선이 그어질 것이다.  이런 용도의 함수는 작업을 좀더 간결히 하는 정도의 효과밖에 기대할 수 없지만, 다음과 같은 경우 호출시 &함수명(“넘길값”)과 같이 인자를 넘기고 호출된 함수에서는 넘겨받은 인자를 계산(처리)하여 결과를 리턴하게되는 완전한 함수의 형태를 띄게 된다.

    for ($bom = 1; $bom <= 10; $bom++) {
       $danvi = &job($bom);
       # job 함수로 $bom이라는 변수값을 넘기고,
       # 돌려 받은 처리결과를 $danvi 변수에
       # 저장한다.
       print $danvi, “\n”; }

    sub job {
      ($input) = @_;   # 값을 넘겨받는다.
      $output = $input * 10;  # 계산한다.
      return $output; }  # 결과를 돌려준다.

    소스를 실행해보면 10, 20, 30, ... 과 같이 $bom 값에 10을 곱한 값이 순서대로 출력된다.

 

8. 배열

    여러 개의 값들을 하나의 변수에 저장하여 활용하기 위해 배열(array)과 조합배열(hash)이 지원된다.  둘의 차이점은 인덱스(index)가 숫자인가 그렇지 않은가 이다.

    배열에 값들을 직접 대입하는 방법은 괄호를 이용하여 다음과 같이한다.
    @way = (1, 2, 3, 4, 5);

    배열대입은 다음과 같이 개별적으로도 가능하다.
    $way[0] = 1;
    $way[1] = 2;
    $way[2] = 3;
    $way[3] = 4;
    $way[4] = 5;

    배열을 일반변수형태로 추출하기 위해서는 다음과 같이 사용한다.
    $way[배열번호]

    첫 번째 값의 배열번호는 0이다.  따라서 $way[0]에는 1이, $way[1]에는 2가 저장되어 있는 것이다.

    배열에서 값을 추출하여 또 다른 변수에 저장해보자.
    $bom1 = $way[0];
    $bom2 = $way[1];
    $bom3 = $way[2];
    $bom4 = $way[3];
    $bom5 = $way[4];

    또 다른 방법으로는 다음과 같은 방법도 존재한다.
    배열에 직접 대입하던 것과는 반대이다.
    ($bom1, $bom2, $bom3, $bom4, $bom5) = @way;

    조합배열은 %기호를 사용하며 숫자인덱스 대신 임의의 문자열(key)을 인덱스로 갖는다.  따라서 저장된 순서는 그다지 큰 의미가 없다.

    %way = (“빨강” => “red”,
                 “파랑” => “blue”,
                 “검정” => “black”);

    앞서 설명한 배열과 동일한 방법으로 대입하였지만 주의 깊게 관찰할 부분은 => 기호를 이용해서 key와 value로 구성된다는 것이다.

    마찬가지로 $way{‘빨강’} = “red”; 와 같은 방식으로 개별적인 저장도 가능하다.

    자료를 추출하기 위해서는 다음과 같이 사용하면 된다.

    $way{key}

    $way{‘빨강’} 에는 “red”가, $way{‘파랑’}에는 “blue”가 저장되어 있다.

    보통 => 기호 대신 ,(comma)를 사용하기도 하지만 가독성을 높이기 위해 => 기호를 사용하는 것이 바람직하다.

    print “파랑색은 영문으로 $way{‘파랑’}입니다.\n”;

    소스가 실행되면 “파랑색은 영문으로 blue입니다.”가 출력될 것이다.
    조합배열은 keys와 values를 이용해 인덱스나 값만을 추출해낼 수 있다.
    print keys %way
    소스가 실행되면 “빨강파랑검정” 등과 같이 출력된다.

    이제 앞에서 설명을 미뤘던 foreach문에 대해 알아보자.

    foreach 추출값 (배열 또는 목록) { 실행영역; }

    배열값을 하나씩 추출하고 그때마다 중괄호로 묶인 실행영역을 거치게 된다. 배열의 모든 원소가 추출되고 나면 루프는 종료된다.

    @bom = (“봄”, “여름”, “가을”, “겨울”);
    foreach $danvi (@bom) {
      $count ++;
      print “$count, $danvi\n”; }

    소스를 실행하면 다음과 같이 출력될 것이다.

    1, 봄
    2, 여름
    3, 가을
    4, 겨울

    foreach문은 다음과 같이 사용할 수도 있다.  결과는 앞의 결과와 동일하다.

    foreach $danvi (“봄”,”여름”,”가을”,”겨울”) {
      $count ++;
      print “$count, $danvi\n”; }

    배열은 @way = (); 또는 %way = (); 와 같이 초기화할 수 있다.

    배열에 원소를 추가하고자 할 경우 push를 이용한다.
    push(배열명, 추가할 값);

    @way = (1, 2, 3, 4, 5);
    push(@way, 6);

    @way 에는 6이라는 원소가 추가된다.

 

9. 문자열 처리

    Perl은 문자열 처리에 상당한 능력을 과시하고 있다.

    “ ”기호로 묶인 상태에서 변수를 직접 사용할 수 있다.
    $way = “CDE”;
    $way1 = “AB$way”;  # ABCDE

    특정 문자를 지정한 수량만큼 복제하기 위해 x 라는 특수한 연산자를 이용한다.  구분선 출력이나 열을 맞추기 위한 공백 등에 자주 사용된다.
    $way2 = “=” x 10;   # ==========

    문자열중 일부분을 추출하는 방법은 다음과 같다.  시작위치는 0부터이다.

    substr(변수, 시작위치, 글자수);

    $way 라는 변수의 값중 첫 번째부터 2글자를 추출하여 $way3에 저장하는 방법은 다음과 같다.
    $way3 = substr($way, 0, 2);   # CD

    문자열 중에서 특정문자의 존재여부를 파악하는 방법은 다음과 같다.  실행결과 “있음”이 출력된다.

    $way = “봄 여름 가을 겨울”;
    if($way =~ /봄/) { print “있음\n”; }
    else { print “없음\n”; }

    문자열을 치환(substitution)하려면 다음과 같이 하면 된다.  실행결과 “이것이 사랑일까?”가 출력된다.

    $way = “이것이 우정일까?”;
    $way =~ s/우정/사랑/;
    print “$way\n”;

 

10. 시스템 명령

    `(역따옴표)기호를 이용할 경우 원하는 시스템 명령을 실행할 수 있으며 실행결과를 얻어와 활용할 수 있다.

    $way = `ls -al`;
    print $way;

    이렇게 할 경우 shell에서와 마찬가지로 ls 명령결과가 출력된다.

    $bom = `w`;
    @user = (“way”, “lawwal”, “danvi”);
    foreach $user_id (@user) { &login_check($user_id); }

    sub login_check {
     ($id) = @_;
     if ($bom =~ /$id/) { print “$id : On-Line\n”; }
      else { print “$id : Off-Line\n”; }
    }

    필자가 사용중인 서버에서 소스를 실행한 결과 다음과 같이 출력되었다.

    way : On-Line
    lawwal : On-Line
    danvi : Off-Line

    만약 결과를 얻어올 필요가 없고 명령에 따라 부수적인 작업을 해야 한다면 system이나 exec를 사용한다.  exec의 경우 실행중인 Perl 프로그램은 종료되고 지정된 명령어가 실행된다는 차이점이 있다.

    print “top 명령을 실행할까요? [Y/n] “;
    $input = <STDIN>;
    if ($input =~ /y/i) { system(“top”); }
    else { print “취소되었습니다.\n”; }

    줄입력연산자인 <> 사이에 STDIN(표준입력)이 들어가게 되면, 키보드로부터 값을 입력받는다. 입력된 값에 Y 또는 y가 존재하는 경우(i 옵션은 대소문자를 구별하지 않는다.) top 명령을 실행하도록 한다.

    이번에는 대략적인 문법을 위주로 설명하였다. 기본적인 문법을 토대로 하나씩 응용범위를 늘려가다 보면 여러분도 어렵지 않게 쓰임세 있는 Perl 프로그램을 개발할 수 있을 것이다.

    다음에는 웹프로그램을 위한 CGI로서의 Perl을 살펴보도록 하겠다.

 

    UNIX 계열에서 사용되는 웹프로그램 언어로는 C, Perl, PHP 등이 있는데 장단점을 개인적 견해로 비교해보면 다음과 같다.

    C는 코딩속도와 라이브러리 등에서 열세를 보이는 반면 실행속도가 빠르다는 장점이 있다.
    Perl의 경우 거의 대부분의 UNIX 시스템에 기본적으로 설치되어 있어 시스템간 호환성이 뛰어나고 코딩 속도도 빠르지만 인터프리터 언어 특성상 실행속도가 약간 느리고 다수 프로그램이 동시에 실행될 경우 서버에 부담이 될 수 있다.  PHP는 코딩 속도와 실행속도 면에서 우위를 보이지만 웹서버와 조화를 이뤄야 하므로 범용 프로그램으로 배포하기엔 부적절한 데다 비대한 웹서버가 동작하게 되어 서버에 부담이 될 수 있다.

    필자가 다른 언어보다 Perl을 주로 사용하는 이유는 코딩속도와 시스템 도구나 문자열 조작도구로 사용할 수 있기 때문이다.

 

Ⅱ. CGI Using Perl

    생소할지 모르지만 다음의 소스는 본쉘(bash) 스크립트를 이용한 간단한 CGI이다.

      #!/bin/bash
      echo Content-type: text/html
      echo
      echo “bash를 이용한 CGI입니다.<BR>”
      echo “접속주소 : $REMOTE_ADDR”

    다음은 C를 이용한 CGI이다.  소스를 way.c라는 파일에 저장한 후 컴파일($ gcc -oway.cgi way.c)해서 웹상에서 실행해보면 간단한 결과가 출력된다.

      main() {
       printf (“Content-type: text/html\n\n”);
       printf (“C를 이용한 CGI입니다.<BR>”);
       printf (“접속주소 : %s”, getenv(“REMOTE_ADDR”));
      }

    이렇듯 많은 언어나 쉘스크립트를 이용해 CGI를 만들어낼 수 있다.

 

1. Perl CGI 맛보기

    Perl로 웹상에서 특정 문자를 출력하기 위해서는 내용을 출력하기 전에 다음과 같은 헤더를 출력하여 형식을 선언해줘야 한다.

      print “Content-type: text/html\n\n”;

    간혹 뉴스그룹(han.comp.lang.perl)에 올라오는 질문 중 적지 않는 질문이 이런 사소한 문제가 원인인 경우가 많다.
    특히 “Internal Server Error(500)”는 대부분 웹서버 Error log나 구문점검 옵션($ perl -c 파일명)을 통해 원인을 파악할 수 있다.
    다음 소스를 way.cgi 라는 파일로 저장한 후 실행권한을 부여(chmod +x)하면 웹브라우져로 “Hello, Way!”라는 내용을 접할 수 있다.

      #!/usr/bin/perl
      print “Content-type: text/html\n\n”;
      print “<H1>Hello, Way!</H1>\n”;

    특정 URL로 접속을 전환(Redirect)하기 위해서는 다음과 같이 사용한다. 보통 http:// 등의 프로토콜 지정 없이 상대 URL로 사용할 수도 있지만 원칙적으로 프로토콜까지 포함해서 적어줘야 한다. 생략할 경우 오라클 웹서버, PWS 구버전 등의 일부 웹서버에서는 에러가 발생하기도 한다. 다음 소스가 브라우져로 실행되면 지정된 URL로 접속이 전환된다.

      #!/usr/bin/perl
      print “Location: http://way.co.kr\n\n”;

 

2. 아파치 웹서버 설정

    만약 사용하는 웹서버에서 CGI 실행을 허용하지 않거나 CGI 파일에 실행권한이 부여되지 않았을 경우 “Forbidden(403)”이나 “Method Not Allowed(405)” 에러가 발생할 것이다. 경우에 따라 Perl 소스가 브라우져에 그대로 출력되기도 한다.

    ※ 아파치 웹서버 CGI 실행오류 해결방법

    가. AddHandler cgi-script .cgi 부분의 주석 제거
    나. <Diretory> 설정 중 Options 에 ExecCGI 추가

     

      <Directory “/usr/local/apache/htdocs”>
      Options Indexes Includes ExecCGI
      AllowOverride FileInfo AuthConfig
      </Directory>
      다음은 계정사용자의 웹문서 경로에 CGI실행을
      허용하는 설정이다.
      <Directory /home/*/public_html>
      Options Indexes Includes ExecCGI
      AllowOverride FileInfo AuthConfig
      </Directory>

     

    다. ScriptAlias 설정

     

      ScriptAlias /cgi-bin/ “/usr/local/apache/cgi-bin/”
      (VirtualHost의 경우도 적용이 가능하다.)

      ·ScriptAlias란?
      1. 실제 디렉토리를 가상 디렉토리로 인식하도록
          한다.(Alias, 별칭 기능)
      2. 디렉토리 내의 파일을 브라우저로 접근하면
          무조건 실행 프로그램(CGI)으로 취급하여 실행한다.
      3. 보안상 HTML 문서 등은 퍼미션 에러나 서버에러가
          발생하고 이미지는 나타나지 않는다.

     

3. FORM 태그

    HTML 문서를 이용해 CGI에 정보를 전달하기 위해 FORM 태그가 사용된다.  FORM 태그는 여느 HTML 태그와 마찬가지로 <FORM>으로 시작하여 </FORM>으로 끝이 난다.

    <FORM ACTION=”연결할 CGI URL” METHOD=GET>

    TARGET을 지정하거나 NAME을 지정할 수 있으며, 다음과 같이 파일 업로드를 위한 선언도 가능하다.

    <FORM ACTION=”연결할 CGI URL” METHOD=POST
      ENCTYPE=”multipart/form-data”>

    다음과 같이 FORM 태그 내부에 <INPUT>, <SELECT>, <TEXTAREA> 등의 태그를 사용하여 실질적인 정보를 전달하도록 한다.

    <FORM ACTION=”way.cgi” METHOD=GET>
      성명 <INPUT TYPE=TEXT NAME=user><BR>
      주소 <INPUT TYPE=TEXT NAME=address><BR>
      성별 <SELECT NAME=sex>
              <OPTION>남
              <OPTION>여
           </SELECT><BR>
           <INPUT TYPE=SUBMIT VALUE=”전송”><BR>
    </FORM>

    성명에 way를 입력하고 주소에 Kwangju를, 성별은 ‘남’을 선택하면 브라우져 주소입력줄에 다음과 같이 나타나며 지정된 CGI에 정보를 전달하게 된다.(GET 방식의 예이다.)

    way.cgi?user=way&address=Kwangju&sex=%B3%B2
    ----- ----- -------- ------- -------------
      CGI        성명                   주소                    성별

    ? 기호를 구분자로 하여 실행될 CGI(좌측)와 인자(우측)로 구성되며 각 인자들은 & 기호로 연결된다.  각 인자들은 = 기호로 키(항목명)와 값으로 구성된다.

 

4. 인자의 전달과 처리

    FORM 태그나 GET방식의 직접연결을 통해 전달된 정보는 약간의 절차를 거쳐야 Perl에서 활용할 수 있다.  특정 기호로 나뉜 인자들을 추출해 내야하고 성별과 같이 16진수 형태로 전달된 한글이나 특수문자를 정상적인 자료로 변환해야 한다.  이를 위해 많은 이들이 cgi-lib.pl 라이브러리나 CGI.pm 모듈을 사용하는 것이 보통이다.  하지만 필요한 범위 이상의 비대한 소스를 사용하고 싶지 않기에 필자는 한번도 사용해본 적이 없다.  적어도 다음의 소스 정도면 넘겨받은 인자를 Perl에서 정상적인 자료로 활용할 수 있기 때문이다.

     

      sub input_form {
      # 사용할 변수를 지역변수로 선언
      local ($form, $tmp, $key, $value);
      # GET, POST 방식을 판별하여 인자 습득
      if($ENV{‘REQUEST_METHOD’} eq “GET”) {
      $form = $ENV{‘QUERY_STRING’}; }
      if($ENV{‘REQUEST_METHOD’} eq “POST”) {
      read(STDIN, $form, $ENV{‘CONTENT_LENGTH’}); }
      # 키와 값으로 구분하고 16진수를 일반문자로 변환
      foreach $tmp (split(/&/, $form)) {
        ($key, $value) = split(/=/, $tmp);
        $key   =~ tr/+/ /;
        $value =~ tr/+/ /;
        $key   =~ s/%(..)/pack(“C”, hex($1))/ge;
        $value =~ s/%(..)/pack(“C”, hex($1))/ge;
        $input{$key} = $value; }
      }

     

    소스를 간략히 설명하면 다음과 같다.
    1. 인자 전달방식(GET 또는 POST) 판별
    2. 전체 인자 습득
    3. 전체인자에서 & 기호를 기준으로 각 인자 추출
    4. 각 인자에서 = 기호를 기준으로 키와 값으로 구분
    5. + 기호로 넘어온 공백문자를 정상적인 공백으로 변환
    6. 한글이나 특수문자인 16진수 자료를 일반문자로 변환
    7. %input 조합배열에 키와 값 저장

    이제 &input_form; 을 먼저 호출하고 $input{‘항목명’} 과 같이 자료를 추출하여 활용할 수 있다.

    앞서 설명한 FORM 태그가 포함된 HTML 문서에서 CGI를 호출하면 전달된 정보를 출력하는 CGI를 만들어 보자.

    다음은 HTML문서(FORM 태그 사용)에 웹브라우져로 정보를 입력하는 화면(lynx)이다.

     

      성명   way_________________
      주소   Kwangju_____________
      성별   [남]
      [ 전송 ]

     

    다음은 way.cgi 파일이다.  반드시 Perl경로를 확인하고 실행권한을 부여해야 정상적인 결과를 얻을 수 있을 것이다.

     

       #!/usr/bin/perl

      &input_form;
      &print_html;

      sub input_form { ... }  # 앞서 설명한 소스
      sub print_html {
      print “Content-type: text/html\n\n”;
      print “<HR>\n”;
      print “<H2>원래 전달되었던 인자</H2>\n”;
      print “$ENV{‘QUERY_STRING’}<BR>\n”;
      print “<HR>\n”;
      print “<H2>변환하여 활용이 가능해진 자료</H2>\n”;
      print “성명 : $input{‘user’}<BR>\n”;
      print “주소 : $input{‘address’}<BR>\n”;
      print “성별 : $input{‘sex’}<BR>\n”;
      print “<HR>\n”;
      exit;
      }

     

    정상적인 경우 결과는 다음과 같다.

     

       ___________________________________________
      원래 전달되었던 인자
      user=way&address=Kwangju&sex=%B3%B2
      ___________________________________________
      변환하여 활용이 가능해진 자료
      성명 : way
      주소 : Kwangju
      성별 : 남
      ___________________________________________

     

    5. 모듈과 라이브러리
    Socket, DBI, DBD 등의 Perl 모듈은 Perl의 활용범위를 확장시켜주는 매우 유용한 도구이다.  Perl 모듈을 사용하기 위해서는 다음과 같이 선언해주어야 한다.

    use 모듈명;

    Socket 모듈의 경우 원격통신을 가능하게 해주는 도구로서, sendmail 이 지원되지 않는 UNIX계열 서버나 WindowsNT/98/95 운영체제의 서버에서 SMTP 서버에 접속하여 메일을 전송하는 경우가 좋은 예이다.  관심 있는 독자는 Way-BOARD(Ver1.2)를 다운로드 하여 압축을 해제한 후 lib/mail.cgi 파일 하단의 Socket Mail 부분을 참고하기 바란다. 많은 모듈은 Perl이 설치되면서 함께 설치되지만 그 밖의 모듈은 http://www.perl.com/CPAN/에서 구할 수 있다.

    자주 사용되는 함수를 특정 파일에 넣어두고 필요에 따라 꺼내 쓸 수 있다.

    require(“라이브러리 파일명”);

    C나 PHP의 include문과 거의 동일한 기능을 한다.  앞의 인자변환 소스도 라이브러리 파일에 넣어두고 각종의 CGI에서 함께 공유할 경우 작업효율은 극대화 된다.  라이브러리 파일에 넣어둘 경우 실행권한을 부여하거나 Perl 위치를 적어줄 필요가 없지만 유의할 점은 파일의 제일 하단에 다음과 같이 return 1; 이라는 줄을 추가해줘야 에러가 발생하지 않는다는 것이다.

     

      # Perl Library File

      sub input_form { ... }

      sub html_header { ... }

      return 1;

     

    6. 환경변수
    앞서 설명한 인자처리 소스와 bash, C를 이용한 CGI에서 각각 환경변수를 읽어들여 활용하는 법을 볼 수 있었다.  Perl에서는 환경변수가 조합배열인 %ENV에 저장되어있다.
    다음은 Perl, PHP, C, bash에서 접속자의 IP정보를 얻어내는 방법이다.

     

    구  분

    환경변수 습득방법

    perl

    $ENV{‘REMOTE_ADDR’}

    PHP, C

    getenv(“REMOTE_ADDR”)

    bash

    $REMOTE_ADDR

     

    다음의 소스를 실행해보면 Perl에서 사용되는 각종의 환경변수명과 값을 구할 수 있다.

      #!/usr/bin/perl
      print “Content-type: text/html\n\n”;
      foreach $env_val (keys %ENV) {
        print “* $env_val : $ENV{$env_val}<BR>\n”; }

    다음은 필자의 서버에서 소스를 실행한 결과다.  대충 훑어보면 어떤 기능을 하는 것들인지 알 수 있을 것이다.  경우에 따라 환경변수 종류가 늘어나거나 줄어들 수 있다.

     

      * SERVER_SOFTWARE : Apache/1.3.9 (Unix)
      * GATEWAY_INTERFACE : CGI/1.1
      * DOCUMENT_ROOT : /home/way/public_html
      * REMOTE_ADDR : 210.123.123.123
      * SERVER_PROTOCOL : HTTP/1.1
      * SERVER_SIGNATURE : Apache/1.3.9 Server
         at way.co.kr Port 80
      * REQUEST_METHOD : GET
      * QUERY_STRING :
      * HTTP_USER_AGENT : Mozilla/4.0 (compatible;
         MSIE 5.0); Windows 98
      * PATH : /bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin
      * HTTP_ACCEPT : */*
      * HTTP_CONNECTION : Keep-Alive
      * REMOTE_PORT : 1129
      * SERVER_ADDR : 210.123.254.100
      * HTTP_ACCEPT_LANGUAGE : ko
      * SCRIPT_NAME : /test/env.cgi
      * HTTP_ACCEPT_ENCODING : gzip, deflate
      * SCRIPT_FILENAME : /home/way/
         public_html/test/env.cgi
      * SERVER_NAME : way.co.kr
      * REQUEST_URI : /test/env.cgi
      * SERVER_PORT : 80
      * HTTP_HOST : way.co.kr
      * SERVER_ADMIN : webmaster@way.co.kr

     

    만약 브라우져에 입력된(특히 VirtualHost에서) 도메인을 얻기 위해서는 $ENV{‘HTTP_HOST’}라는 변수를 읽어들이면 된다.

    예시에 나와있지 않지만 HTTP_COOKIE(쿠키정보), HTTP_REFERER(CGI 실행 URL), REMOTE_USER
    (인증 사용자 ID) 등의 환경변수도 매우 유용하다.

    다음 소스는 환경변수를 이용해 스스로의 URL을 표시하는 간단한 소스이다.

      #!/usr/bin/perl
      print “Content-type: text/html\n\n”;
      print “http://$ENV{‘HTTP_HOST’}$ENV{‘SCRIPT_NAME’}\n”;

 

7. 시간정보

    시간정보를 얻기 위해서는 time() 함수를 이용한다. 얻을 수 있는 값은 1970년 1월 1일을 기준으로 한 초 단위 값이다. 보통의 경우 localtime() 함수를 사용하여 단위별 값들을 리스트로 추출해낸다. 다음은 시간정보를 보기 좋게 추출해내는 소스이다. 주의할 점은 요일($weekday)이나 월($month)은 0부터 시작된다는 것이다. 또한 항간에 시끄러운 Y2K문제와 관련해 고려할 부분은 $year가 실제 연도에서 1900을 뺀 수치로 구성된다는 것이다. 그러니까 $year 값에 1999년은 99, 2000년은 100이 저장된다.
    외국서버를 사용하는 경우와 같이 필요에 따라 지역시간대에 따른 조정이 필요하면 time()에 초단위로 조정을 가할 수 있다. localtime(time()+(12*60*60))와 같이 사용할 경우 서버시간에서 12시간 빠른 시간정보가 추출된다.

     

      #!/usr/bin/perl
      print “Content-type: text/html\n\n”;
      ($second, $minute, $hour, $date, $month,
      $year, $weekday, $yearday, $Isdst)
      =  localtime(time());
      $month ++;
      $year += 1900;

      @dayname = (“일”,”월”,”화”,”수”,”목”,”금”,”토”);
      $weekday = $dayname[$weekday];

      print “일자 : $year년 $month월 $date일 $weekday요일<BR>\n”;
      print “시간 : $hour시 $minute분 $second초<BR>\n”;

     

    PHP에서 사용하는 date 함수처럼 표시형식의 자유로운 지정이 가능한 Perl 함수를 만들어보는 것도 좋은 공부가 될 것이다.

     

8. 연속출력

    정보를 연달아 출력하기 위해서는 수많은 print문을 사용하기도 하고 print문 하나로 다수의 라인을 출력하기도 하지만 “ 기호 등을 표시할 때 \”와 같이 표시해 줘야하므로 매우 불편하다. 하지만 다음과 같이 print문을 print <<”구분문자”;와 같이 사용하게 되면 “구분문자”가 나타날 때까지 계속 출력할 수 있다. 해당범위 내에서는 HTML 문서처럼 코딩해도 크게 문제가 없으며 Perl에서 사용중인 변수를 직접 사용할 수도 있고 “ 등의 큰따옴표를 가공 없이 사용할 수도 있다.  
    유의할 점은 끝을 알리는 “구분문자”는 첫 번째 자릿수부터 입력되어야 한다는 것과 메일주소에 쓰이는 @기호는 \@와 같이 사용해야 한다는 것이다. 만약 print <<’EOF’;와 같이 작은따옴표를 이용해 구분문자를 감싸주면 @ 기호를 그대로 사용할 수 있지만 변수참조가 불가능하다.

     

      #!/usr/bin/perl
      $user = “way”;
      $address = “Kwangju”;
      print “Content-type: text/html\n\n”;
      print <<EOF;
      <HTML>
      <HEAD>
      <TITLE>연속출력 시험</TITLE>
      </HEAD>
      <BODY>
      성명 : “$user”<BR>
      주소 : “$address”<BR>
      </BODY>
      </HTML>
      EOF
      print “출력완료”;

     

9. 메일전송

    게시판에 새로운 글이 등록되거나 방문자가 방명록에 글을 남길 경우, 특정양식의 신청서를 입력받는 경우 메일로 정보가 전달되도록 메일을 전송하는 방법을 알아보자.
    앞서 설명했듯이 파일을 조작하기 위해 다음과 같이 사용했었다.

    open(파일핸들, “[조작방법] 파일명”);

    여기에 조작방법을 “|” 기호로 하고 파일명 대신 명령어(sendmail)나 장치(프린터 등)를 적어주면 Perl에서 정보를 전달할 수 있다.

    다음은 메일을 전송하는 소스이다.  @기호는 \@와 같이 표시하거나 큰따옴표 대신 작은따옴표로 감싸주면 에러가 발생하지 않는다.

     

      #!/usr/bin/perl
      open(MAIL,”|/usr/sbin/sendmail -t”);
        print MAIL “To: way\@abc.co.kr\n”;
        print MAIL “Subject: 제목입니다.\n\n”;;
        print MAIL “메세지 본문입니다.”;
      close (MAIL);

      print “Content-type: text/html\n\n”;
      print “전송완료”;

     

    발송자(From)를 지정하려면 To 아랫줄에 다음과 같이 삽입한다.

    print MAIL “From: 임대호 <lawwal\@way.co.kr>\n”;

    HTML형식으로 메일을 보내려면 Subject 부분을 다음과 같이 바꿔주면 된다.

    print MAIL “Subject: 제목입니다.\n”;
    print MAIL “Content-type: text/html\n\n”;

 

10. 쿠키

    게시판에 글을 쓰면 다음에 글을 쓸 때 자동으로 이름과 메일주소를 적어주거나 접속자 인증에 주로 쿠키가 사용된다.  Perl에서도 쿠키를 직접 핸들링할 수 있다.
    다음 소스는 Perl에서 사용자의 브라우져에 쿠키값을 저장하는 함수이다.

     

      sub cookie_set {
         local($name, $value, $exp) = @_;
         $exp = scalar localtime(time()+(24*60*60)*$exp);
         print “Content-type: text/html\n”;
         print “Set-Cookie: $name=;\n”;
         print “Set-Cookie: $name=$value;expires=$exp;\n”;
       }

     

    사용자의 브라우져에 저장된 쿠키값은 환경변수인 $ENV{‘HTTP_COOKIE’}에 저장된다.
    다음 소스는 쿠키를 읽어들여 %cookie라는 조합배열에 넣어 활용이 가능토록 하는 함수이다.  자세히 보면 앞서 설명한 input_form() 함수와 유사하다는 것을 알 수 있을 것이다.

     

      sub cookie_read {
      @strings = split(/; /,$ENV{‘HTTP_COOKIE’});
      foreach $strs (@strings) {
         local ($name, $value) = split(/=/,$strs);
             $value =~ tr/+/ /;
             $value =~ s/%(..)/pack(“C”, hex($1))/ge;
             $cookie{$name} = $value; }
        }

     

    두 개의 함수를 이용해서 쿠키를 저장하고 읽어보도록 하겠다.

      #!/usr/bin/perl
      &cookie_set(“name”, “way”, 7);   # 키, 값, 만료기일
        print “Content-type: text/html\n\n”;
        print “쿠키를 저장하였습니다.”;

      #!/usr/bin/perl
      &cookie_read;
      print “Content-type: text/html\n\n”;
        print “읽어온 name의 값은 $cookie{‘name’}입니다.”;

 

11. 실행권한과 보안

    Perl뿐만 아니라 모든 CGI가 파일을 조작할 경우 웹서버가 nobody 등으로 실행되므로 nobody 등의 소유로 파일이 생성되고 조작되어진다.  이는 실제 파일소유자가 접근하기도 곤란할뿐더러 동일한 서버를 사용하는 구성원은 공통의 권한을 가지는 형국이 되므로 보안문제가 야기될 수 있다.(역효과를 우려해 소스를 소개하지는 않겠다.)  해결방법으로는 다음 세 가지 정도로 요약할 수 있다. 세 가지 방법 모두 실제 사용자의 권한으로 실행되므로 중요한 정보를 지닌 자료파일이나 디렉토리는 chmod go-rwx 와 같이 해두어도 무방하다.

가. 아파치 suexec 적용

    아파치 설치시 다음과 같이 suexec를 적용하고 컴파일한다.  가상 도메인의 경우 아파치 설정시 <VirtualHost> 부분에 User와 Group을 실제 계정 사용자로 지정해주면 된다.

    # ./configure --prefix=/usr/local/apache
     --enable-suexec
     --suexec-caller=nobody
     --suexec-docroot=/home
     --suexec-logfile=/usr/local/apache/logs/suexec_log
     --suexec-userdir=public_html
     --suexec-uidmin=500
     --suexec-gidmin=500
     --suexec-safepath=/usr/local/bin:/usr/bin:/bin
    # make
    # make install

    * 아파치 VirtualHost 설정
    <VirtualHost 210.123.254.100>
       User           way
       Group          user
       ServerAdmin    webmaster@way.co.kr
       DocumentRoot   /home/way/public_html
       ScriptAlias    /cgi-bin/ /home/way/public_html/cgi-bin/
       ServerName     www.way.co.kr
       ServerAlias    way.co.kr www.way.co.kr
       TransferLog    /home/way/www_log/today.log
       ErrorLog       /home/way/www_log/error.log
    </VirtualHost>

나. cgiwrap 적용

    ftp://ftp.umr.edu/pub/cgi/cgiwrap/cgiwrap-Version.tar.gz
    # tar xvfz cgiwrap-Version.tar.gz
    # ./configure --help(옵션 확인)
    # ./configure --with-httpd-user=nobody
    # make
    # make install
    # cp cgiwrap /usr/local/apache/cgi-bin
    # cd /usr/local/apache/cgi-bin
    # chown root cgiwrap
    # chmod 4755 cgiwrap
    # ln -s cgiwrap cgiwrapd
    # ln -s cgiwrap nph-cgiwrap
    # ln -s cgiwrap nph-cgiwrapd

    필자의 경우 위와 같이 설치한 후 cgi-bin은 이미 /home/way/public_html/cgi-bin 으로 ScriptAlias 되어있으므로 /usr/local/apache/cgi-bin 디렉토리를 cgi-sys로 ScriptAlias해서 다음과 같이 실행하였다.

    * 시스템 경로 : /home/way/public_html/cgi-bin/file.cgi
    * 실행URL : http://way.co.kr/cgi-sys/cgiwrap/way/file.cgi
      (우측의 way는 사용자 ID를 뜻한다.)

다. Set User ID

    Perl 경로를 적는 소스 첫 번째줄에 -U 옵션을 함께 적어준 후 chmod +s way.cgi 와 같은 방법으로 파일에 실행권한을 부여한다.
    #!/usr/bin/perl -U

     

12. 방명록 만들기

    이제 CGI 예제로 간단한 방명록을 한번 만들어 보자.  지면관계상 메일이나 쿠키, 페이지 나눔 등은 배제하도록 하겠다.  참고로 방명록파일(guestbook.dat)은 nobody 등의 웹사용자가 쓸 수 있는 권한이 부여되어야 한다.

    #!/usr/bin/perl
    # 먼저 인자정보를 습득한다.
    &input_form;

    # 전달된 값중 ‘작업’ 항목의 값이 없을 경우
    # 이미 등록된 글을 출력(방명록 보기)하고 종료한다.
    if(!$input{‘작업’}) { &print_guestbook; exit; }

    # 전달된 값중 ‘작업’ 항목의 값이 ‘쓰기’일 경우
    # 쓰기화면을 출력한다.
    elsif($input{‘작업’} eq “쓰기”) {
           &guestbook_write; exit; }

    # 전달된 값중 ‘작업’ 항목의 값이 ‘처리’일 경우
    # 글을 등록하고 등록된 글을 출력한다.
    elsif($input{‘작업’} eq “처리”) {
           &guestbook_add; &print_guestbook; exit; }

    # 어느것도 아닐 경우 그냥 종료한다.
    else { exit; }

    sub input_form {

    # 앞서 설명했던 인자처리함수이다.
    local ($form, $tmp, $key, $value);
    if($ENV{‘REQUEST_METHOD’} eq “GET”) {
      $form = $ENV{‘QUERY_STRING’}; }
    if($ENV{‘REQUEST_METHOD’} eq “POST”) {
      read(STDIN, $form, $ENV{‘CONTENT_LENGTH’}); }
    foreach $tmp (split(/&/, $form)) {
      ($key, $value) = split(/=/, $tmp);
      $key   =~ tr/+/ /;
      $value =~ tr/+/ /;
      $key   =~ s/%(..)/pack(“C”, hex($1))/ge;
      $value =~ s/%(..)/pack(“C”, hex($1))/ge;
      $input{$key} = $value; }
     }

    sub print_guestbook {

    # HTML 머릿말 출력
    &html_start(“방명록 보기”);

    print “<TABLE BORDER=1 WIDTH=500>\n”;

    # 방명록 파일로 사용할 파일명을 지정한다.
    local $file = “guestbook.dat”;

    # 파일이 존재하지 않거나 크기가 0byte면 작성된
    # 자료가 없다고 출력하고 종료한다.
    if(!-e $file || -s $file eq 0) {
      print “<TR><TD>작성된 글이 없습니다.</TD></TR>\n”;
      &guestbook_write_link;
      print “</TABLE>\n”;
      &html_end; exit; }

    local $print_msg;

    # 방명록 파일을 열어 출력한다.
    # 파일을 열 수 없을 경우 종료한다.
    open(FILE, “$file”) || die “파일을 열 수 없습니다.($file)”;
      while(<FILE>) {
        # 한줄을 읽어와서 끝에있는 개행문자(\n)를 제거한다.
        chomp $_;
        # 읽어온 줄의 자료를 | 기호를 기준으로 추출하여
        # @guest 라는 배열에 집어 넣는다.
        local @guest = split(/\|/, $_);
        # 항목별로 추출한다.
        local $name = $guest[0];
        local $mail = $guest[1];
        local $home = $guest[2];
        local $date = $guest[3];
        local $usip = $guest[4];
        local $mesg = $guest[5];
        # 자료를 가공(링크 삽입 및 시간 출력)한다.
        if($mail) { $mail =
         “<A HREF=mailto:$mail>$mail</A>”; }
        if($home) { $home =
         “<A HREF=$home>$home</A>”; }
        $date = &time_conv($date);
        # HTML 형식으로 자료를 변수에 저장한다.
        $print_msg .= “<TR>\n”;
     

       $print_msg .= “<TD><FONT SIZE=2>\n”;
        $print_msg .= “◈ 성명 : $name($mail)<BR>\n”;
        $print_msg .= “◈ 홈페이지 : $home<BR>\n”;
        $print_msg .= “◈ 일자 : $date (IP:$usip)<BR>\n”;
        $print_msg .= “◈ $mesg”;
        $print_msg .= “</FONT></TD>\n”;
        $print_msg .= “</TR>\n”;
      }  # -- while 루프 종단점
    close(FILE);

    # 모든 글을 출력한다.
    print $print_msg;

    # 글쓰기 부분의 링크를 출력한다.
       &guestbook_write_link;
       print “</TABLE>\n”;
    # HTML을 마무리한다.
       &html_end;
    }

    sub guestbook_write {

    &html_start(“방명록 쓰기”);

    # 등록양식을 출력한다.
    # 주의할 사항은 FORM ACTION 부분과
    # 하단의 ‘작업’ 부분이다.
    print <<”EOF”;
    <FORM ACTION=$ENV{‘SCRIPT_NAME’}
                 METHOD=POST>
    <TABLE BORDER=1 WIDTH=500>
    <TR>
    <TD>성명</TD>
    <TD><INPUT TYPE=TEXT NAME=name  SIZE=20></TD>
    </TR>
    <TR>
    <TD>E-mail</TD>
    <TD><INPUT TYPE=TEXT NAME=mail SIZE=35></TD>
    </TR>
    <TR>
    <TD>홈페이지</TD>
    <TD><INPUT TYPE=TEXT NAME=home SIZE=35
            VALUE=”http://”></TD>
    </TR>
    <TR>
    <TD>남기실 내용</TD>
    <TD><TEXTAREA NAME=mesg
               ROWS=5 COLS=35></TEXTAREA>
    </TR>
    <TR>
    <TD COLSPAN=2>
    <INPUT TYPE=SUBMIT VALUE=”글을 등록합니다.”>
    </TD>
    <INPUT TYPE=HIDDEN NAME=”작업” VALUE=”처리”>
    </FORM>
    </TR>

    </TABLE>
    EOF
    &html_end;
    }

    sub guestbook_add {

    # 전달받은 각 항목별 정보를 저장하기 위해 준비한다.
    # 등록된 시간과 글쓴이의 IP주소도 습득한다.
    local $name = $input{‘name’};
    local $mail = $input{‘mail’};
    local $home = $input{‘home’};
    local $date = time();
    local $usip = $ENV{‘REMOTE_ADDR’};
    local $mesg = $input{‘mesg’};

    # $mesg 값에서 개행문자를 공백문자로 변환한다.
    $mesg =~ s/\cM//g;
    $mesg =~ s/\r\n/\n/g;
    $mesg =~ s/\n/ /g;

    # 가급적 자료 구분자로 사용할 | 기호가 입력된
    # 자료를 다른 기호로 변환한다.
    $mesg =~ s/|/\:/g;
    local $file = “guestbook.dat”;
    local $original;

    # 최근에 등록된 글을 먼저 출력하기위해
    # 종전에 등록된 글이 있을 경우 읽어온다.
    if(-e $file) {
    open(FILE, “$file”) || die “파일을 열수 없습니다.($file)”;
      while(<FILE>) { $original .= $_; }
    close(FILE); }

    # 새로운 글을 저장하고 종전의 글도 뒤이어 저장한다.
      open(NEW, “>$file”) || die “파일을 열수 없습니다.($file)”;
      print NEW “$name|$mail|$home|$date|$usip|$mesg|\n”;
      print NEW $original;
    close(NEW);
    }

    sub html_start {

    # TITLE로 인쇄할 정보를 넘겨받는다.
    local($title) = @_;

    # HTML 문서형식으로 출력하겠다고 선언한다.
    print “Content-type: text/html\n\n”;

    # HTML 태그 연속 출력(TITLE 포함)
    print <<”EOF”;
    <HTML>
    <HEAD>
    <TITLE>$title</TITLE>
    </HEAD>
    <BODY>
    <HR SIZE=1>
    <H2>$title</H2>
    <HR SIZE=1>
    EOF
     }

    sub html_end {

    # HTML 출력을 마무리한다.
    print <<”EOF”;
    <HR SIZE=1>
    </BODY>
    </HTML>
    EOF
      }

    sub guestbook_write_link {

    # ‘쓰기’ 링크를 출력한다.
    print <<”EOF”;
    <TR>
    <TD>[ <A HREF=”$ENV{‘SCRIPT_NAME’}?작업=쓰기”>글쓰기</A> ]</TD>
    </TR>
    EOF
     }

    sub time_conv {

    # 시간변환 함수이다.
    # time() 값으로 저장된 자료를 출력형식에 맞게
    # 변환한다.
    local ($data) = @_;
    local ($second, $minute, $hour, $date, $month,
    $year, $weekday, $yearday, $Isdst) = localtime($data);
    $month ++;
    $year += 1900;
    $month < 10 && ($month = “0$month”);
    $date < 10 && ($date = “0$date”);
    $hour < 10 && ($hour = “0$hour”);
    $minute < 10 && ($minute = “0$minute”);
    $second < 10 && ($second = “0$second”);
    return “$year/$month/$date $hour:$minute:$second”;
     }

 

    Perl을 이용해서 할 수 있는 일에는 무엇이 있을까? 그나마 많은 이들에게 알려진 Perl의 용도가 CGI 개발일 것이다. Perl이 CGI 개발도구로 부각되기까지는 자유로운 자료형, 막강한 문자열 처리, 소스의 공개성, CPAN 등을 통해 보급되는 풍부한 모듈과 스크립트 등이 많은 기여를 했다.
    이번 단원에서는 CGI로서의 Perl이 아니라 UNIX 시스템을 좀더 편리하게 다루기 위한 ‘시스템 도구로서의 Perl’을 소개해보도록 하겠다.

     

Ⅲ. Perl 시스템 도구

    사용하는 서버에 mysql이 설치되어 있다면 mysql 설치경로에서 mysql_setpermission 프로그램을 찾아보기 바란다. 이 프로그램은 다름 아닌 Perl로 만들어진 시스템 도구의 일종이다. 이 프로그램은 mysql을 사용함에 있어 번거로운 작업이 아닐 수 없는 database와 user의 생성 및 권한조정 등의 작업을 자동화한 도구이다. 뿐만 아니라 my sqlaccess, mysql_find_rows, mysql_zap 등의 프로그램도 마찬가지로 Perl로 만들어진 시스템 도구의 일종이다. 이렇듯 Perl을 이용해 시스템 도구를 만들어두면 빈번하게 발생하는 복잡한 작업들을 자동화할 수 있어 서버 운영자 및 시스템 사용자에게 편리함을 제공한다.

    Perl 소스 프로그램을 시스템 도구로 직접 사용하기 위해서는 앞서 설명했듯이 실행권한(chmod +x)을 부여하고 소스 첫 번째 줄에 Perl의 위치를 지정해줘야 한다. 또한 Perl 소스 프로그램이 PATH에 걸려있지 않다면 ./program 과 같이 실행해야 한다는 점을 잊지 말자.

     

    1. Perl 명령행 옵션

    Perl을 명령행에서 실행할 경우 목적에 따라 다양한 옵션을 사용할 수 있다. Perl이 시스템 도구로 사용될 때 특히 옵션을 자주 사용하게 되는데, 주로 사용되는 명령행 옵션은 다음과 같다.
     

    옵션

    설명

    e

    주어진 Perl 명령 실행

    p

    지정한 파일을 대상으로 작업

    i

    원본파일을 결과파일로 대체

    c

    구문검사(문법에러 점검)

    v

    Perl 버전정보

    V

    Perl 버전정보(상세)

    w

    경고 출력(개발과정에 주로 사용)

    U

    비안전모드, SUID 적용시 사용

     

    2. 문자열 일괄 치환

    회사명이나 도메인, 메일주소 등이 바뀌었을 때 수많은 문서파일에 적힌 내용을 모두 고치기란 쉬운 일이 아니다. 하지만 Perl을 이용하면 너무도 간단히 변경 내용을 반영할 수 있다. 아래 명령은 html 확장자를 지닌 문서파일에서 “우정”이라는 문자열을 찾아 “사랑”이라는 문자열로 모두 치환하는 명령(Perl 명령행 옵션)이다.
     

      $ perl -pi -e “s/우정/사랑/g”  *.html

     

    find 명령을 이용하면 하위 경로의 문서도 일괄적으로 치환할 수 있다.
     

      $ find . -type f -name “*.html”  \
      -exec perl -pi -e “s/우정/사랑/g” {} \;

     

    3. 파일명 대소문자 일괄변경

    파일명의 대소문자를 구분하지 않는 운영체제(윈도우즈 등)에서 생성된 파일명은 많은 문제를 야기하곤 한다. 특히 HTML 문서에서 링크오류를 유발하거나 파일명을 입력할 때 입력혼선을 가중시키기도 한다. 하지만 다음 도구(recase)를 이용하면 파일명을 소문자 또는 대문자로 일괄적으로 변경할 수 있다.
     

    $ ls
    HOME.GIF LIST.GIF NEXT.GIF
    $ recase * (또는 $ recase -l *)
    $ ls
    home.gif list.gif next.gif
    $ recase -u *
    $ ls
    HOME.GIF LIST.GIF NEXT.GIF

     

    File Name : recase

    #!/usr/bin/perl
    # 사용법 : recase [옵션] [파일 리스트]
    # 옵션 : -l(소문자, 기본값), -u(대문자)
    # 옵션 점검(소문자화 또는 대문자화)
    if($ARGV[0]  =~  /-L/i) { $method=”L”; shift; }
    elsif($ARGV[0]  =~  /-U/i) { $method=”U”; shift; }
    else { $method = “L”; }

    foreach $file (@ARGV) {
    # 디렉토리인 경우 처리하지 않음
    next if -d $file;
    ($dir, $filename)  =  $file  =~  /(.*[\/\\])(.*)$/;
    $filename = $file if $file !~ /\//;
    # 변경이 필요한 파일명인지 확인
    next if $method eq “L” and
    $filename  !~  /[A-Z]/;
    next if $method eq “U” and
    $filename  !~  /[a-z]/;
    # 대소문자 변환
    $filename  =~  tr/A-Z/a-z/ if $method eq “L”;
    $filename  =~  tr/a-z/A-Z/ if $method eq “U”;
    if(-f “$dir$filename”) {
    # 새로운 이름의 파일이 이미 존재하는 경우
    print “Already exist! : $dir$filename\n”;
    next; }
    # 파일명 변경
    rename(“$file”, “$dir$filename”);

     

    특정경로 이하의 모든 파일명을 소문자로 변경하려면 다음과 같이 사용하면 된다.
     

    $ find way-cart -type f -exec recase {} \;

     

    4. 문서형식 변환(UNIX ↔ DOS)

    Perl이나 shell 스크립트 파일을 바이너리(Binary) 모드로 전송(FTP 등)하면 스크립트 실행시 에러가 발생하곤 한다. 이를 극복하기 위해서는 파일을 아스키(ASCII) 모드로 다시 전송하면 되지만, Perl을 이용해 간단히 해결할 수도 있다. 이는 앞서 설명한 ‘문자열 치환’과 동일한 원리다.
     

    # DOS → UNIX 문서형식으로 변환
    $ perl -pi -e “s/\r//” *.cgi

    # UNIX → DOS 문서형식으로 변환
    $ perl -pi -e “s/\n/\r\n/” *.txt

     

    명령(Perl 명령행 옵션)이 기억하기 쉽지만은 않을 테니 shell 스크립트로 한번 만들어 보자. 파일에 실행권한을 부여한 후 사용자들의 PATH에 포함되어있는 경로(/usr/local/bin 등)에 위치해두면 편리하게 사용할 수 있을 것이다.
     

    File Name : dos2unix

    #!/bin/sh
    # DOS → UNIX 문서형식으로 변환
    perl -pi -e “s/\r//” $*

    File Name : unix2dos
    #!/bin/sh
    # UNIX → DOS 문서형식으로 변환
    perl -pi -e “s/\n/\r\n/” $*

     

    $ dos2unix way-board.cgi

     

    5. 파일 크기정보 출력 및 활용

    다음은 지정한 파일의 크기를 출력하는 도구(sizeof)로서, 얻어진 정보는 다른 응용프로그램에서도 활용할 수 있다.

     

    File Name : sizeof

    #!/usr/bin/perl
    # 1. 파일테스트 연산자중 -s(Size) 옵션 이용
    # 2. 하나의 파일만 확인할 때는
    # 다른 응용프로그램에서 활용할 수 있도록
    # 파일크기만 출력
    if($#ARGV eq 0) {
    print -s $ARGV[0]; exit; }
    foreach (@ARGV) {
    print “$_ : “, -s $_, “\n”; }

     

    $ sizeof  *.cgi
    way-admin.cgi : 46031
    way-board.cgi : 19065
    way-notice.cgi : 6264

    그런데 이런 도구는 언제 쓰일까?
    필자는 매주 1G byte가 넘는 자료를 원격으로 백업 받는다. 그런데 백업파일이 저장될 컴퓨터를 항상 켜둘 수 없는 특수한 상황인지라 백업이 종료되면 자동으로 halt시켜야 한다.
    문제는 정상적으로 백업이 이루어졌는지 알 수 없는 상태에서 무턱대고 halt시켜서는 안된다는 점이다. 그래서 생각해낸 것은(물론 다른 방법으로도 가능하다.) 수신된 백업파일 크기를 확인하여 원본파일과 동일한 크기일 경우만 halt명령을 실행하도록 하는 것이다.
     

    File Name : remotebackup

      #!/bin/sh
      # 사용법 : remotebackup [백업파일크기]
      # 파일 다운로드
      ncftpget -u way -p “********”  \
      “ftp://way.co.kr/backup/19991231.tar.gz”
      # 외부명령인 Perl 프로그램을 실행하고
      # 결과를 받아오기 위해 Perl에서와 마찬가지로
      # shell 스크립트도 `(역따옴표)기호 사용
      SIZE=`sizeof 19991231.tar.gz`
      # 입력한 파일크기와 백업 받은 파일크기 비교
      if [ “X$1” = “X${SIZE}” ]
      # 일치(정상백업)할 경우 시스템 종료
      then halt
      fi

     

    6. 수치정보 컴마 표기

     

    File Name : comma

    #!/usr/bin/perl
    # 사용법 : comma [수치]
    1 while $ARGV[0]   =~   s/(\d+)(\d{3})/$1,$2/;
    print $ARGV[0];

     

    이 도구는 또 언제 사용될까? 필자는 계정 사용자의 웹서버 로그파일을 이용해 일일 자료 전송량을 산출한다. 산출된 정보를 출력할 때 식별을 용이하게 하기 위해 이 도구를 이용해 3자리마다 컴마를 넣어 출력하고 있다.

    $ comma 1234567890
    1,234,567,890

    앞서 소개한 remotebackup과 마찬가지로 shell 스크립트 등의 응용프로그램에서 외부명령으로 comma 명령을 사용하면 보기 좋게 수치정보를 표시할 수 있을 것이다.

    파일 크기를 파악한 후 보기 좋게 출력해보자.

    $ comma `sizeof way-board.cgi`
    19,065

     

    7. 파일 부분 출력

    이제 소개할 도구는 특정 파일의 일부분을 출력하는 도구다. 예를 들어 tmp라는 파일의 5번째 줄부터 3줄을 출력하고자 한다면 다음과 같이 사용한다.

     

    $ partprint 5 3 tmp

     

    $ cat tmp
    Line 1
    Line 2
    Line 3
    Line 4
    Line 5
    Line 6
    Line 7
    Line 8
    Line 9
    Line 10

    $ partprint 5 3 tmp

            FILE : tmp
            LINE : (from 5 to 7 / total 10)

    Line 5
    Line 6
    Line 7

     

    File Name : partprint

    #!/usr/bin/perl
    # 사용법 : partprint [시작줄] [출력줄] [파일]
    $start = shift;
    $end = shift;
    $filename = shift;
    open(FILE, “$filename”) or
    die “Error: $filename, $!”;
    @File = <FILE>;
    close(FILE);
    $start --;
    $start = 0 if $start < 0;
    $end = 0 if $end < 0;
    $end  --;
    $end = $start + $end;
    print “=” x 80, “\n”;
    print “FILE : $filename\n”;
    print “LINE : “;
    print “(from “, $start + 1, “ to “, $end + 1,
    “ / total “, $#File+1, “)\n”;
    print “=” x 80, “\n”;
    @File = @File[$start .. $end];
    foreach (@File) { print $_; }

     

     

    8. 웹호스팅 서버관리 시스템

     

    이제 간단하면서도 종합적인 기능을 수행하는 시스템 도구를 만들어 보자. 이 도구는 세 곳의 웹호스팅 업체에서 10개월 전부터 유용하게 사용되고 있다. 지면으로 옮기기 어려운 일부 기능은 배제하였으며, 소스를 소개하고 각 부분의 기능을 설명함으로써 이해와 응용을 돕고자 한다.


    실제 사용을 원한다면 임시 서버를 이용해 여러 차례 시험을 거친 후 사용하기를 권하며 super user 권한으로 실행되므로 세심한 주의를 요한다.

     

    주요 기능은 다음과 같다.


    가. 사용자 계정 관리
    나. POP 계정 관리
    다. 디스크 할당
    라. 도메인 설정
    마. 데몬(bind, sendmail, apache) 관리

     

    #!/usr/bin/perl


    #=====================================
    config();
    menu();
    #=====================================

     

    sub config {
    # 이 루틴은 시스템에서 사용할 주요 설정을 선언하는
    # 부분이다.

    # 그룹 및 사용자 기본값으로서
    # 이미 존재하는 그룹 및 사용자여야 한다.
    # 계정 추가시 기본 그룹
    $user_basegroup = “user”;
    # POP계정 추가시 기본 그룹
    $pop_basegroup = “pop”;
    # 디스크 할당 표본 사용자
    $disk_baseuser = “way”;

    # 사용자 계정 관리 등에서 조작이 허용되는 최소 UID
    $min_uid = 500;

    # 사용자에게 부여할 IP 주소를 기록한다.
    # Name-based Virtual Host(apache)를 전제로 한다.
    $ip_address = “210.123.12.1”;

    # 아래 경로들은 /etc/skel에 미리 생성되어 있어야 한다.
    # $ mkdir /etc/skel/public_html  /etc/skel/wwwlog
    # 웹문서 경로
    $webdir = “public_html”;
    # 웹로그 경로
    $logdir = “wwwlog”;

    # 아래는 특수한 경우가 아닌 한 변경할 필요가 없다.
    # shadow 시스템을 사용하더라도 변경할 필요가 없다.
    # 비밀번호 파일
    $pass_file = “/etc/passwd”;
    # 그룹파일
    $group_file = “/etc/group”;

    # Message
    $prog_msg = “[MSG] “;
    }

     

    #=====================================

     

    sub menu {
    # 이 루틴은 기본 메뉴화면을 출력하는 기능을 한다.

    # 실행자가 super user가 아니면 종료한다.
    chomp($run_id = `id -u`);
    if($run_id ne “0”) {
    print “$prog_msg 실행권한이 없습니다.(UID:$run_id)\n”;
    exit; }

    # get_date 함수를 통해 현재 시간정보를 얻는다.
    $date_info = get_date(time);
    $date_seri = “$date_info” . “01”;

    # 화면을 정리하고 출력한다.
    system(“clear”);
    print  <<”__END__”;

            웹호스팅 서버 관리 시스템
    11. 계정 등록                      26. 사용자 password 지정
    12. 계정 삭제                      27.
    13. 계정 조회                      28. 프로세서 조회
    14. 전체 계정 조회               29. 데몬 관리
    15. POP 계정 등록               30.
    16.                                    31.
    17. 디스크 할당                   32.
    18. 디스크 할당(수동)           33.
    19. 디스크 할당 조회            34.
    20.                                    35.
    21. 도메인 등록                   36.
    22. 도메인 정보 갱신            37.
    23. 도메인 정보 상세조회      38.
    24.                                    39.
    25.                                    40.
    \\. EXIT
    __END__

    print “ 선택 : “;
    $choice  =  <STDIN>;
    chomp $choice;
    print “-”  x 80, “\n”;


    # 입력한 메뉴번호에 따라 해당되는 기능을 수행한다.
    if ($choice eq 11) { add_user(); }
    elsif ($choice eq 12) { delete_user(); }
    elsif ($choice eq 13) { query_user(); }
    elsif ($choice eq 14) { all_user_info(); }
    elsif ($choice eq 15) { pop_add(); }
    elsif ($choice eq 17) { disk_quota(); }
    elsif ($choice eq 18) { disk_quota_edit(); }
    elsif ($choice eq 19) { disk_query(); }
    elsif ($choice eq 21) { add_domain(); }
    elsif ($choice eq 22) { domain_apply(); }
    elsif ($choice eq 23) { domain_display(); }
    elsif ($choice eq 26) { pass_define(); }
    elsif ($choice eq 28) { ps_ax(); }
    elsif ($choice eq 29) { demon(); }
    elsif ($choice eq “\\”) { print “\nbye...\n\n”; exit; }
    else { menu(); }

    }

     

    #=====================================

    # 여기서부터는 시스템에서 공통적으로 사용되는
    # 함수들이다.

     

    sub check {
    # 명령의 실행을 최종적으로 확인하도록 하는 루틴
    print “$prog_msg 확실합니까? [yes/No] “;
    $input  =  <STDIN>;
    chomp $input;
    if ($input  =~  /yes/i) { return; }
    else { print “\n$prog_msg 실행을 취소합니다. [Enter] “;
    $input = <STDIN>;
    menu(); }
    }

     

    sub end {
    # 실행결과를 출력하고 초기화하는 루틴
    local ($end) = @_;
    if ($end eq “”) { $end = “처리가 완료되었습니다.”; }
    print “\n$prog_msg $end [Enter] “;
    $input = <STDIN>;
    exec(“$0”);
    }

     

    sub file_error {
    # 파일 조작시 문제가 발생하면 에러사실을 경고하는 루틴local ($err) = @_;
    if ($err eq “”) { $err = “파일 저장 오류, $FILE”; }
    print “=” x 30 . “ 중대 오류 발생 “ . “=”  x 30 . “\n”;
    print “ 오류 내용 : $err \n”;
    print “=” x 80;
    print “중대한 오류입니다.”;
    print “ 해당 파일을 면밀히 검토하시기 바랍니다.\n”;
    exit;
    }

     

    sub user_check {
    # 넘겨받은 사용자 ID 등의 정보를 이용해 사용자 정보를
    # 얻는 함수
    local ($search_data, $search_field) = @_;
    $FILE = “$pass_file”;
    open(FILE, “$FILE”) || file_error(“파일 읽기 오류, $FILE”);
    while($user_data = <FILE>) {
    chomp $user_data;
    @ud  =  split(“:”, $user_data);
    $search_fld  =  $search_field  - 1;
    $search_fld_data = $ud[$search_fld];
    if ($search_data eq $search_fld_data) {
    return $user_data; }
    }
    close(FILE);
    return 0;
    }

     

    sub group_check {
    # 넘겨받은 그룹 ID 등의 정보를 이용해 그룹 정보를
    # 얻는 함수
    local ($search_data, $search_field) = @_;
    $FILE = “$group_file”;
    open(FILE, “$FILE”) || file_error(“파일 읽기 오류, $FILE”);
    while($group_data  =  <FILE>) {
    chomp $group_data;
    @gd = split(“:”, $group_data);
    $search_fld = $search_field - 1;
    $search_fld_data = $gd[$search_fld];
    if ($search_data eq $search_fld_data) {
    return $group_data; }
    }
    close(FILE);
    return 0;
    }

     

    sub get_date {
    # 시간정보를 얻는 함수
    local ($data) = @_;
    local ($second, $minute, $hour, $date, $month,
    $year, $weekday, $yearday, $Isdst) = localtime($data);
    $month  ++;
    $year  +=  1900;
    $month  <  10 && ($month  = “0$month”);
    $date  <  10 && ($date  = “0$date”);
    $hour  <  10 && ($hour  = “0$hour”);
    $minute  <  10 && ($minute  = “0$minute”);
    $second  <  10 && ($second  = “0$second”);
    return “$year$month$date”;
    }

     

    #=====================================

    # 여기서부터는 메뉴에 나열된 실제 작업을 수행하는
    # 루틴이다.

     

    sub add_user {
    # 11. 계정 등록
    print “$prog_msg 등록할 사용자 ID : “;
    $id  =  <STDIN>;
    chomp $id;
    if ($id eq “”) { add_user(); }
    if ($id eq “\\”) { menu(); }
    if (user_check($id, “1”)) {
    end(“이미 등록되어있는 ID입니다.”); }
    print “$prog_msg 등록할 사용자의 소속그룹”;
    print “(default=$user_basegroup) : “;
    $grp  =  <STDIN>;
    chomp $grp;
    if  ($grp eq “”) { $grp = $user_basegroup; }
    if  ($grp eq “\\”) { menu(); }
    if  (!group_check($grp, “1”)) {
    end(“등록되지 않은 GROUP입니다.”); }
    check();
    # adduser 명령에 -g 옵션을 사용해 그룹을 지정한다.
    system(“/usr/sbin/useradd $id -g $grp”);
    # 웹로그 경로의 소유권을 super user로 바꾼다.
    system(“/bin/chown 0.0 /home/$id/$logdir”);
    end();
    }

     

    sub delete_user {
    # 12. 계정 삭제
    print “$prog_msg 삭제할 사용자 ID : “;
    $id  =  <STDIN>;
    chomp $id;
    if  ($id eq “”) { delete_user(); }
    if  ($id eq “\\”) { menu(); }
    if  (!($user_data = user_check($id, “1”))) {
    end(“등록되지 않은 ID입니다.”); }
    ($ud1, $ud2, $ud3, $ud4, $ud5, $ud6, $ud7)
    =  split(“:”, $user_data);
    if  ($ud3 < $min_uid) {
    end(“해당 ID를 삭제할 수 없습니다.”); }
    check();
    # userdel 명령에 -r 옵션을 이용해 계정을 삭제한다.
    # 자료보호를 위해 삭제할 계정의 홈경로가 /home 경로
    # 하위에 위치하지 않으면 홈경로를 삭제하지 않는다.
    if($ud6 =~ /^\/home/) { $home_del = “-r”; }
    system(“/usr/sbin/userdel $home_del $id”);
    end();
    }

     

    sub query_user {
    # 13. 계정 조회
    # 지정한 사용자에 대해 다음과 같은 결과를 출력한다.
    # [MSG] 조회할 사용자 ID : way
    # [MSG] ID : way (539)
    # [MSG] GROUP : user (500)
    # [MSG] HOME DIR : /home/way
    # [MSG] SHELL : /bin/bash
    print “$prog_msg 조회할 사용자 ID : “;
    $id  =  <STDIN>;
    chomp $id;
    if  ($id eq “”) { query_user(); }
    if  ($id eq “\\”) { menu(); }
    if  (!($user_data = user_check($id, “1”))) {
    end(“등록되지 않은 ID입니다.”); }
    ($ud1, $ud2, $ud3, $ud4, $ud5, $ud6, $ud7)
    =  split(“:”, $user_data);
    $s_group  =  $ud4;
    if  (!($group_data = group_check($s_group, “3”))) {
    end(“등록되지 않은 GROUP입니다. (GID=$s_group)”); }
    ($gd1, $gd2, $gd3, $gd4) = split(“:”, $group_data);
    print “$prog_msg ID : $id ($ud3)\n”;
    print “$prog_msg GROUP : $gd1 ($s_group)\n”;
    p

    rint “$prog_msg HOME DIR : $ud6\n”;
    print “$prog_msg SHELL : $ud7\n\n”;
    end(“조회가 완료되었습니다.”);
    }

     

    sub all_user_info {
    # 14. 전체 계정 조회
    # 비밀번호 파일을 출력한다.
    $FILE = “$pass_file”;
    open(FILE, “$FILE”) || file_error(“파일 읽기 오류, $FILE”);
    while($user_data  =  <FILE>) {
    chomp $user_data;
    @ud = split(“:”, $user_data);
    $search_fld_data = $ud[2];
    if ($search_fld_data > =  $min_uid ) {
    $print_data .= “$user_data\n”; }
    }
    close(FILE);
    print $print_data;
    end(“조회가 완료되었습니다.”);
    }

     

    sub pop_add {
    # 15. POP 계정 등록
    print “$prog_msg 등록할 POP ID : “;
    $id  =  <STDIN>;
    chomp $id;
    if  ($id eq “”) { pop_add(); }
    if  ($id eq “\\”) { menu(); }
    if  (user_check($id, “1”)) {
    end(“이미 등록되어있는 ID입니다.”); }
    print “$prog_msg 등록할 사용자의 소속그룹”;
    print “(default=$pop_basegroup) : “;
    $grp = <STDIN>;
    chomp $grp;
    if  ($grp eq “”) { $grp = $pop_basegroup; }
    if  ($grp eq “\\”) { menu(); }
    if  (!group_check($grp, “1”)) {
    end(“등록되지 않은 GROUP입니다.”); }
    check();


    # POP 계정은 noshell이라는 간단한 쉘스크립트를
    # 기본 shell로 지정한다.

     

    File Name : /usr/local/bin/noshell
    #!/bin/sh


    echo
    echo “ ============================= “
    echo “ 현재 접속하신 ID는”
    echo “ TELNET, FTP 접속이 허용되지”
    echo “ 않습니다.”
    echo “ ============================= “
    echo

    system(“/usr/sbin/useradd $id -g $grp -M  \\
    -s /usr/local/bin/noshell -d /”);
    end();
    }

     

    sub disk_quota {
    # 17. 디스크 할당
    # quota 기능을 이용해 사용자의 디스크를 할당한다.
    print “$prog_msg 디스크를 할당할 사용자 ID : “;
    $id  =  <STDIN>;
    chomp $id;
    if  ($id eq “”) { disk_quota(); }
    if  ($id eq “\\”) { menu(); }
    if  (!user_check($id, “1”)) {
    end(“등록되지 않은 ID입니다.”); }
    print “$prog_msg 표본 사용자 ID(default:$disk_baseuser) : “;
    $id_2  =  <STDIN>;
    chomp $id_2;
    if  ($id_2 eq “”) { $id_2 = $disk_baseuser; }
    if  ($id_2 eq “\\”) { menu(); }
    if  (!user_check($id_2, “1”)) {
    end(“등록되지 않은 ID입니다.”); }
    check();
    # edquota 명령에 -p 옵션을 이용해 새로 지정할
    # 사용자에게 표본 사용자의 디스크 할당정보를
    # 복사한다.(man edquota)
    system(“/usr/sbin/edquota -p $id_2 $id”);
    end();
    }

     

    sub disk_quota_edit {
    # 18. 디스크 할당(수동)
    print “$prog_msg 디스크를 할당할 사용자 ID : “;
    $id  =  <STDIN>;
    chomp $id;
    if  ($id eq “”) { disk_quota_edit(); }
    if  ($id eq “\\”) { menu(); }
    if  (!user_check($id, “1”)) {
    end(“등록되지 않은 ID입니다.”); }
    check();
    system(“/usr/sbin/edquota -u $id”);
    end();
    }

     

    sub disk_query {
    # 19. 디스크 할당 조회
    print “$prog_msg 디스크 사용상황을 조회할”;
    print “ 사용자 ID(default:all) : “;
    $id  =  <STDIN>;
    chomp $id;
    if  ($id eq “”) { $id = “ALL”; }
    if  ($id eq “\\”) { menu(); }
    if  ( !user_check($id, “1”) && ($id ne “ALL”) ) {
    end(“등록되지 않은 ID입니다.”); }
    # 전체 사용자의 디스크 정보는 repquota 명령
    # (man repquota)을 이용하고, 개별 사용자는 quota 명령
    # (man quota)을 이용한다.
    if($id eq “ALL”) { system(“/usr/sbin/repquota -avug”); }
    else { system(“/usr/bin/quota -u $id”); }
    end(“조회가 완료되었습니다.”);
    }

     

    sub add_domain {
    # 21. 도메인 등록
    print “$prog_msg 도메인을 등록할 사용자 ID : “;
    $id  =  <STDIN>;
    chomp $id;
    if  ($id eq “”) { add_domain(); }
    if  ($id eq “\\”) { menu(); }
    if  (!user_check($id, “1”)) {
    end(“등록되지 않은 ID입니다.”); }
    print “$prog_msg 등록할 도메인(ex. abcd.co.kr) : “;
    $domain  =  <STDIN>;
    chomp $domain;
    if  ($domain eq “”) { add_domain(); }
    if  ($domain eq “\\”) { menu(); }
    $domain  =~  tr/[A-Z]/[a-z]/;
    $domain  =~  s/^ +| + _$//g;
    # 중복을 막기 위해 네임서버 zone 파일이
    # 이미 존재하는지 점검한다.
    if(-e “/var/named/custom/$domain”) {
    print “\n$prog_msg ERROR, 동일한 도메인이”;
    print “ 이미 등록되어 있습니다. [Enter] “;
    $input  =  <STDIN>;
    menu(); }
    check();

    # bind 8.x대에서 사용하는 /etc/named.conf에
    # 추가할 정보를 생성한다.
    $named_conf  =  <<”__END__”;

    zone “$domain” {
    type master;
    file “custom/$domain”;
    };

    __END__

    # zone 파일(/var/named/custom/도메인명)에
    # 기록할 정보를 생성한다.
    # serial 부분의 $date_seri 값은 일자정보에
    # 번호(01)를 조합한 값(1999123101)을 사용한다.
    $named_zone  =  <<”__END__”;
            @       IN      SOA     ns.way.co.kr domain.way.co.kr.
            (
                                    $date_seri   ; serial
                                    8H ; refresh
                                    2H ; retry
                                    1W ; expiry
                                    1D ) ; minimum

                    IN      NS      ns.way.co.kr.
                    IN      NS       dns.way.co.kr.
                    IN      A       $ip_address
                    IN      MX      10 mail.way.co.kr.
        www     IN      A       $ip_address
                    IN      MX      10 mail.way.co.kr.
    __END__

    # apache 설정파일(httpd.conf)의 VirtualHost
    # 정보를 생성한다.
    # httpd.conf 파일에 미리 NameVirtualHost
    # 지시자를 선언해 두어야 한다.
    # 추가할 모든 도메인이 동일한 IP를 사용하는
    # Name-based Virtual Host 임을 전제로 한다.
    $httpd_conf  =  <<”__END__”;
    <VirtualHost $ip_address>
    ServerAdmin webmaster\@$domain
    DocumentRoot /home/$id/$webdir
    ScriptAlias /cgi-bin/ /home/$id/$webdir/cgi-bin/
    ServerName www.$domain
    ServerAlias $domain www.$domain
    TransferLog /home/$id/$logdir/today.log
    ErrorLog /home/$id/$logdir/error.log
    </VirtualHost>

    __END__

    # 생성한 named.conf 정보를 파일에 기록한다.
    $FILE = “/etc/named.conf”;
    open(FILE, “>>$FILE”) || file_error();
    print FILE $named_conf;
    close(FILE);

    # 생성한 named zone 파일 정보를 기록한다.
    $FILE = “/var/named/custom/$domain”;
    open(FILE, “>$FILE”) || file_error();
    print FILE $named_zone;
    close(FILE);

    # 추가한 도메인으로 도착한 메일을 수신할 수 있도록
    # 설정한다.
    $FILE = “/etc/sendmail.cw”;
    open(FILE, “>>$FILE”) || file_error();
    print FILE “$domain\n”;
    close(FILE);

    # 도메인별로 가상 사용자 mail을 사용할 수
    # 있도록 virtusertable을 설정(추가)한다.
    # @way.co.kr way라고 지정하면 way.co.kr 도메인으로
    # 수신되는 모든 메일은 way 계정으로 배달된다.
    # sendmail.cf 파일에 virtusertable을 사용할 수 있는
    # 설정이 되어있어야 한다.
    $FILE = “/etc/mail/virtusertable”;
    open(FILE, “>>$FILE”) || file_error();
    print FILE “\@$domain $id\n”;
    close(FILE);

    # 추가한 도메인의 VirtualHost정보를 httpd.conf
    # 파일에 기록(추가)한다.
    # apache를 RedHat rpm으로 설치한 경우에는
    # /etc/httpd/conf/httpd.conf 파일에 반영해야한다.
    $FILE = “/usr/local/apache/conf/httpd.conf”;
    open(FILE, “>>$FILE”) || file_error();
    print FILE “$httpd_conf”;
    close(FILE);
    end();
    }

     

    sub domain_apply {
    # 22. 도메인 정보 갱신
    # “도메인 등록”후 등록내용을 반영하기 위해 실행한다.
    # apache를 RedHat rpm으로 설치한 경우에는
    # /etc/rc.d/init.d/httpd 파일을 이용한다.
    # restart를 이용할 수도 있지만 연속되는 실행에 약간의
    # 여유를 두기 위해 1초 간격으로 stop과 start를 실행한다.

    check();
    system(“/usr/local/apache/bin/apachectl stop”);
    sleep 1;
    system(“/usr/local/apache/bin/apachectl start”);
    sleep 1;
    system(“/etc/rc.d/init.d/named stop”);
    sleep 1;
    system(“/etc/rc.d/init.d/named start”);
    sleep 1;
    system(“/etc/rc.d/init.d/sendmail stop”);
    sleep 1;
    system(“/etc/rc.d/init.d/sendmail start”);
    sleep 1;
    end();
    }

     

    sub domain_display {
    # 23. 도메인 정보 상세조회
    # 도메인의 등록내용(각 설정파일)을 조회한다.
    # vi editor를 이용해 설정파일들을 조회하고
    # 편집할 수 있다.
    print “$prog_msg 조회할 도메인(ex. abcd.co.kr) : “;
    $domain  =  <STDIN>;
    chomp $domain;
    if  ($domain eq “”) { domain_display(); }
    if  ($domain eq “\\”) { menu(); }
    $domain_file_list = “/etc/named.conf “;
    $domain_file_list .= “/var/named/custom/$domain “;
    $domain_file_list .= “/etc/sendmail.cw “;
    $domain_file_list .= “/etc/mail/virtusertable “;
    $domain_file_list .= “/usr/local/apache/conf/httpd.conf “;
    check();
    system(“vi $domain_file_list”);
    end();
    }

     

    sub pass_define {
    # 26. 사용자 password 지정
    print “$prog_msg password를 지정(변경)할 사용자 ID : “;
    $id  =  <STDIN>;
    chomp $id;
    if  ($id eq “”) { pass_define(); }
    if  ($id eq “\\”) { menu(); }
    if  (!($user_data = user_check($id, “1”))) {
    end(“등록되지 않은 ID입니다.”); }
    ($ud1, $ud2, $ud3, $ud4, $ud5, $ud6, $ud7)
    =  split(“:”, $user_data);
    if  ($ud3 < $min_uid) {
    end(“해당 ID의 password를 지정(변경)할 수 없습니다.”); }
    check();
    system(“/usr/bin/passwd $id”);
    end();
    }

    sub ps_ax {
    # 28. 프로세서 조회
    system(“/bin/ps ax”);
    end();
    }

     

    sub demon {
    # 29. 데몬 관리
    # “도메인 정보 갱신”과 “프로세서 조회”를 통해 데몬의
    # 수동 조작(stop, start, restart)이 필요한 경우 사용한다.
    print “$prog_msg {httpd,named,sendmail}”;
    print “ {start,stop,restart}\n”;
    print “$prog_msg # “;
    $cmd  =  <STDIN>;
    chomp $cmd;
    if  ($cmd eq “”) { demon(); }
    if  ($cmd eq “\\”) { menu(); }
    ($cmd, $act) = $cmd =~ /^(.*) (.*)$/;
    if($cmd  =~  /^httpd/) {
    $cmd = “/usr/local/apache/bin/apachectl”; }
    elsif($cmd  =~  /^named/) {
    $cmd = “/etc/rc.d/init.d/named”; }
    elsif($cmd  =~  /^sendmail/) {
    $cmd = “/etc/rc.d/init.d/sendmail”; }
    # 임의작동을 막기 위해 httpd, named, sendmail 이외의
    # 데몬은 조작할 수 없도록 한다.
    else { end(“허용되지 않는 명령입니다.”); }
    system(“$cmd $act”);
    demon();
    end();
    }

     


 

출처: http://www.linuxlab.co.kr

       임대호( lawwal@way.co.kr / Way-CGI 시리즈 개발자)

 

 

 

 

 

'OS > LINUX' 카테고리의 다른 글

/dev/null 다시 만들기  (0) 2011.06.04
/dev/zero와 /dev/null  (0) 2011.06.04
프로세스 - fork & exec  (0) 2011.05.14
쉘 스크립트 #6  (0) 2011.05.14
쉘스크립트 ex5  (0) 2011.05.14

+ Recent posts