AWK의 현주소
원문은 Ben Hoyt님이 에 게재했습니다. 이 블로그 구독하기
AWK는 40년이 넘는 역사를 지닌 텍스트 처리 언어다. POSIX 표준을 갖추고 있으며 이를 따르는 여러 구현체가 존재하고, 2020년 현재까지도 놀라울 정도로 여전히 유효하다 — 단순한 텍스트 처리 작업은 물론 “빅데이터”를 다루는 일에서도 마찬가지다. 최근 출시된 GNU Awk 5.1은 AWK 생태계를 전반적으로 살펴보고, GNU Awk가 그동안 어떤 변화를 겪었는지, 그리고 오늘날 AWK가 어디에서 활용되고 있는지 짚어보기에 좋은 계기가 된다.
이 언어는 1977년 벨 연구소에서 탄생했다. 이름은 최초 저자인 Alfred Aho, Peter Weinberger, Brian Kernighan 세 사람의 이니셜에서 따왔다. 근본적으로 유닉스 도구인 AWK는 한 가지 일을 잘 하도록 설계됐다. 텍스트 행을 필터링하고 변환하는 일이다. 주로 로그 파일에서 필드를 추출하거나 다른 도구의 출력을 가공하고, 단어나 필드의 등장 횟수를 세는 데 쓰인다. Aho는 AWK의 기능을 다음과 같이 간결하게 요약했다:
AWK는 입력을 한 번에 한 줄씩 읽는다. 프로그램에 있는 각 패턴에 대해 한 줄을 스캔하고, 일치하는 패턴이 있을 때마다 그에 연결된 동작을 실행한다.
AWK 프로그램은 대개 커맨드라인에서 직접 실행하는 원라이너 형태가 많다. 예를 들어 가상의 웹 서버 로그에서 GET 요청의 평균 응답 시간을 계산하려면 다음과 같이 입력하면 된다:
$ awk '/GET/ { total += $6; n++ } END { print total/n }' server.log
0.0186667이는 다음과 같은 의미다. 정규식 /GET/과 일치하는 모든 행에 대해 응답 시간(여섯 번째 필드, 즉 $6)을 합산하고 해당 행 수를 센 뒤, 마지막에 응답 시간의 산술 평균을 출력한다.
다양한 AWK 버전
현재 널리 쓰이는 AWK에는 세 가지 주요 버전이 있으며, 모두 POSIX 표준을 (적어도 대다수 사용 사례에서는 충분히 가깝게) 준수한다. 첫 번째는 클래식 awk로, Aho, Weinberger, Kernighan이 저서 The AWK Programming Language에서 설명한 AWK다. 흔히 “new AWK”(nawk) 또는 “one true AWK”라고도 불리며 현재는 GitHub에서 관리된다. 이 버전은 macOS를 포함한 많은 BSD 계열 시스템에 기본으로 설치되어 있다(다만 macOS에 포함된 버전은 오래되었으므로 업그레이드할 가치가 있다).
두 번째는 GNU Awk (gawk)로, 단연 가장 기능이 풍부하고 활발히 유지보수되는 버전이다. Gawk는 보통 리눅스 시스템에 기본으로 설치되어 있으며 대개 기본 awk로 동작한다. macOS에서는 Homebrew를 이용해 쉽게 설치할 수 있고 Windows 바이너리도 제공된다. Arnold Robbins는 1994년부터 gawk의 메인테이너를 맡아 지금까지 언어를 이끌어 오고 있으며(클래식 awk 버전에도 많은 수정을 기여했다). Gawk에는 awk나 POSIX 표준에는 없는 많은 기능이 있는데, 새로운 함수, 네트워킹 기능, C 확장 API, 프로파일러와 디버거, 그리고 가장 최근에는 네임스페이스가 포함된다.
세 번째로 흔한 버전은 mawk로, Michael Brennan이 작성했다. Ubuntu와 Debian 리눅스에서 기본 awk로 쓰이며, 바이트코드 컴파일러와 더 메모리 효율적인 값 표현 덕분에 여전히 가장 빠른 AWK 버전이다. (Gawk 역시 4.0부터 바이트코드 컴파일러를 갖추어 이제는 속도가 mawk에 훨씬 가까워졌다.)
원라이너나 기본적인 텍스트 처리를 위해 AWK를 쓰려는 경우라면 위 중 어느 것을 써도 무방하다. 더 큰 스크립트나 프로그램을 작성할 생각이라면 Gawk의 기능 덕분에 Gawk가 합리적인 선택이다.
그 밖에도 성숙도와 유지보수 수준이 각기 다른 여러 AWK 구현체가 존재한다. 대표적으로 임베디드 리눅스 환경에서 쓰이는 크기 최적화 BusyBox 버전, 런타임에 Java 언어 기능에 접근할 수 있는 Java 재작성 버전, 그리고 필자가 직접 만든 Go로 작성된 POSIX 호환 버전인 GoAWK가 있다. 세 가지 주요 AWK와 BusyBox 버전은 모두 C로 작성됐다.
4.0 이후 Gawk의 변화
LWN이 gawk 4.0 출시를 다룬 지 거의 10년이 지났다. “2011년 이후 많은 것이 바뀌었다”고 말하고 싶겠지만, 사실 AWK 세계에서는 변화가 비교적 천천히 진행된다. 여기서는 4.0 이후 주목할 만한 기능들을 설명하겠지만, 더 자세한 내용은 전체 4.x 및 5.x 변경 로그를 참고하면 된다. Gawk 5.1.0은 한 달여 전인 4월 14일에 출시됐다.
사용자가 체감할 수 있는 가장 큰 기능은 5.0에서 도입된 네임스페이스다. 대부분의 현대 언어에는 대규모 프로젝트나 라이브러리를 이름 충돌 없이 배포하기 쉽게 하려는 네임스페이스 개념이 있다. Gawk 5.0은 하위 호환성을 유지하면서 네임스페이스를 추가해, 개발자가 다음과 같은 간단한 수학 라이브러리 같은 라이브러리를 만들 수 있게 했다:
# area.awk
@namespace "area"
BEGIN {
pi = 3.14159 # namespaced "constant"
}
function circle(radius) {
return pi*radius*radius
}라이브러리 안의 변수나 함수를 참조하려면 C++와 유사한 namespace::name 구문을 사용한다:
$ gawk -f area.awk -e 'BEGIN { print area::pi, area::circle(10) }'
3.14159 314.159Robbins는 AWK에 네임스페이스가 없었던 것이 AWK가 대규모 프로그래밍 언어로 자리 잡지 못한 핵심 이유 중 하나이며, gawk 5.0의 이 기능이 그 문제를 해소하는 데 도움이 될 수 있다고 믿는다. Robbins가 AWK의 발목을 잡는 또 다른 큰 문제로 꼽는 것은 제대로 된 C 확장 인터페이스의 부재다. Gawk의 동적 확장 인터페이스는 4.1에서 완전히 개편됐으며, 이제 정의된 API를 갖추고 기존 C 및 C++ 라이브러리를 래핑해 AWK에서 쉽게 호출할 수 있게 한다.
사용 설명서에 있는 예제 C 코드 래퍼의 다음 스니펫은 AWK 배열(문자열을 키로 하는 해시 테이블)에 파일 이름과 stat() 시스템 호출에서 얻은 값들을 채운다:
/* empty out the array */
clear_array(array);
/* fill in the array */
array_set(array, "name", make_const_string(name, strlen(name), &tmp));
array_set_numeric(array, "dev", sbuf->st_dev);
array_set_numeric(array, "ino", sbuf->st_ino);
array_set_numeric(array, "mode", sbuf->st_mode);4.2 릴리스(그리고 5.0까지 이어진)에서 또 다른 변화는 소스 코드 프리티 프린터의 전면 개편이었다. Gawk의 프리티 프린터 덕분에 Go의 go fmt 도구나 Python의 Black 포매터처럼 표준화된 AWK 코드 포매터로 활용할 수 있다. 예를 들어 위의 area.awk 파일을 프리티 프린트하려면 다음과 같이 실행한다:
$ gawk --pretty-print -f area.awk
그 결과는 다음과 같다:
@namespace "area"
BEGIN {
pi = 3.14159 # namespaced "constant"
}
function circle(radius)
{
return (pi * radius * radius)
}도구의 선택에 의문을 가질 수도 있다. 왜 “BEGIN {”는 function과 달리 “{” 앞에 줄바꿈을 하지 않는지?(알고 보면 AWK 문법상 허용되지 않기 때문이다.) 왜 함수 앞에 빈 줄이 두 줄 들어가고 return 식을 괄호로 감싸는지? 하지만 적어도 일관성은 있으며 코드 스타일 논쟁을 피하는 데 도움이 될 수도 있다.
Gawk는 제한적인 런타임 타입 검사를 허용하며, 4.2에서 typeof() 함수가 추가되면서 그 기능이 확장됐다. typeof()는 입력 타입에 따라 “string”, “number”, “array” 같은 문자열 상수를 반환한다. 이러한 함수들은 예컨대 중첩 배열의 모든 항목을 재귀적으로 순회하는 코드에 중요하다(POSIX AWK로는 할 수 없는 일이다).
4.2부터 gawk는 정규식 상수를 @/foo/ 구문을 이용해 일급 데이터 타입으로 지원한다. 이전에는 정규식 상수를 변수에 저장할 수 없었으며, typeof(@/foo/)는 “regexp”라는 문자열을 반환한다. 성능 측면에서는 gawk 4.2가 리눅스 시스템에서 가능한 경우 fwrite_unlocked()을 사용함으로써 큰 개선을 이뤘다. gawk는 싱글 스레드로 동작하므로 논락킹 stdio 함수를 사용할 수 있어 원시 출력 속도가 7~18% 향상된다 — 예를 들어 대용량 파일에 대해 gawk '{ print }'를 실행할 때가 그렇다.
GNU Awk 사용자 안내서는 원래부터 충실한 레퍼런스였지만, 4.1과 다시 5.x 릴리스에서 새로운 예제, 요약 섹션, 연습 문제, 그리고 대대적인 교정을 포함해 대폭 업데이트됐다.
마지막으로(그리고 가장 사소하게) 4.0에서 내가 재미있다고 느낀 미묘한 변화는 sub()과 gsub()에서 백슬래시 처리 방식이 되돌려진 것이다. Robbins는 이렇게 쓴다:
sub()과 gsub()에서 백슬래시의 기본 처리 방식이 3.1 버전의 동작으로 되돌려졌다. 표준 준수를 위해서라도 그런 식으로 호환성을 깨뜨릴 수 있다고 생각한 것은 어리석은 일이었다.
sub과 gsub 함수는 핵심 정규식 치환 함수이며, 복잡한 백슬래시 처리에 대한 작은 “수정”조차 사람들의 코드를 망가뜨렸다:
버전 4.0.0이 출시됐을 때 gawk 메인테이너는 POSIX 규칙을 기본값으로 만들었고, 이는 10년이 넘는 하위 호환성을 깨뜨렸다. 말할 것도 없이 이는 잘못된 생각이었으며, 버전 4.0.1부터 gawk는 종전 동작으로 돌아가 --posix가 주어졌을 때만 POSIX 규칙을 따른다.
Robbins는 원래 변경에서 판단 착오가 있었을지 모르지만, 그가 하위 호환성을 진지하게 여긴다는 것은 분명하다. 특히 gawk처럼 널리 쓰이는 도구에서는 오랫동안 작동해 온 방식을 바꾸는 것보다 차라리 명세를 계속 위반하는 편이 나을 때도 있다.
AWK는 여전히 유효한가?
AWK가 여전히 유효한지 묻는 것은 공기가 여전히 유효한지 묻는 것과 비슷하다. 눈에 보이지는 않아도 어디에나 있기 때문이다. 많은 리눅스 관리자와 DevOps 엔지니어는 데이터를 가공하거나 로그 파일을 통해 문제를 진단하는 데 AWK를 사용한다. 거의 모든 유닉스 기반 머신에는 AWK의 한 버전이 설치되어 있다. 일회성 사용 외에도 많은 대형 오픈소스 프로젝트가 빌드나 문서 도구 어딘가에서 AWK를 활용한다. 몇 가지 예만 들자면 리눅스 커널은 x86 툴링에서 objdump 파일을 검사하고 재구성하는 데 AWK를 쓰고, Neovim은 문서 생성에, FFmpeg는 빌드와 테스트에 사용한다.
AWK 빌드 스크립트는 사람들이 없애려 해도 놀라울 정도로 끈질기다. 2018년 LWN은 GCC 기여자 중 일부가 옵션 파싱 코드를 생성하는 스크립트에서 AWK를 Python으로 교체하려 한다는 내용을 다뤘다. 당시 이 제안에 대한 지지가 일부 있었지만, 분명 실제 포팅 작업을 자청한 사람은 아무도 없었고 AWK 스크립트는 지금도 남아 있다.
Robbins는 2018년 논문에서 AWK(특히 gawk)를 “시스템 프로그래밍 언어”, 즉 더 큰 도구와 프로그램을 작성하기 위한 언어로서 활용하자고 주장한다. 그는 AWK가 널리 쓰이지 못한 이유를 설명하지만, Kernighan은 확장 메커니즘의 부재가 더 큰 프로그램에 AWK가 널리 쓰이지 않는 주된 이유라는 데 “100% 확신하지는 않는다”. 그는 시스템 호출 등에 대한 접근을 지원하는 기능이 내장되어 있지 않기 때문일 수도 있다고 제안했다. 하지만 그럼에도 여러 사람이 더 큰 도구들을 만드는 것을 멈추지 않았다. Robbins 자신의 1,300줄짜리 문학적 프로그래밍 도구인 TexiWeb Jr., Werner Stoop의 소스 코드 속 Markdown 주석으로부터 문서를 생성하는 800줄짜리 도구인 d.awk, 그리고 클라우드 기반 번역 API에 대한 꽤 강력한 커맨드라인 인터페이스를 제공하는 6,000줄짜리 AWK 도구인 Translate Shell이 그 예다.
지난 몇 년간 여러 개발자가 Spark나 Hadoop 같은 무거운 분산 컴퓨팅 시스템보다 훨씬 간단하고(때로는 더 빠른) 도구로서 AWK를 자신들의 “빅데이터” 툴킷에 활용하는 것에 대해 글을 썼다. Nick Strayer는 여러 코어에 걸쳐 AWK와 R을 사용해 25테라바이트의 데이터를 파싱한 경험을 썼다. 다른 빅데이터 사례로는 Adam Drake의 도발적인 제목의 글 “Command-line Tools can be 235x Faster than your Hadoop Cluster”와 Brendan O’Connor의 “Don’t MAWK AWK – the fastest and most elegant big data munging language!”가 있다.
일회성 텍스트 가공부터 빌드 툴링, “시스템 프로그래밍”, 빅데이터 처리에 더해 텍스트 모드 1인칭 슈팅 게임까지 — AWK는 2020년에도 건재한 것으로 보인다.
[이 글의 초안을 검토해 준 Arnold Robbins에게 감사한다.]
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기