반응형
 
AWK와 SED는 비슷한 또래의 사촌이다. 유닉스 초창기에 개발됐고 훌륭한 기능을 제공하므로 1979년 이후 다양한 유닉스 변종들과 유닉스-like 운영체제에 꼭 포함돼 많은 사랑을 받아왔다. 둘이 맡은 역할은 텍스트 프로세싱이다. 유닉스 프로그램들은 데이터를 일반 텍스트 파일로 저장하는 경우가 많기 때문에 유닉스 환경에서 AWK와 SED를 활용함으로써 처리할 수 있는 작업은 종류를 셀 수 없을 것이다. 게다가 파이프라인이 가능하므로 표준 출력을 AWK와 SED의 표준 입력으로 받아 처리 할 수 있으므로 텍스트가 들어가는 모든 작업에 사용될 수 있다해도 과언은 아니다. 이 둘은 헤아릴 수 없이 많이 카피돼 지난 한 세대 동안 메인프레임이나 대형 서버에서 프로세스로서 숨쉬어왔다. 하지만 80년대 중반에 더 강력한 기능을 제공하는 펄이 등장했고 그후로 AWK와 SED의 인기는 점점 사그라들었다. 게다가 AWK와 SED의 스크립트를 작성하기도 해석하기도 어렵기 때문에 조금이라도 복잡한 스크립트를 프로그래밍할 필요가 생기면 펄이나 파이썬을 추천하는 추세다. 

그러나 더 강력한 기능과 더 나은 개발 용이성을 제공하는 언어의 등장에도 불구하고 AWK와 SED는
30년 넘게 살아남았다. 대체 그 이유가 뭘까? 유닉스 초창기에 대한 향수 때문에 올드 프로그래머들이 사용하는 걸까? 아니면 복잡한 프로그래밍을 즐기는 소수 마니아들이 끈질기게 그 둘을 놓지 않기 때문일까? 아마 직접 써보면 알게 될 것이다.


이 문서의 목적
AWK와 SED에 처음 발을 들여놓는 사람들을 위한 가이드라인이다. AWK나 SED의 모든 기능을 다루지는 않았고, 문서 곳곳에서 '그밖의 내용은 맨페이지를 참고하라'는 식의 말이 나온다. AWK와 SED에 필수적인 내용을 조감하기 위함이다.

현재 문서 버전

2009년 11월 24일 버전 <- 현재
2009년 11월 21일 버전

AWK와 SED는 사용하기 어렵다?
많은 이들이 AWK와 SED로 작성된 스크립트를 보고 해석하기 난해해 하는 이유는 많은 사용자들이 AWK와 SED가 프로그래밍 언어인지 모르기 때문이라 생각한다. 사용방법이 비교적 간단해서 금방 배울 수 있는 유틸리티와 혼동하여 "AWK나 SED도 유틸리티인데 이걸 공부할 필요까진 없다" 라고 착각하기 때문에, 유틸리티 치고는 AWK와 SED 는 어렵다고 손사래 치는 것이다. 그러나 AWK나 SED도 엄연히 프로그래밍 언어고 어느 정도의 지식이 있어야 사용할 수 있다. 하지만 언어치고는 습득에 걸리는 시간이 길지 않고, 이 짧은 문서에 많은 내용을 담을 수 있을만큼 알아야할 것이 많지 않기 때문에 어려울 거란 염려는 넣어두시라. ;)

작성자
정준영; 이 문서는 2009년 11월, 한주마다 열리는 HLUG 내부 세미나를 위한 발표자료를 토대로 작성하였다.

목차
I. AWK & SED 역사적 배경 (이 포스트에서 설명)
II. 정규표현식 소개 (이 포스트에서 설명)
III. AWK (이 포스트에서 설명)
IV. AWK Examples(이 포스트에서 설명)
V. SED (다음 포스트에서 설명)
VI. SED Examples(다음 포스트에서 설명)


I. AWK와 SED 배경

AWK 역사적 배경

1977년 Bell Laboratories의 Alfred Aho, Peter Weinberger 그리고 Brian Kernighan이 처음 개발했다. 이 셋의 이름을 따 AWK라 부르고 auk[ɔ́ːk]로 발음한다. 옆에 있는 사진은 바다쇠오리라는 새인데 영어로 auk라 부른다. AWK와 발음이 같기 때문에 AWK 책에 표지모델로 쓰기이기도 했다.
1979년 Version 7 Unix에 처음 배포되었다. V7 Unix는 역사적으로 의미가 있는 유닉스다. 유닉스는 벨연구소에서 PDP-7에 MULTICS를 이식하면서 개발이 시작됐고 1970년 PDP-11에 포팅되면서 UNIX의 첫번째 판이 나왔다. 1973년 Versioin 4 Unix는 C언어로 재작성됐고 1974년 대학에 널리 알려지게 되면서 대학과 벨연구소가 함께 개발하기 시작했다. 그리고 1979년에 V7이 나왔고 여기에 최초로 C 컴파일러와 본셸이 들어갔다. 물론 AWK와 SED도 이 버전에 최초로 포함됐고 make도 이때 포함됐다. 역사적으로 의미가 있다는 이유는, V7이 research unix로서 널리 알려진 유닉스 중 마지막 버전이기 때문이다. 즉, 1980년대  들어서 유닉스가 상업적으로 사용되기 시작했는데, 그 전에 나온 마지막 유산이라는 뜻이다. 물론 V8, V9, V10까지 나오긴 했었지만 V7만큼 알려지진 않았다. 게다가 많은 유닉스 변종들은 V7을 기반으로 했고 그 중 BSD와 SystemV는 또 그 후에 모든 상업적 유닉스의 기반이 됐기 때문에 역사적으로 V7이 중요한 의미를 가진다고 말할 수 있다.


SED 역사적 배경

SED는 Stream EDitor에서 따온 이름이다. 읽을 때는 세드라고 발음한다.  1973년 벨연구소의 Lee E. McMahon이 개발했다. 추측컨대 그 당시의 벨연구소는 지금의 구글보다 더 높은 위상을 가졌을 것 같다. 벨연구소는 전화장비, 물리분야, 네트워크, 소프트웨어 등 첨단 기술의 리더 역할을 했다. 그 연구소에서 완료한 연구 업적으로 7번의 노벨상이 수여됐다고 한다. 하지만 1996년 벨연구소의 전화장비와 물리연구분야가 Lucent Technologies로 독립했고 소수만 남아서 AT&T Bell Laboratories를 이어갔다. 그후 AT&T Laboratories로 존재하다가 2005년에 AT&T Corp.가 통째로 SBC Communications에 인수됐고 SBC Communications는 자신의 이름을 AT&T Inc.로 바꿨다. 그리고 AT&T Corp.의 일부였던 AT&T Laboratories는 SBC Communications의 R&D  부서에 흡수됐고 그 부서 역시 AT&T Labs. Inc 로 자신의 이름을 바꿨다. 예전에 정점을 달리던 AT&T는 사실상 SBC Communications에 흡수되어 사라졌다. 단지 AT&T 이름이 상징적이기 때문에 SBC Communications가 그 이름을 계속 사용하는 것이다.
SED는 AWK와 마찬가지로 1979년 Version 7 Unix에 처음 배포됐다. 그러나 80년대 중반에 perl 등장했고 널리 알려지기 시작하면서 AWK와 SED의 사용 빈도는 급격히 줄었다. 왜냐하면 조금만 길어져도 스크립트가 너무 복잡해지기 때문이다. 그렇기 때문에 복잡한 프로그래밍으로는 거의 사용되지 않고 관용적인 one-liner로써 명맥을 이어가고 있는 실정이다. sed는 ed로 부터 비롯됐고 perl에 영향을 주었다.
*one-liner 라는 단어는 명령행에 한줄로 쓰여져 동작하는 프로그램을 뜻한다.



II. 정규표현식

AWK와 SED는 정규표현식이 적용되지 않아도 작동하지만, 정규표현식을 사용하면 AWK와 SED로 할 수 있는 일이 훨씬 많아진다.

정규표현식이란 텍스트 안에 있는 복잡한 패턴을 표현하는 방법이다. 정규표현식은 정규식으로 줄여서 쓸 수 있고 regular expression은 regex 또는 regexp로 줄여서 쓸 수 있다. 발음하기는 regex가 편하고 레긱스 내지는 레직스라고 읽을 수 있다. 대체 regexp는 어떻게 발음하는지 모르겠다. 아시는 분은 제보 부탁드린다 ;^)

정규표현식은 automata theory와 formal language theory 연구에서 시작됐다. 그리고 최초로 정규표현식을 적용시킨 유틸리티는 Ken Thompson의 QED이다. 또한 나중에 Ken Thompson은 ed에도 정규표현식 기능을 넣었고 이로 인해 정규표현식이 널리 알려지게 됐다. 현재 정규식을 지원하는 유틸리티 중에 가장 유명한 것은 grep일 것이다. grep은 ed에 직접적으로 영향을 받았다. ed 명령어 g/re/p에서 그 이름을 따올 정도였으니까 말이다. 현재 다양한 에디터, 프로그래밍 언어 (특히 스크립팅 언어) 그리고 많은 텍스트 프로세싱 유틸리티들에서 정규표현식이 지원된다.

이 문서에서는 널리 쓰이는 AWK와 SED의 관용구들을 이해할만한 선에서 정규표현식의 일부를 소개한다. 그 선이라는 것은 내 경험을 바탕으로 정한 것이므로 절대 '정규표현식은 이게 전부다'라고 믿지는 말라 :P

누구나 워드 프로세서의 '찾기' 기능을 써본 적 있을 것이다. 정규표현식이 없는 '찾기'는 사용자가 입력한 스트링에 문자 그대로 정확히 매칭되는 단어 또는 문장만 찾을 수 있다. 그런데 정규표현식을 사용하면 '찾기' 기능에 "전체 텍스트 중에서 이러저렇게 생긴 부분을 찾아줘"라고 말할 수 있다. 다시 말하자면, "이 패턴을 가진 부분을 찾아라"라고 명령할 수 있다는 얘기다.

Characters and Character class

abc : abc 캐릭터 그대로
\t   : tab 캐릭터
\n  : newline 캐릭터
[abc] : a or b or c 대괄호는 정규식에서 특수한 의미를 가진다. 대괄호는 캐릭터 클래스를 형성한다. 문자 그대로의 대괄호를 매칭시키고 싶으면 백슬래시를 앞에 두어야 한다. \[ \]
[a-z] : 소문자 a~z 캐릭터 클래스는 범위로 지정할 수도 있다.
[A-Z] : 대문자 A~Z
[0-9] :  숫자 0~9
[a-zA-Z0-9] 다중 범위를 지정할 수도 있다.
[^abc] : abc 제외(반드시 ^이 맨 처음에 나와야함) 캐릭터 클래스에 맨 첫 글자가 ^면 ^이 특별한 의미를 가진다. 캐릭터클래스 안에 있는 캐릭터를 제외한 아무 캐릭터를 뜻한다. 만약 캐릭터 클래스의 맨처음이 아니라 중간에 ^이 들어가면 문자그대로의 ^을 뜻한다.
 .      : 아무 character. 점하나가 아무 캐릭터 하나를 뜻한다. 빈칸도 되고 알파벳, 숫자, 특수기호 등도 가리킨다.


경계(boundary)


^  : 행의 맨 처음
$  : 행의 맨 끝, 그렇기 때문에 ^$은 아무캐릭터가 없는 행 즉, 빈행을 의미한다
\w : 단어. 영숫자를 의미함.
\W:  단어가 아닌 캐릭터. 즉, ~`!@#$%^&*()-+=|\{}[];':"?/<>,. 그리고 공백문자를 뜻한다.
\b: 단어 경계(이질성)
\B: 비 단어 경계(동질성)


이해를 돕기 위해 \b, \B, \w, \W의 예를 들어보겠다.
abcdef:!~ghi-jkl에 대해
 패턴 매칭
(\B\w)* abcdef:!~ghi-jkl
 (\b\w)* abcdef:!~ghi-jkl
 (\b\W)* abcdef:!~ghi-jkl
 (\B.\B)* abcdef:!~ghi-jkl
 (\b.\b)* abcdef:!~ghi-jkl

즉 \B는 자기와 같은 그룹(단어냐 비단어냐)에 속한 녀석에 둘러쳐졌는지 보는 것이고 \b는 자기와 다른 그룹에 속한 녀석에 둘러쳐졌는지 본다.


횟수(quantities)


* : 0번 이상 나옴, 예) a*b  : bbb 매칭
+ : 1번 이상 나옴, 예) a+b  : bbb 매칭 안됨
?  : 0,1번
{n,m} n~m번
{,m} ~m번
{n,} n번이상
{x} x번


캡처링 그룹과 백레퍼런스(capturing groups and back references)

()  패턴을 그룹으로 묶음, 그룹화의 가장 큰 목적은 나중에 특정 그룹을 가리키기 위해서이다.
\n n번째 그룹 가리키는 백레퍼런스
백레퍼런스 지정 순서: 왼쪽 괄호가 등장한 순서대로 해석한다.
((A)(B(C)))  
               \1 : ((A)(B(C)))
   \2: (A) 
   \3: (B(C))
               \4: (C)


백레퍼런스의 키포인트: 백레퍼런스로 가리키는 것은 매칭된 결과 값임을 주의! 패턴을 가리키는 것이 아님!

여기저기서 갖고 온 것들 :P

\  : 인용부호; 특수 캐릭터의 해석을 막음
       예) \^ 행의 처음이라는 의미 상실 
            \\ 알파벳에 특수한 의미를 부여하는 기능(이스케이프 시퀀스) 상실
            \. 아무 캐릭터나 가리킬 수 있는 기능 상실
X|Y : X or Y
\s : 공백문자
\S : 공백문자를 제외한 캐릭터
[\]: \\ 한 것과 의미 동일
[.^]: \. 과 \^ 한 것과 의미 동일



III. AWK

syntax

1. pattern{ action statements }
2. function name(parm list) { statements }

2번은 함수를 정의하는 문법이다. C와는 조금 다른 점은 parm list에 매개변수 뿐만아니라 로컬변수도 써줘야 하는 점이다. 그 점 외에는 특별한 것이 없다.

awk의 프로세싱은 입력 스트림 => awk processing => 출력 스트림이다. 입력 스트림은 파일 또는 표준 입력을 지정할 수 있다. 출력 스트림 역시 파일 또는 표준출력임은 두말하면 잔소리다. 그중 awk processing 부분이 우리가 주목해야할 부분인데 1번에 나온 문법대로 프로그래밍을 해야 한다.
즉 입력스트림을 받아서 pattern에 적용해 보고 패턴에 일치하면 action statements 를 실행하는 것이다. 또한 다수의 pattern{ action }을 체인으로 사용할 수 있다. 1번을 조금 더 C와 비슷한 모양으로 고치면 이렇게 쓸 수도 있다. pattern{ action } 에서 pattern 부분을 생략하고 action의 if 문 안에 패턴을 검사하는 statement를 넣었다.
{
   if (pattern) {
         action statements
   }
}


보다시피 pattern{ action }의 pattern 은 생략가능 하다. 이 경우 모든 입력스트림에 대해 {action}을 수행하게 된다.

그리고 pattern{ action } 에서 { action }도 역시 생략 가능하다! 이 경우 pattern과 일치하는 입력 스트림 부분을 print하는 action이 자동으로 실행한다.


AWK를 배우는데 문법 구조 외에 알아야 할 게 또 있다. 바로 미리 정의된 변수들(pre-defined variables)이다. 이 변수들은 각각 특별한 값을 가지게 되는데 상황에 따라 매번 변하는 변수도 있고 사용자가 변경하지 않는한 계속 그대로인 변수도 있다.

또한 AWK는 일반변수와 배열도 지원한다.

그리고 다양한 표준 함수들도 가지고 있다.

이제 어째서 AWK를 프로그래밍 언어라고 하는지 감이 오지 않는가? 이제 위에 나열한 AWK의 기능들을 설명하겠다. 이 문서는 레퍼런스라기보다는 초보자용 가이드라인이므로 awk의 모든 기능을 설명하는 대신 자주 사용되는 기능들만 설명한다.


pre-defined variables

미리 정의된 변수들이 있다. built-in variables 라고 부르기도 한다. 이 변수들을 이해하기 위한 몇가지 개념을 먼저 소개하겠다. record나 field는 awk의 입력으로 들어온 전체 텍스트 중 일부다. 전체 텍스트가 awk를 거치는 모습은 간단히 이렇게 그릴 수 있을 것이다.
Input ==> awk processing ==> output
input 은 표준입력이 될 수도 있고 평범한 파일일 수도 있는데 awk는 작업을 처리하기 위해서 input을 통째로 작업대 위에 올려놓지 않고 부분부분 잘라서 올려놓는데 이 때 작업대 위로 올려놓은 게 레코드다. 그리고 그 레코드는 또 필드로 이루어져 있다.


이제 '변수이름: 의미' 형식으로 미리정의된 변수들의 의미를 중요한 것만 알아보겠다.

RS: record separator (디폴트는 개행문자)
FS: field separator (디폴트는 공백문자)
NF: the number of fields (FS로 나뉜 한 레코드 안의 필드 개수)
ORS: output record separator(디폴트는 개행문자)
NR: total number of record so far(현재 라인 번호)
ARGC, ARGV: 매개변수의 개수와 매개변수를 가리키는 변수
FNR: number of record in the current input file
OFS: ouput field separator(디폴트는 공백문자)
FILENAME: name of input file
SUBSEP: separate multiple subscripts in array elements(,; \034; \0x1C)
IGNORECASE: 값이 0이 아니면 패턴 매칭에 대소문자 구별 안 함


RS는 awk가 전체 텍스트 중 작업대 위로 레코드를 올릴 때 어떤 단위로 올릴지 정하는 변수라 할 수 있겠다. 디폴트 값은 newline 캐릭터이므로 이 때 awk는 grep이나 sed처럼 한 줄 씩 끊어서 작업한다고 말할 수 있다.
FS는 레코드 내의 필드들의 구분자이다. 기본 값은 공백이므로 어떤 레코드의 내용이 abc def ghi jkl 이면 1번 필드($1)는 abc,2번($2)은 def 이런 식이다. 그리고 $0은 abc def ghi jkl 이다. 즉 현재 레코드의 전체 부분을 가리킨다.
awk의 매개변수에 파일을 여러개 지정했다면 FNR과 NR의 차이점도 알아야 한다. FNR은 지금 awk가 작업하고 있는 파일 내에서 몇번째 레코드인지 가리키고 NR은 파일 구분 없이 맨처음부터 지금까지 총 몇번째 레코드인지 가리킨다.
SUBSEP은 배열 사용시 필요한 내용인데, 배열을 설명할 때 부가 설명드리겠다.

시험삼아 위에 설명한 내장 변수들을 사용한 몇가지 간단한 관용구처럼 사용되는 one liner들을 따져보자.

1. awk 'NR%2==0{ print $0 }'
2. awk 'NR%2'
3. awk '0'
4. awk '”0”'
5. awk 'NR>5&&NR<10'
6. awk '$0 = NR" "$0'

1번은 짝수 줄NR%2==0이면 출력하라{ print $0 }는 뜻이다. 2번은 홀수 줄이면 출력하라는 뜻이다. 왜냐하면 NR이 홀수 줄이면 NR%2 가 0이 아니므로 디폴트 액션인 { print $0 }이 출력되기 때문이다. 여기서 NR%2의 값은 0또는 1인데 0은 false를 뜻하고 0이 아닌 숫자는 true를 뜻한다. 3번은 아무 줄도 출력하지 않는다. 왜냐하면 항상 0, false이기 때문이다. 4번은 항상 출력한다. "0"은 문자열이기 때문에 항상 참이다. 5번은 6,7,8,9 줄을 출력한다. 6번은 줄앞에 줄번호를 붙인다. $0은 현재 레코드인데 $0 = NR" "$0 은 $0에 NR" "$0을 대입하라는 의미이다. 중간에 " " 은 단지 줄번호(NR)과 레코드($0) 사이에 공간을 두기 위해 붙인 문자열에 지나지 않는다.

변수와 배열

변수는 숫자거나 스트링일 수 있다. awk의 변수와 배열은 따로 초기화하거나 타입을 지정할 필요가 없으며 필요시 자동으로 형변환이 된다. 즉 사칙연산이 필요할 때는 숫자였다가 문자열 이어붙이기를 할 때는 스트링이 되기도 한다.

초기화가 필요 없으므로 곧바로 a++; 이렇게 쓸 수도 있다. 이때 a의 값은 1이 된다. 왜냐하면 초기화하지 않은 변수의 값은 숫자의 경우 0이기 때문이다. 그리고 변수에 octal 과 hexadecimal 값을 넣을 수 있다.  a=0nn;, b=0xnn; 이렇게 사용하면 된다.

변수에 스트링을 대입하는 방법은 a="string" 이런 식이다. 또한 스트링 안에 이스케이프 시퀀스도 사용할 수 있다. 몇가지 이스케이프 시퀀스의 예는 다음과 같다.
\\: 백슬래시 캐릭터 그대로. 백슬래시는 이스케이프 시퀀스를 만들 때 사용되므로 문자 그대로의 백슬래시를 써야할 때는 백슬래시를 두개붙여쓴다.
\a: ASCII의 BEL 캐릭터
\b: 백스페이스 캐릭터
\n: 개행문자
\r: 캐리지 리턴
\t: 수평 탭
\v: 수직 탭
\ddd: octal value로 표현되는 캐릭터. 0-7까지의 숫자만 이용해서 캐릭터를 표현하며 범위 밖의 숫자 또는 문자는 이스케이프 시퀀스에 해당하지 않는다. 즉 그 앞 부분까지만 octal value로 표현되는 이스케이프 시퀀스다. 예를들어 "\438"은 #8이다.
\xnnn: hexadecimal value로 표현되는 캐릭터. 역시 \x 뒤를 따르는 모든 16진수 숫자들을 이스케이프시퀀스로 받아들이고 범위를 벗어나면 그냥 문자그대로로 인식한다. 예를들어 "\x23G"는 #G이다.


배열도 변수와 마찬가지로 초기화가 필요하지 않다. 다짜고짜 a[0]++; 이런 식으로 쓸 수 있다. 그리고 특이하면서 유용하게 사용되는 특징은 배열의 인덱스에 스트링을 넣을 수 있다는 점이다. 그리고 숫자로는 음수나 소수를 넣을 수 있다. 예를 들어
a["Foundations of AWK"]=1; 이렇게 쓰거나 a[-12.07]++; 이렇게 쓸 수도 있다. 또한 awk의 배열은 다차원 배열을 흉내낼 수 있는데 다음과 같이 쓸 수 있다. 
a["Foundations of AWK","Junyeong"]="Jeong";
a["Foundations of AWK","Alfred"]="Aho";
a[0,1,0]="Kernighan";
i=0; j=1; k=1;
a[i,j,k]="Weinberger";

즉 배열의 섭스크립트를 여러 문자열이나 숫자의 조합 또는 변수의 조합으로 사용할 수 있으며 조합시에 , 를 사용하는데 이것은 아까 pre-defined 변수에서 SUBSEP 변수의 값인 캐릭터이다. 이 캐릭터가 눈에 잘 띄지 않는다면 SUBSEP변수를 바꿔서 다른 캐릭터로 변경해도 되고 캐릭터를 octal 이나 hexadecimal 값으로 기입해도 상관없다. 즉 위의 예를
a["Foundations of AWK\034Junyeong"]="Jeong";
a["Foundations of AWK\034Alfred"]="Aho";
이렇게 쓸 수 있다는 말이다. 물론 a["Foundations of AWK","Junyeong"]="Jeong"; 으로 값을 할당하고 a["Foundations of AWK\034Junyeong"] 으로 값을 가져오는 것도 당연 가능하다. 단지 "," 캐릭터를 스트링의 이스케이프 시퀀스로 표현했을 뿐이기 때문이다.

그리고 배열의 특정 섭스크립트를 검색하는 연산자로서 in 이 있다. 사용법은 subscript in array이다. 만약 위의 예를 적용하면
if ("Foundations of AWK\034Junyeong" in a) print a["Foundations of AWK\034Junyeong");
이다. 혹은 if (("Foundations of AWK","Junyeong") in a) print a["Foundations of AWK","Junyeong"]; 도 똑같다.



패턴

AWK의 syntax, awk 'pattern{ action statements }' 중에서 pattern 부분을 작성하는 방법은 다음과 같다.

BEGIN{ }: 모든 인풋보다 먼저 실행되는 statements를 지정, 즉 awk의 작업대에 첫 레코드가 올라가기 전에 실행되는 statement들의 블록을 지정하는 셈이다. 초기화 작업을 하는 곳이라고 생각하면 된다. BEGIN 패턴은 한 awk 명령에 여러번 나올 수 있는데 모든 BEGIN블록은 한데 모아진다.
END{}: 더 이상 인풋이 없을 때 실행되는 statements지정. 역시 END블록은 여러번 나올 수 있고 하나의 END블록으로 모아진다.
/정규식/: / / 로 묶어서 정규식을 사용할 수 있다.
패턴1 && 패턴2: AND 논리 연산, 물론 패턴이라는 말은 /정규식/도 내포한다.
패턴1 || 패턴2: OR 논리 연산
!패턴: NOT 논리 연산,
패턴1, 패턴2: range pattern인데 패턴1인 레코드로부터 패턴2인 레코드까지인 범위를 지정한다.



연산자들

기본적으로 C에서 사용할 수 있는 연산자들(포인터나 구조체 관련한 연산자 제외)을 사용가능하다. 거기에 덧붙여 AWK에서 사용되는 특별한 연산자들이 있으니 그것만 여기 소개하겠다.
$: field reference, 레코드에서 FS로 구분되는 필드를 가리킨다. 특별히 $0은 모든 필드들을 뜻한다.
space: 공백 연산자는 스트링과 스트링을 붙이는데 사용한다.
|, |&: getline, print, printf에 사용되는 piped I/O
~, !~: 정규표현식 매치, 비매치. 정규식을 연산자의 오른쪽에 써야만 한다.
in: 배열에서 특정 섭스크립트 검색

제어문

C에서 사용하는 제어문과 비슷하다. AWK는 C를 많이 따랐기 때문에 비슷한 것은 당연하다. 다만 배열에 관련해서 delete문이 있고 switch문은 없는데, switch 문은 awk의 옵션으로 --enable-switch로써 사용가능하다.
if (condition) statement [ else statement ]
while (condition) statement
do statement while (condition)
for (expr1; expr2; expr3) statement
for (var in array) statement
break
continue
delete array[index]
delete array
exit [ expression ]
{statements }


I/O 문

I/O statements는 여러 종류가 있지만 그중 자주 사용되는 getline과 print, printf만 알아보겠다. 다른 종류(close, system, fflush, next, nextfile) 대해서는 맨페이지를 참고하시라 ;^)

getline: 다음 레코드를 $0로 설정함
getline < "file": 다음 레코드를 "파일"에서 가져온다
getline var: 다음 레코드를 변수 var에 넣는다
getline var < "file": 다음 레코드를 "파일"에서 가져와서 변수 var에 넣음
"command" | getline [var]: "command"를 실행하여 표준출력을 파이프로 넘기고 넘어온 입력을 getline이 $0 [또는 변수var]으로 설정한다
"command" |& getline [var]: "command"를 co-process로 실행시켜 표준출력을 파이프로 넘이고 getline은 넘어온 입력을 $0 [또는 변수var]로 설정한다
print: 현재 레코드를 출력한다. 출력된 레코드는 ORS가 맨 끝에 붙음
print expr-list: 표현식을 출력한다. 각 표현식은 OFS로 구분되어 출력된다. 예를 들어 print $1, $2;
print expr-list > "file": 표현식을 "file"로 출력한다
print expr-list >> "file": 표현식을 "file" 끝에 덧붙인다
print expr-list | "command": 표준출력을 파이프에 쓰고 "command"에서 그 파이프를 표준입력으로 받는다
print expr-list |& "command": print 표준출력을 파이프에 쓰고 "command"는 co-process로 실행돼 파이프에서 표준입력을 읽는다
printf format, expr-list: C의 printf처럼 포맷을 지정해서 표현식을 출력한다(포맷은 C와 유사하며 자세한 내용은 awk맨페이지를 참고하시라)
printf format, expr-list > "file": 포맷을 지정해서 표현식을 파일로 출력한다.

위의 설명들은 쭉 읽어보면 아마 하나 빼고 바로바로 이해가 될 내용일 것이다. 그 하나는 바로 co-process 를 실행시킨다는 |& 연산자를 사용한 부분일 것이다.
"command" | getline 이나 print | "command" 중 전자는 각각 "command"의 표준출력을 getline의 표준입력으로 보낸 것이고, 후자는 print의 표준출력을 "command"의 표준입력으로 보낸 것이다. 둘 다 일방통행이라는 공통점이 있다. 그런데 |& 연산자를 사용할 시에는 "command"를 co-process(awk와 어깨를 나란히 하고 같이 뛰는 프로세스)로 실행시키게 된다. 그러므로 양방향 통신이 가능한데, 무슨 뜻이냐하면 print |& "command1"; "command1" |& getline 처럼 사용해서 print로써 명령으로 입력을 보내고 getline으로 결과를 받아본다는 뜻이다. 당연히 양쪽에 기입된 "command1"은 서로 같아야 출력을 올바로 볼 수 있다. 실제 예를 보면 확실히 이해가 될 것이다.
to_upper 배시 셸 스크립트
#!/bin/bash
#set -x
while read arg
do
    echo "$arg" | tr '[a-z]' '[A-Z]'
done

g6라는 awk 스크립트
{
  print $0 |& "./to_upper"
  "./to_upper" |& getline hold
  print hold
}

이제 이 awk 스크립트를 명령행에서 이렇게 실행시킨다
$ awk -f g6 < SOMEFILE

결과는 SOMEFILE 안에 있는 모든 영소문자들이 영대문자로 치환되어 출력된다. 만약 g6의 |&대신 |을 사용한다면 의도한대로 실행되지 않을 것이다. 비록 첫번째 statement는 ./to_upper로 print의 출력을 입력시킬 순 있지만 두번째 statement의 ./to_upper 프로세스는 첫번째 statement에서 돌아간 ./to_upper의 프로세스랑 다른 것이므로 원하는 결과는 얻을 수 없다!

I/O문에서 사용하는 특별한 파일들

리다이렉션을 이용해 파일로 print, printf 의 결과를 출력하거나 getline으로 파일에서 입력을 받을 수 있는데, 몇몇 파일이름들은 이미 awk가 특별한 의미를 부여해 놓았으므로 파일시스템 상의 파일을 의미하진 않는다.

/dev/stdin: 표준입력
/dev/stdout: 표준출력
/dev/stderr: 표준에러
/dev/fd/n: n번 파일디스크립터
/inet/tcp/lport/rhost/rport: TCP/IP 커넥션. 로컬의 포트는 lport, 원격호스트는 rhost, 원격포트는 rport. lport를 0으로 지정하면 운영체제가 알아서 선택한다.
/inet/udp/lport/rhost/rport: UDP/IP 커넥션. TCP/IP 와 사용법 같다.

그리고 현재 실행 중인 awk 프로세스에 대한 정보를 얻기위해
/dev/pid, /dev/ppid, /dev/pgrid, /dev/user 를 사용할 수 있다. gawk에서는 이 파일들 대신 미리정의된 변수인 PROCINFO 배열을 사용한다. PROCINFO에서 사용되는 섭스크립트는 다음과 같다. "FS", "egid", "euid", "gid", "group1", "group2", "pgrpid", "pid", "ppid", "uid", "version"


AWK가 제공하는 표준 함수들

표준함수에 대한 내용은 이문서에 설명하지 않겠다. 함수에 대한 설명이 맨페이지에 일목요연하게 나와있기 때문이다. 다만, 표준함수들에 대한 분류만 보기 좋게 써보겠다.
Numeric functions
String functions
Time functions
Bit Manipulation functions
Internationalization functions(since gawk 3.1)

뭐니뭐니해도 스트링 함수들이 가장 자주 사용되므로 거기에 있는 유용한 함수들은 잘 알아두는 게 좋다.


사용자 정의 함수

맨처음 syntax 섹션에서 function name(parm list) { statements }라고 적어놓은 것을 기억할 것이다. 이게 함수를 정의하는 방식이다. 특이한 점은 parm list 에 로컬 변수도 적어줘야 하는 점이다. 그리고 함수이름 name과 왼쪽 괄호 ( 는 떨어져서 안 된다. 예를 들어
function test(p, q,          i, j) {
      statements
}
p,q 는 함수를 호출 시 넘겨야하는 파라미터고 i,j 는 statements에서 사용되는 로컬 변수이다. 관습적으로 파라미터와 로컬변수 사이는 널찍하게 스페이스를 둔다.


awk 옵션들

커맨드라인에서 awk를 실행시킬 때 유용하게 쓸 수 있는 옵션 몇가지를 추려보았다.
-F fs: FS의 값을 지정한다. BEGIN{ FS="fs" } 이렇게 한 것과 같다.
-v var=val: 변수 var를 val로 지정한다. BEGIN{var=val} 한 것과 같다
-f file: awk 스크립트를 지정한다
--dump-variables[=file]: global 변수들의 이름과 값을 출력한다. file을 지정하지 않으면 현재 디렉터리의 awkvars.out 파일로 출력한다. 디버깅용으로 사용할 수 있다.
--re-interval: 정규표현식 중 인터벌을 사용할 수 있게 한다. {n,m}



IV. AWK Examples

앞에서 이론 공부를 했으니 이제 몇가지 예제를 보면서 익히는 게 좋을 것 같다. 우아한 것들로 골라봤다.

1. awk '$7 ~ /^[a-f]/'
7번 필드가 정규식 /^[a-f]/에 매칭된다면 출력한다.

2. awk '$7 !~ /^[^a-f]/{ print > “/proc/self/fd/2” }'
바로 앞의 예와 같은 패턴 매칭이지만 출력을 표준에러로 한다. "/dev/stderr" 를 대신 쓸 수 있다.

3. awk 'NF'
NF가 0(false)이 아니면 출력한다. 즉 빈행이 아니면 출력한다. 여기서 action statements는 생략됐으므로 디폴트인 { print $0 }가 실행됨은 이제 다들 아실거라 생각한다.

4. awk 'NF{ $0=cnt++" "$0}1'
NF가 0이 아니면 줄 앞에 cnt를 표시한 뒤 출력한다. 1이라는 숫자에는 의미가 없다. 단지 패턴에 0이 아닌 다른 숫자나 스트링이 오면 항상 true 이기 때문에 디폴트 액션이 실행된다.

5. awk '{ sub(/^[ \t]*/,""); print }'
표준 스트링 함수인 sub(pattern,replacement) 를 사용했다. 모든 패턴에 대해서 sub(/^[ \t]*/,"") 를 적용하며 이 함수는 레코드 자체를 수정하므로 리턴값을 받을 필요는 없다. 함수 의미를 풀면 정규식 ^[ \t]* 를 지우라는 뜻이다. 즉, leading 공백문자들을 삭제한다.

6. awk '{ gsub(/^[ \t]*|[ \t]*$/,"") }1'
이것은 앞선 예와 비슷한 기능이다. gsub() 함수는 sub의 global버전이라 할 수 있겠다. sub은 최초 매치만 substitution을 행함에 반해 gsub은 모든 매치들에 대해 substitution을 수행한다. 위 one-liner의 뜻은 모든 leading whitespace 캐릭터들 (^[ \t]*) 또는 (|) 모든 trailing whitespace 캐릭터들 ([ \t]*$)을 제거하라는 뜻이다. 그리고 맨 마지막에 1을 붙여서 디폴트 액션을 수행하게끔 하였다.

7. awk '!a[$0]++'
이것은 배열의 섭스크립트에 스트링도 넣을 수 있다는 점과 awk에서 변수나 배열은 초기화가 필요하지 않다는 점을 십분활용한 예다.
만약 현재 레코드가 처음 나온 레코드라면(현재 라인이 앞선 라인들과 중복되지 않은 라인이면) a[$0]의 값은 0이다. 그러므로 !a[$0]은 0이 아니다. true다. 패턴이 true이므로 디폴트 액션문이 실행되는 조건을 만족시킨다. 그러고 나서 a[$0]을 1 증가시켜서 (a[$0]++) 값이 1이 된다. 만약 나중에 똑같은 레코드가 들어왔다면, !a[$0]은 false가 돼서 출력은 되지 않을 것이다. 그래도 a[$0]++은 실행되므로 값이 증가된다. 결론적으로 이 one-liner의 역할은 중복된 라인을 제거하면서 똑같은 라인의 개수를 파악하는 것이다.

8. awk 'BEGIN{FS=""}{ for (i=NF;i>=1;i--) printf "%s",$i; print ""}'
우선 인풋이 들어오기 전에 BEGIN{} 블록을 실행한다. FS를 널스트링으로 바꿨다. FS 의 값 설정은 awk -F"" 로써 편리하게 할 수도 있다. FS가 널스트링이면 한개의 필드는 한개의 캐릭터에 대응된다. for 문에서 사용한 NF는 필드의 개수를 뜻하므로 i=NF는 마지막 필드를 가리키는 셈이다. for문을 돌면서 현재 i가 가리키는 필드를 printf문으로 출력한다. 즉 맨마지막 캐릭터부터 맨앞 캐릭터까지 역순으로 출력한다. 마지막 print "" 는 단지, 마지막에 ORS를 붙여주기 위함이다.

9. cat /usr/man/man1/awk.1 |grep '\(^\.SH\|^\.SS\)'|awk '/\.SS/{ $0 = "    "$0}{sub(".SH",""); sub(".SS", "")}1'
awk 맨페이지를 cat으로 읽은 뒤 타이틀을 뜻하는 .SH와 .SS 라인만 추려내 그 라인을 보기 좋도록 .SH와 .SS를 떼어내는 one-liner이다. 즉 맨페이지의 목차를 보여준다. 만약 맨페이지가 gzip 압축됐다면 cat 명령어를 gzip -cd 로 대체하면 된다.

10. awk 'NR==40001,NR==40100{ array[$3]++};END{ size=asorti(array,dest); for (i=1; i <= size; i++) {print dest[i], array[dest[i]]}}' /var/log/messages
messages 파일 중 40001번째 줄부터 40100번째 줄까지 읽고 세번째 필드의 출현 횟수를 기록한다. messages의 세번째 필드는 시각이다. 그러고 나서 모든 인풋이 소진되면 END블록이 실행된다. asorti 는 표준 스트링함수인데 array배열을 받아서 배열의 값대로 정렬한 뒤 결과를 dest 배열의 값에 넣고 인덱스는 1번부터 시작해 증가시킨다. 즉 dest배열은 시간순으로 정렬된 셈이다. 그리고 마지막 for문에 print dest[i], array[dest[i]] 로써 "시간  출현횟수" 형식으로 출력한다.

11.  awk 'END{ print NR}'
모든 인풋이 소진되고 END블록이 실행된다. NR의 값은 마지막 줄번호를 가리키므로 총 라인 수를 출력하는 셈이다.

12. awk 'BEGIN{ mysql="mysql -u rhdxmr --password=somepassword mydb" } { string1="\047Jaco Pastorius\047"; string2="\047Cha Cha\047"; print "insert into test values("string1","string2");"|mysql}'

이 예에서는 print I/O statement의 표준출력을 | 연산자로써 외부 프로그램의 표준입력에 주었다는 것이 주목할 점이다. 명령행에서 작은 따옴표를 넣으면 의도와 다르게 배시에서 파싱돼버리므로 octal value인 \047로 작은 따옴표를 표현했다. 그래서 MySQL에 입력된 쿼리문은 insert into test values('Jaco Pastorius','Cha Cha');다. 이 예에서 처럼 쿼리문의 일부를 변수로 대체시켜 상황에 따라서 다른 쿼리문을 생성할 수 있다.

13. awk 'length < 86'
length는 변수처럼 보이지만 원래는 표준 함수 length()이다. awk에서 유일하게 변수형태와 함수형태 모두 사용가능한 녀석이다. 이것은 어디까지 historical feature이므로 깊게 생각할 필요는 없다. length 는 length($0)와 같은 의미인데 이 one-liner는 한 레코드의 전체 길이가 86이하인 짧은 줄만 출력하라는 뜻이 된다.

14. awk '{ print $NF }'
NF는 현재 레코드의 총 필드의 개수를 의미하는데 이는 곧 마지막 필드가 몇번째 필드인지 가리킨다. 그러므로 print $NF는 마지막 필드를 출력하라는 의미이다.

15. awk '{  for(i=1; i<=NF; i++) A = A" "$i; print A; A = "" }'
i의 초기값으로 지정된 필드부터 마지막 필드까지 출력한다. for문의 i값에 처음 출력을 원하는 필드번호를 적으면 그 필드부터 마지막 필드까지 출력한다.

여기까지 먼길을 왔는데 AWK에 대한 설명은 이쯤에서 마치도록 하겠다. 조금 쉬었다 바로 SED가 무언지 보러 갈테니 아직 긴장을 풀지 마시길. 낄낄


출처 : http://rhdxmr.tistory.com/57
반응형

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

CENTOS 7에 XRDP 설치하기  (0) 2017.08.26
리눅스 백업 및 복구  (0) 2013.01.25
삼성 컴퓨터 유분투 설치기.  (0) 2012.02.06
rkhunter (리눅스 침입 탐지 사용하기)  (1) 2010.10.14
ps auxc 와 ps aux 결과 비교하기  (0) 2010.01.21

+ Recent posts