BEGIN, END, BEGINFILE, ENDFILE

AWK 에서는 입력 스트림을 두 가지로 구분해 볼 수 있습니다. 하나는 기본적으로 알고 있는 awk 명령 사이클에 의해 처리되는 메인 입력 스트림으로 next, nextfile, 단독으로 쓰이는 getline, getline var 형태는 모두 메인 스트림에 적용되는 구문들입니다. 이때는 입력 파일이 변경됨에 따라서 FILENAME 변수값이 설정되고 BEGINFILE, ENDFILE 블록이 실행됩니다.

다른 하나는 파이프나 redirection 기호를 이용하여 직접 getline 으로 읽어 들이는 경우입니다. 이 경우는 메인 입력 스트림과 관계없이 단독으로 읽어 들이는 것입니다. 레코드를 읽어 들임으로써 $0$1, $2, $3 ... NF, RT 변수값들은 설정되지만 NR, FNR 변수들은 설정되지 않고 또한 파일을 읽어 들인다고 해서 FILENAME 변수값이 설정되거나 BEGINFILE, ENDFILE 블록이 실행되지는 않습니다.

이 특수 블록들은 스트립트 내에서 하나만 사용할 수 있는 것이 아닙니다. 여러 개를 중복해서 사용할 수 있습니다. 이때는 같은 블록일 경우 위에서부터 아래로 블록이 위치한 순서에 따라 실행됩니다.

$ awk '
BEGIN { print "BEGIN " FILENAME } 
BEGINFILE { print "BEGINFILE " FILENAME } 
{ print $0 " .............FNR: " FNR ", NR: " NR }
ENDFILE { print "ENDFILE " FILENAME } 
END { print "END " FILENAME }
' file1 file2 file3

BEGIN            # BEGIN 블록에서는 FILENAME 변수값이 설정되지 않습니다.
BEGINFILE file1
111 .............FNR: 1, NR: 1      # 111, 222, 333 으로 표시되는 것은 파일 내용입니다.
222 .............FNR: 2, NR: 2
333 .............FNR: 3, NR: 3
ENDFILE file1
BEGINFILE file2
444 .............FNR: 1, NR: 4
555 .............FNR: 2, NR: 5
666 .............FNR: 3, NR: 6
ENDFILE file2
BEGINFILE file3
777 .............FNR: 1, NR: 7
888 .............FNR: 2, NR: 8
999 .............FNR: 3, NR: 9
ENDFILE file3
END file3

BEGIN { . . . }

  • 명령 사이클이 시작되기 전이지만 파이프나 redirection 을 이용하는 getline 을 사용할 수 있고 이때 $0$1, $2 ..., NF, RT 변수값들을 사용할 수 있습니다.

  • getline, getline var 형태를 이용하여 메인 입력 스트림으로부터 레코드 읽기를 시작할 수 있고 이때 FILENAME 변수도 설정됩니다.

  • 하지만 next, nextfile 는 BEGIN 블록에서 사용할 수 없습니다.

BEGINFILE { . . . }

  • 해당 파일에서 처음 레코드를 읽어들이기 전에 실행됩니다.

  • FILENAME 변수값이 설정되므로 사용할 수 있습니다.

  • FNR 변수값이 0 으로 reset 됩니다.

  • 레코드를 읽어들이기 전이라 next 는 블록 내에서 사용할 수 없습니다.

  • 하지만 nextfile 은 사용할 수 있습니다.

  • 파이프나 redirection 을 이용하는 getline 은 사용할 수 있지만 getline, getline var 형태를 이용해 메인 입력 스트림으로부터 읽기를 할 수는 없습니다.

  • ERRNO 변수가 clear 됩니다.

사용자 관점에서 BEGINFILE 블록이 필요한 이유는 BEGIN 블록에서는 파일을 읽어들이기 전이라 FILENAME 변수를 사용할 수 없다는 점입니다.

# BEGIN 블록에서는 FILENAME 변수를 사용할 수 없습니다.
$ awk 'BEGIN { 
    while ((getline < FILENAME) > 0) 
        print $1, $3, $5 > FILENAME ".new" 
}' file1 file2
awk: cmd. line:1: fatal: expression for '<' redirection has null string value

# 따라서 다음과 같이 BEGINFILE 블록을 이용해야 합니다.
$ awk 'BEGINFILE { 
    while ((getline < FILENAME) > 0) 
        print $1, $3, $5 > FILENAME ".new" 
}' file1 file2
$ ls -1
file1
file1.new
file2
file2.new

또 한 가지는 파일을 읽어 들일 때 오류가 발생할 경우 fatal error 로 바로 awk 명령이 종료됩니다. 하지만 BEGINFILE 블록을 이용하면 ERRNO 변수가 설정되어 에러 핸들링을 할 수가 있습니다. ERRNO 변수는 한번 설정되면 자동으로 clear 되지 않지만 인수로 사용된 입력 파일명이 바뀔 때는 clear 되므로 BEGINFILE 블록에서는 if ( ERRNO != "" ) 로 오류를 체크할 수 있습니다.

# foobar 파일을 읽기 실패하여 바로 awk 명령이 종료된다.
$ awk '{ print $1 }' file1 foobar file2
111
222
333
awk: cmd. line:1: fatal: cannot open file 'foobar' for reading (Permission denied)
.......................................................................................

# BEGINFILE 블록을 이용하면 에러 처리를 하고 다음 파일로 진행할 수가 있습니다.
$ awk '
BEGINFILE { 
    if (ERRNO) { 
        printf "Error: %s (%s)\n", FILENAME, ERRNO > "/dev/stderr"
        nextfile
    } 
} 
{ print $1 }
' file1 foobar file2
111
222
333
Error: foobar (Permission denied)
444
555
666

ENDFILE { . . . }

  • 해당 파일에서 마지막 레코드 처리가 끝나면 실행됩니다.

  • 외부에서 nextfile 이 실행될 때에도 실행됩니다.

  • 파일이 empty 파일인 경우에도 실행됩니다.

  • 해당 파일에서 레코드 처리가 모두 끝난 상태이므로 next 는 사용할 수 없습니다.

  • nextfile 도 블록 내에서는 사용할 수 없습니다.

  • 파이프나 redirection 을 이용하는 getline 은 사용할 수 있지만 getline, getline var 형태를 이용해 메인 입력 스트림으로부터 읽기를 할 수는 없습니다.

  • exit 문에 의해 종료될 때는 실행되지 않습니다.

END { . . . }

  • 모든 레코드 처리가 끝나고 프로그램이 종료될 때 마지막으로 실행됩니다.

  • exit 문에 의해 프로그램이 종료될 때도 실행됩니다.

  • 파이프나 redirection 을 이용하는 getline 을 사용할 수 있고 getline, getline var 형태를 이용해 메인 입력 스트림으로부터 레코드를 읽을 수 있습니다.

메인 입력 스트림에서 데이터를 읽어들이지 않고 BEGIN 블록에서 처리할 경우 다음 첫 번째와 같이 작성하게 되면 BEGIN 블록 종료시 stdin 으로부터 입력 대기 상태가 되어 END 블록이 실행되지 않습니다. 이때는 입력 파일로 /dev/null 을 지정하면 문제를 해결할 수 있습니다.

# BEGIN 블록 실행이 완료되었으나 종료되지 않고 stdin 으로부터 입력 대기 상태가 된다.
$ awk 'BEGIN { print "begin..." } END { print "end..." }'
begin...
^C

# /dev/null 을 입력 파일로 사용하면 END 블록이 실행되고 정상 종료한다.
$ awk 'BEGIN { print "begin..." } END { print "end..." }' /dev/null
begin...
end...

BEGIN, BEGINFILE, { } 은 global 영역에 해당됩니다.

따라서 foo 함수에서 변수 AA, BB, EE 에 접근할 수 있습니다.

$ echo | awk '
BEGIN{ AA = 100 } 
BEGINFILE { BB = 200 } 
ENDFILE { CC = 300 } 
END { DD = 400 } 

{ EE = 500; foo() } 

function foo() { 
    print "BEGIN : " AA
    print "BEGINFILE : " BB
    print "ENDFILE : " CC
    print "END : " DD
    print "{ } : " EE
}' 

#####  실행 결과  #####

BEGIN : 100
BEGINFILE : 200
ENDFILE : 
END : 
{ } : 500

예제 )

현재 디렉토리에 있는 *.awk 파일 total size 구하기

$ awk -f - <<\EOF *.awk
BEGINFILE { 
    cmd = "stat -c %s '" FILENAME "'" 
    if (cmd | getline var) { 
        printf "%20s %13'd\n", FILENAME, var
        total += var
    }
    close(cmd); nextfile
} END { 
    print " ----------------------------------" 
    printf "%20s %13'd\n", "total size", total
}
EOF
           debug.awk           320
          getopt.awk         2,133
             inc.awk            64
          primes.awk           142
       transpose.awk           213
       webserver.awk         1,732
 ----------------------------------
          total size         4,604

Quiz

다음 sample 데이터의 세 번째 컬럼 값을 기준으로 중복되는 라인들만 출력하는 것입니다. 그러니까 xtp3xtp5 가 포함되는 라인은 uniq 한 라인이기 때문에 출력이 되면 안 됩니다.

$ cat sample
111:aa:xtp1:ada
111:aet:xtp1:papa
333:wc:xtp3:der          # uniq 라인
111:ab:xtp2:adad
111:ac:xtp4:lk
333:dc:xtp5:zsw          # uniq 라인
222:cc:xtp4:eq
222:ap:xtp2:lstp
  1. case 0 : 일 경우는 처음 라인이 되므로 출력하지 않고 first 변수에 저장해 놓습니다.

  2. case 1 : 부터는 중복 라인이 되므로 값을 출력하는데 먼저 기존에 저장해둔 print first[$3] 라인을 출력하고 출력 후에는 필요가 없으므로 delete first[$3] 합니다. 그리고 break 을 제거하여 다음 default : 에서 현재 라인이 출력되도록 합니다.

  3. default : 부터는 case 2 이후가 되므로 바로 현재 라인을 출력합니다.

$ awk -F: '{
    switch (count[$3]++) {
        case 0:  first[$3] = $0; break;
        case 1:  print first[$3]; delete first[$3];
        default: print $0
    }
}' sample

111:aa:xtp1:ada
111:aet:xtp1:papa
111:ac:xtp4:lk
222:cc:xtp4:eq
111:ab:xtp2:adad
222:ap:xtp2:lstp

2 .

위의 첫 번째 문제에서 기존의 데이터 순서를 유지하면서 출력하려면 어떻게 할까요?

다음과 같이 BEGINFILE 블록을 활용하면 2 pass 로 처리할 수 있습니다. 먼저 BEGINFILE 블록에서 전체 파일을 읽어서 count 변수에 카운팅을 하고 ( 1 pass ), 다음에 메인스트림에서 데이터를 차례로 읽어들일 때 카운트 값을 활용합니다 ( 2 pass ).

$ awk -F: '
BEGINFILE {
    while (getline < FILENAME > 0)           # 1 pass
        count[$3]++;
}
{ if (count[$3] > 1) print }                 # 2 pass
' sample

111:aa:xtp1:ada
111:aet:xtp1:papa
111:ab:xtp2:adad
111:ac:xtp4:lk
222:cc:xtp4:eq
222:ap:xtp2:lstp