내밥줄/프로그래밍

[펌] [linux] AWK & SED chunk 2

jjoell 2013. 5. 16. 08:54

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


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


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

현재 문서 버전

2009년 12월 11일 버전 <- 현재

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(이 포스트에서 설명)



V.SED

SED 작동의 기본 개념


인풋스트림은 물론 표준 입력 내지는 파일입력이다. 인풋스트림이 패턴스페이스에 올라갈 때는 1라인씩 끊어서 올라간다. 패턴스페이스에 들어간 스트림은 sed명령어를 이용해 수정할 수 있다. 홀드버퍼는 패턴스페이스에 들어간 스트림 중 차후에 사용하려는 부분을 임시로 저장할 수 있다. h, g, x 등의 명령을 쓰면 홀드버퍼에 내용을 쓰거나 홀드버퍼로부터 내용을 가져올 수 있다. 마지막으로, sed 명령을 모두 수행하면 패턴스페이스의 내용을 아웃풋스트림으로 출력한다.


SED명령어들

SED를 사용하기 위해서 익혀야할 것은 패턴스페이스 위에 올려진 한 줄을 요리하기 위한 명령어다. 각 명령에 대한 간략한 설명을 기술하겠다.

= : 현재 줄 번호를 출력한다.
b label : 이 명령어로 지정한 label로 이동한다(점프한다). label 이 명시되지 않으면 맨 끝으로 이동한다(패턴스페이스에서 할 일은 끝났고 아웃풋스트림으로 출력)
d: 현재 패턴스페이스 위에 올라온 내용을 지운 뒤 다음 줄을 읽어서 패턴스페이스에 올린다. 그리고 맨 처음 명령어로 돌아간다.
D: 패턴스페이스 위에 올라온 내용 중 첫번째 newline 캐릭터까지만 지운다. 만약 패턴스페이스에 내용이 남아있다면 다음 줄을 읽는 것은 건너뛴다. 그리고 맨 처음 명령어로 돌아간다. 
g: 홀드 버퍼에 있는 내용을 패턴스페이스에 덮어쓴다
G : 홀드 버퍼에 있는 내용을 패턴스페이스의 맨 뒤에 붙인다.
h : 패턴스페이스의 내용을 홀드버퍼에 덮어쓴다.
H : 패턴스페이스의 내용을 홀드버퍼의 맨 뒤에 붙인다.
n : 패턴스페이스의 내용을 출력하고 패턴스페이스를 비운 뒤 다음 줄을 읽는다
N: 다음 줄을 현재 패턴스페이스의 맨 뒤에 붙인다
p : 현재 패턴스페이스 전체를 출력한다
P : 현재 패턴스페이스 중 첫번째 newline캐릭터까지 출력한다
q :  즉각 sed 를 종료한다. 단 auto-print 기능이 꺼져있지 않다면 현재 패턴스페이스를 출력한 뒤 종료한다.
Q: auto-print 기능과 상관 없이 더 이상의 프로세스를 수행하지 않고 즉각 sed를 종료한다.
s/regex/repl/ : regex 를 repl 로 변경한다. 찾아바꾸기 기능이라고 생각하면 된다.
t label : s/// 명령어가 성공했을 경우(regex가 repl로 바뀌면) label로 점프한다. 아무 것도 정해지지 않으면 (label이 명시되지 않으면 맨뒤로 점프한다)
T label : s/// 명령어가 성공하지 않을 경우 label로 점프한다.(label이 명시되지 않으면 맨 뒤로 점프한다)
x : 홀드버퍼의 내용과 패턴스페이스 내용을 맞바꾼다.
r filename: 파일에서 읽은 내용을 통째로 패턴스페이스 뒤에 붙인다
R filename: 파일에서 읽은 한 줄을 패턴스페이스 뒤에 붙인다. 또 이 명령이 실행되면 그 다음 줄을 읽는다.
w filename: 현재 패턴스페이스의 내용을 파일에 쓴다.
W filename: 현재 패턴스페이스 내용 중 newline 캐릭터까지 파일에 쓴다.

아래 그림은 위에서 설명한 명령어들을 이해를 돕기 위한 pattern space 와 hold buffer 그림이다.

d,n,p 명령어는 패턴스페이스 전체부분에 적용된다. 각각은 패턴스페이스 전체를 지우고, 전체를 출력하고 비우고, 전체를 출력한다. D,P는 패턴스페이스 중 맨 처음 나타나는 newline캐릭터까지 지우고, 출력하는 명령어들이다.
h,H는 패턴스페이스 내용을 홀드버퍼에 덮어쓰거나, 이어붙이는 명령이다. g,G는 홀드버퍼의 내용을 패턴스페이스에 덮어쓰거나, 이어붙이는 명령이다. x는 패턴스페이스의 내용과 홀드버퍼의 내용을 서로 맞바꾸는 명령이다.


주소 지정

주소 개념은 어떤 라인에서 SED 명령을 실행할지를 결정하는 조건의 개념이다. 주소를 정하는 방법은 라인번호를 직접 정할 수있고, 정규식과 매칭하는 줄만 골라낼 수도 있고 몇줄부터 몇줄까지라고 범위를 표현할 수도 있다.

조건을 부정하려면 ! 연산자를 쓴다. !는 주소 다음이면서 명령어 앞에 위치해야 한다. 예를 들어 $!p 이렇게 맨마지막줄이 '아니면' p를 하라고 지시할 수 있다. sed에서 사용할 수 있는 주소 형식은 다음과 같다. 

first~step: first가 매치하는 줄부터 매 step 다음 줄을 출력한다. 1~5면 1,6,11,16, 이런식이다.
$: 맨 마지막 줄
/regex/: 정규표현식이 매치되는 줄
addr1,addr2 : addr1 를 매치하는 줄부터 addr2를 매치하는 줄까지 그 사이에 있는 모든 줄. addr1이나 addr2 는 /정규식/일 수도 있고 줄번호(5,15 이런식)일 수도 있다.
0,addr2: 첫번째 줄부터 addr2가 매치하는 줄까지 그 사이에 있는 모든 줄. addr2 가 첫번째 줄에 매치한다면 1,addr2하고 다른 결과를 낸다. 왜냐하면 주소 범위를 지정할 경우 주의사항이 addr1과 addr2가 같은 줄에 있어선 안 되기 때문이다.
addr1,+N: addr1이 매치되는 라인부터 그 뒤로 N번째라인까지 사이의 모든 라인을 지정한다. 110,+6 이런식으로 쓰면 된다. 110번째 줄부터 116번째 줄까지다.
addr1,~N: addr1 매치되는 라인부터 N의 배수 줄까지 그 사이의 모든 라인을 지정한다. 예를 들어 110,~6이면 110~114 줄을 지정한다. 

0,addr2 와 1,addr2 를 설명하면서 잠깐 언급했는데, 주소를 범위(address range)로 지정할 때는 규칙이 있다. 

1. addr1, addr2 형식
2. addr1은 언제나 accept 이다. 그러므로 addr2가 addr1보다 작은 값이 왔다고 하더라도 에러가 발생하진 않는다.
3. addr2가 /정규식/이라면 addr1이 매치된 라인에서는 검사하진 않는다. 

3번규칙때문에 1,addr2에서 addr2가 첫번째 줄에만 매치되는 정규식이라면 이 표현은 결국 첫번째 줄부터 맨마지막 줄까지 모두 해당된다.


sed 옵션

-n: auto-print 기능을 끈다. 즉 p명령어로 출력하는 경우에만 출력한다.
-e 'script': 'script'를 실행한다. -e를 쓰지 않아도 되지만 가독성을 높이기 위해 여러개의 -e 'script' -e 'script1'... 이런식으로 쓰는 것도 좋은 방법이다.
-r : 확장된 정규표현식을 사용한다. 예를들어 \(\) 이런식으로 쓸 필요 없이 () 이렇게 쓸 수 있다.
-f script-file: 지정된 sed 스크립트 파일을 실행한다.



VI. SED 예제

1. sed '/^$/d;G'
/^$/ 정규식을 매칭하는 어드레스면 d 명령을 실행한다. d는 현재 패턴스페이스 내용을 지운 후 다시 sed 스크립트의 맨 처음 부분으로 돌아간다. 즉 또 다시 /^$/를 매칭하는지 검사한다. 또 매칭하면 지운다. 이렇게 반복하다가 /^$/에 매칭되지 않으면 G를 실행하게 된다. G는 홀드버퍼에 있는 내용을 패턴스페이스 뒤에 붙이는 명령어다. 이 경우 홀드버퍼에 아무 내용도 없으므로 그냥 빈행이 이어붙여지는 셈이다. 정리하자면 빈행은 모두 지우고 빈행이 아닌 행 다음에 빈행을 한 줄 추가한다.

2. sed = file| sed 'N;s/\n/\t/'
=는 줄번호를 출력한다. 그러나 줄번호는 카운트된 라인의 바로 앞줄에 따로 표시가 되기 때문에 모양이 보기 좋지 않다. 즉 홀수줄은 줄번호가 나오고 짝수줄은 기존 file의 라인이 나온다. 
그러므로 두번째 sed를 해석하면 첫째줄에서 N으로 두번째 줄을 패턴스페이스 뒤에 이어붙인 뒤 첫째 줄과 두번째 줄 사이의 경계 역할을 하는 newline 캐릭터를 horizontal tab캐릭터로 대체한다. 1,2 줄을 읽어드렸으므로 그다음은 3번째 줄에서 N을 실행해 4번째 줄도 읽어드린다.
최종 결과는 줄번호가 앞에 나오고 탭캐릭터로 구분돼 본문이 표시된다.

3. sed -n '$='
-n 옵션은 auto-print 기능을 끄는 설정을 한다. 그러므로 마지막 줄($)에서만 =로 줄번호가 출력된다. 즉 전체 라인수를 카운트한 것이다.

4. sed 's/^\s*//'
^\s*가 뜻하는 바는 라인시작에 공백문자 여러개를 의미한다. 물론 0개일 수도 있다. 그 공백문자를 아무것도 아닌 것으로 대체한다. 즉 앞에 나오는 모든 공백문자를 지운다.

5. sed '1!G;h;$!d'
첫줄일 경우는 G는 건너실행되지 않고 h로써 패턴스페이스의 내용을 홀드버퍼에 넣는다. 그리고 마지막줄 역시 아니라고 가정하면 현재 패턴스페이스 내용을 모두 지우고 다음줄을 읽은 뒤 처음으로 돌아간다. 두번째 줄의 경우는 G명령으로써 첫번째 줄의 내용을 뒤에 이어붙인다. 그리고 다시 패턴스페이스 내용 전체를 홀드버퍼에 덮어쓴다. 만약 마지막줄이면 d를 건너뛰게 되므로 모든 명령을 마쳤으므로 auto-print 기능 때문에 패턴스페이스의 전체 내용이 출력된다.
결과는 전체 라인의 순서가 역순으로 출력된다.

6. sed '/\n/!G;s/\(.\)\(.*\n\)/&\2\1/;//D;s/.//'
첫줄이면 G로 홀드버퍼의 내용을 패턴스페이스 뒤에 붙인다. 그리고 패턴스페이스의 내용을 s 명령어로 치환한다. 위의 s명령어를 해석하면, 첫줄 내용이 1234라면 현재 패턴스페이스의 내용은 1234\n일 것이다. s명령어의 replacement에 &가 나오면 regex를 매칭하는 패턴스페이스의 부분을 뜻하므로 결국 replacement의 내용은 1234\n234\n1이 되며 결국 패턴스페이스의 내용은 1234\n234\n1이 된다. 그리고 // 는 가장 최근에 매칭한 정규표현식을 의미하므로 현재 패턴스페이스의 내용이 (.)(.*\n)형태라면 첫번째 newline캐릭터까지 지운 뒤 맨 처음으로 돌아가게 된다. 그러므로 1234\n까지 지워서 234\n1인 상태로 다시 처음부터 시작하게 된다. 
이번엔 G 명령은 건너뛰고 s 명령의 replacement는 234\n34\n2이 되고 결국 패턴스페이스의 내용은 234\n34\n21이 된다. 그리고 역시 D명령어로 34\n21이 된다. 이렇게 쭉 반복하면 \n4321이 되고 D명령을 건너뛰고 s명령어로 맨 처음캐릭터인 \n을 없앤 뒤 출력하게 된다. 여기서 키 포인트는 &나 백레퍼런스는 정규식에 이미 매칭된 리터럴을 의미하지만 //는 정규표현식 자체를 의미한다는 점이다.
결과는 한 라인의 모든 캐릭터들을 역순으로 출력하게 된다.

7. sed -e :a -e 'N; s/\n//; ta'
N명령으로 다음 줄을 패턴스페이스 맨 뒤에 이어붙인다. 그 다음 이번 줄과 다음줄 사이에 들어간 newline캐릭터를 없앤다. substitution이 성공했으므로 t 명령이 실행되어 a 레이블로 이동한다. 역시 N 명령어로 패턴스페이스 맨 뒤에 다음줄을 이어 붙인 뒤 newline 캐릭터를 없애고 다시 레이블 a로 이동한다. 그리고 맨 마지막 줄에서는 s 명령이 성공하지 못하므로 t를 건너뛰고 전체 내용이 출력되게 된다.
결과는 모든 라인들이 한 줄로 이어져 출력된다.

8. sed -e :a -e '$!{N;ba}; s/\n//g'
마지막줄이 아니면 다음 줄을 붙인 다음 a레이블로 이동한다. 맨 마지막 줄까지 와서 모든 \n을 제거한다. 결과는 7번과 동일하다.

9. sed '/regex/!d'
정규식이 매칭하지 않으면 지운다. 거꾸로 얘기하면 정규식을 매칭하는 부분만 출력한다. grep을 생각하면 맞다.

10. sed -e :a  -e '/regex/q;N;5,$D;ba'    
정규식에 매칭되면 q로써 sed를 종료한다. 단, auto-print기능으로 인해 현재 패턴스페이스의 내용은 출력하고 종료한다. 
그외의 라인에 대해서는 첫번째 줄일 때는 N으로 두번째 줄을 붙이면 2번째 줄이 되므로 5,$D는 해당하지 않으므로 건너뛴 후 a 레이블로 이동한다. 이 작업이 4번째 줄에서 시작하게 되면 N으로 붙여서 5번째 줄이되고 5,$에 매칭돼 D로써 첫번째 줄을 지운다. 즉 패턴스페이스에 2,3,4,5 줄을 가진 상태다. 즉 라인 수가 커져도 항상 최근 4개 라인을 유지한다. 그리고 그 상태에서 정규식에 매칭되면 최근 4번째 줄을 출력하고 끝나게 되는 것이다.

11. sed -n '/^.\{65\}/p'
한 줄에 캐릭터가 65개 이상이면 정규식에 매칭하므로 그 줄을 출력한다.

12. sed -rn '/^.{65}/p'
-r 옵션을 사용하면 \의 사용을 줄이고 정규표현식을 깔끔하게 쓸 수 있다. 내용은 11번과 동일하다.

13. sed '100q'
100 줄까지 출력하고 종료한다.

14. sed -n '100,$p'
100줄부터 맨마지막줄까지 출력한다.

15. sed -r '$!N; /^(.*)\n\1$/!P; D'
마지막 줄이 아니면 N으로 다음 줄을 붙이고 정규표현식으로써 패턴스페이스 안에 두 줄이 동일한 줄인지 판단한다. 만약 같지 않다면 P로써 newline캐릭터까지 출력한 뒤 D명령어로 newline캐릭터까지 지운뒤 처음으로 돌아간다.
즉 연속으로 중복된 줄이 있다면 한줄만 출력하는 것이다.

16. sed '/^$/d'
빈행은 삭제한다.

17. sed '/./,/^$/!d'
문단과 문단 사이에 빈행을 하나만 두고 모두 제거한다.

18. sed -e :a -e '/^\n*$/{$d;N;ba}'
패턴스페이스에 빈행만 있거나 newline캐릭터만 여러개 있으면 {} 안의 명령들을 실행한다. 처음으로 빈행이 매칭되면 마지막줄이 아니라면 d는 건너뛰고 N으로 다음 줄을 붙인 뒤 b로 처음으로 돌아간다. 만약 다음 줄 역시 빈행이었다면 정규식에 또 매칭되므로 또 다음줄을 붙인 뒤 처음으로 돌아간다. 만약 이번에 붙여진게 빈행이 아니었다면 매칭되지 않기때문에 모든 명령이 끝나고 그대로 출력된다. 만약 빈행이 계속 매칭되다가 $로써 마지막행에 매칭하면 결국 패턴스페이스의 모든 내용을 삭제한 뒤 처음으로 돌아가게 된다. 그러나 마지막 줄이었으므로 더이상 할 일은 남아있지 않고 sed는 끝난다.
즉 글 뒤에 맨 마지막으로 따르는 다수의 빈행들을 삭제하는 것이다.

19. sed 's/'”$1”'/^[[1;33m&^[[0m/g'
이건 셸스크립트나 배시 함수형태로 만들어 놓고 사용하는 것이다. 
만약 함수를 
hl() { sed 's/'”$1”'/^[[1;33m&^[[0m/g' } 이렇게 정의해서 사용한다면 
ps aux |hl 7533 이런식으로 쓸 수 있다. 이건 $1로 넘긴 7533의 색깔을 터미널에서 노란색으로 표시하게 해준다. 주의할 점은 ^[ 은 ^와 [이 아니라 Ctrl-v,Ctrl-[다.



여기까지 SED에 대해서 알아보았다. 앞서 설명한 awk를 잘 다룰 줄 알면 sed의 입지는 조금 좁아지는 게 사실이긴 하지만, sed의 s 명령어는 awk의 그것보다는 더 편리하게 쓸 수 있기 때문에 앞으로도 sed는 중요한 순간에 빛을 발할 것이다. 특히 정규표현식의 백레퍼런스 기능은 sed의 s명령어에 더 큰 힘을 실어준다. 내가 알기로 awk는 백레퍼런스를 사용할 수 없기 때문에 sed의 s 명령이 가지는 유연함을 따라오지 못한다.

AWK와 SED는 분명 겹치는 기능도 있지만 각각이 가지는 장점이 다르기 때문에 둘을 혼용해서 사용할 줄 알면 텍스트를 처리하는데 더 완성도 있는 스크립트를 작성할 수 있다.