...
AWK (Aho Weinberger Kernighan) 개념
AWK는 텍스트가 저장되어 있는 파일을 원하는 대로 필터링하거나 추가해주거나 기타 가공을 통해서 나온 결과를 행과 열로 출력해주는 프로그램 이다. 좀더 쉽게 말하자면, awk는 "awk programming language"라는 프로그래밍 언어로 작성된 프로그램을 실행 하는 명령어라고 이해하면 좋다.
즉, 리눅스에서 쉘 스크립트(Shell Script)로 작성된 파일이 리눅스 쉘(Shell)에 의해 실행되는 것처럼, awk가 "awk programming language" 문법으로 작성된 코드를 이해하고 실행 한다는 의미로 보면 된다.
명령어 예를 들자면, 다음과 같이 a.txt 파일이 있다고 하자. 파일내용은 다음과 같다.
1 2
3 4
만일 1열의 데이터와 2열의 데이터를 각각 곱한 값을 얻는 로직을 ( 1*2=2 / 3*4=12 ) awk 언어로 작성된 awk명령어를 통해 구현한다고 하면
$ cat a.txt | awk '{print $1 * $2;}'
이런식으로 따로 쉘스크립트 작성없이 단 한줄로 스크립트 동작을 구현 할 수 있다.
awk는 파일로부터 레코드(record)를 선택하고, 선택된 레코드에 포함된 값을 조작하거나 데이터화 한다.
위와 같은 텍스트 파일 내용이 있다면, 여기서 각 단어들은 공백으로 구분되어져 있다. 그리고 여기서 각 줄(line)은 레코드(Record)라고 칭하고 그 안에 각각의 단어들이 필드(Field)라고 칭해진다. (데이터베이스 테이블 과 같다.)
AWK 프로그래밍 문법에서는 레코드가 $0, 그리고 $1, ..., $N은 각 필드 인자를 나타내게 프로그래밍 되어 있다.
따라서, 위에서 예시를 든 코드를 다시 보자면, 이런식으로 해석이 된다.
$0는 한 행을 의미하고, $1 는 그 행의 첫번째 필드, $2 는 두번째 필드가 되는 것이다.
간단하게 awk가 제공해주는 동작 원리를 살펴보았다.
정리하자면, awk는 명령의 입력으로 지정된 파일로부터 데이터를 분류한 다음, 분류된 텍스트 데이터를 바탕으로 패턴 매칭 여부를 검사하거나 데이터 조작 및 연산 등의 액션을 수행하고, 그 결과를 출력하는 기능을 수행하는 것이다.
마치 리눅스의 SQL문이라고 생각해도 된다.
행(레코드)과 열(필드)로 이루어진 테이블 형태의 FROM 데이터들 중에서 출력하고 싶은 열(필드)만 SELECT하고 WHERE이나 GROUP BY를 통해 데이터들을 합치거나 일부만 뽑아 내어 화면에 출력하는 것이 sql문인데, 위의 awk 과정이 바로 이것과 다름이 없기 때문이다. 거기다 SQL의 스토어드 프로시저나 함수처럼 awk역시 조건문, 반복문 코딩이 가능하다.
awk 명령으로 출력할 수 있는 일들을 간단히 나열해 보면 다음과 같다. 지금은 아직 세세한 문법은 모르지만 눈에 익혀두자.
AWK 명령어 문법
$ awk [옵션] 'pattern { action }' [파일|변수값]
awk 옵션 | 설명 |
-u | 버퍼를 사용하지 않고 출력한다. |
-F | 확장된 정규 표현식으로 필드구분자를 지정한다, 다중 필드 구분자 사용 가능하다. |
awk -F | 단일로 사용시 ':' 를 필드구분자로 사용 |
awk -F'[ :\t]' | 다중 필드구분자 ':'와 tab을 필드구분자로 사용 |
-v | 스크립트를 실행하기 전에 미리 변수를 지정하여 준다. |
-f | awk 명령 스크립트를 파일에서 읽어온다. |
awk 동작 원리
패턴(pattern) 과 액션(action)
- awk는 파일 또는 파이프를 통해 입력 라인을 얻어와 $0라는 내부 변수에 라인을 입력한다.
각 라인은 레코드라고 부르고, newline(개행)에 의해 구분되다.
이때 패턴이 없으면 전체 라인을 얻어오고, 원하는 라인만 얻어오고 싶을때는 패턴을 사용해 분별할 수 있다. - awk를 실행할때 내장 변수인 FS라고 부르는 필드 분리자가 공백을 할당 받는다. (필드 분리 기준을 공백이 아닌 다른 값으로 바꿀수도 있다)
그러면 awk는 라인을 공백을 기준으로 각각의 필드나 단어로 나눈다.
필드는 $1부터 시작해서 많게는 $100 이상의 변수에 저장할 수 있다. - 각 필드 데이터들을 저장했다면 awk는 액션을 통해 동작 스크립팅을 할 수 있다.
예를 들어, 필드들을 화면에 출력할 때 print 함수를 사용하면 된다.
# -F : 필드 구분 문자를 공백 말고 ":" 로 설정
# pattern /linux/ : linux 문자열을 포함한 모든 레코드 출력
# action {print $1} : 각 행(레코드)에서 첫번째 필드를 출력
$ awk -F":" '/linux/ {print $1}' test.txxt
pattern 과 { action }은 반드시 명령어에 모두 써줘야 되는 건 아니다.
아래와 같이 pattern이 생략되는 경우,
매칭 여부를 검사할 문자열 패턴 정보가 없기 때문에 모든 레코드가 선택되는 거고,
# pattern 생략.
$ awk '{ print }' ./file.txt # file.txt의 모든 레코드 출력.
action을 생략하면, 기본 액션인 print가 실행된다.
# action 생략.
$ awk '/p/' ./file.txt # file.txt에서 p를 포함하는 레코드 출력.
awk 명령어는 단일로 file을 참조해서 실행해도 되고,
다른 명령어와 파이프와 조합하여도 사용할 수 있다.
# 파일에서 입력을 받아들일 때 #
$ awk 'pattern' filename
$ awk '{ action }' filename
$ awk 'pattern { action }' filename
# 커맨드에서 입력을 받아들 때 파이프와 조합하여 사용 #
$ command | awk 'pattern'
$ command | awk '{ action }'
$ command | awk 'pattern { action }'
awk 패턴 종류
비교연산 패턴
다시 정리하자면, 패턴은 각 라인(행) 들 중에, 내가 원하는 부분만 뽑아내고 싶을때 사용한다.
가장 간단하게 사용할 수 있는게 비교 연산 패턴인데, 마치 조건문 처럼 해당 조건에 부합한 데이터 라인들을 뽑아 낸다.
숫자, 알파벳 모두 비교 연산이 가능 하다.
# 파일에서 3번째 필드 $3의 값이 7000보다 클경우 나머지 필드들을 print한다.
$ awk '$3 > 7000 {print $1, $2}' filename
# 복잡한 논리식 여깃 사용 가능
$ awk '$3 > $5 && $3 <= 100' filename
정규표현식 패턴
grep 명령어와 같이 패턴 부분에 정규식 /regex/ 를 넣어서 라인을 분별 할 수도 있다.
# 대소문자 구분없는 알파벳으로 시작하고 뒤에 어느 한 문자가 오는 라인 매칭
$ awk '/^[A-Z][a-z]+ /' filename
# "정" 자로 시작하는 라인을 골라서 print
$ awk '/^정/{print $1, $2, $3}' filename
# POSIX 문자 클래스
$ awk '/[[:lower:]]+g[[:space:]]+[[:digit:]]' filename
정규식과 POSIX 가 무엇인지 모르겠다면, 해당 포스팅을 참고하길 바란다.
패턴 매칭 연산
match 연산자(~) : 표현식과 매칭되는 것이 있는지 검사하는 연산자
- ~ 일치하는 부분
- !~ 일치하지 않는 부분
새로운 문법이지만, 이렇게 이해하면 간단하다. 보통 값을 비교할때, == 연산자를 쓰는데, 패턴매칭을 비교하기 위해선 == 대신 match연산자(~)를 쓰는 것이다.
# 2번 필드 문자열이 문자 g로 끝나지 않는 라인 출력
$ awk '$2 !~ /g$/' filename
BEGIN / END 패턴
awk 패턴(pattern) 중에는 "BEGIN" 과 "END" 라고 하는 특별한 패턴이 존재한다.
awk가 BEGIN 패턴을 식별하면 입력 데이터로부터 첫 번째 레코드를 처리하기 전에 "BEGIN"에 지정된 액션을 실행한다. 그리고 "END" 패턴은 모든 레코드를 처리한 다음 "END"에 지정된 액션을 실행하는 원리이다.
정말 알기쉽게 예제로 그림으로 표현하자면 다음과 같다.
[file.txt 파일 내용]
1 2
3 4
5 6
7 8
$ awk 'BEGIN { print "TITLE : Field value 1,2"} {print $1, $2} END {print "Finished"}' file.txt
awk 제어문
문법 자체는 C언어의 제어문과 비슷하다.
# if문
if ( condition ) { Routine } else { Routine }
# for문
for ( init ; condition ; re ) { Routine }
# while문
while (condition) { Routine }
# do ~ while문
do { Routine } while (condition)
# 반목문 제어
break
continue
return
# 프로그램 제어
next
exit
if문
# 단일 if
$ awk '{if($6>50) print $1 "Too high" }' filename
$ awk '{if($6>20 && $6<=50) {safe++; print "OK"}}' filename
# if ~ else
$ awk '{if($6>50) print $1 "Too high"; else print "Range is OK"}' filename
$ awk '{if($6>50) {count++; print $3} else{x+5; print $2}}' filename
한줄로 표현하다보니 굉장히 가독성이 안좋다.
다행히 awk는 개행 문법을 지원한다.
다음과 같은 파일내용이 있다고 하자.
각 라인에서 첫번째 필드 $1가 "d" 로 시작하면, 즉 디렉토리라면 출력하고 아니면 null를 출력하는 간단한 if문 예제 이다.
$ awk '{
if ($3 >=35 && $4 >= 35 && $5 >= 35)
print $0,"=>","Pass";
else
print $0,"=>","Fail";
}' filename
$ awk \ # \문자를 써도 터미널에서 개행이 가능하다.
'
{
if ($3 >=35 && $4 >= 35 && $5 >= 35)
print $0,"=>","Pass";
else
print $0,"=>","Fail";
}
'
# if elif else
$ awk '{
total=$3+$4+$5;
avg=total/3;
if ( avg >= 90 )
grade="A";
else if ( avg >= 80)
grade ="B";
else if (avg >= 70)
grade ="C";
else
grade="D";
print $0, grade;
}' filename
# 삼항 연산자 역시 사용이 가능하다
$ awk '{max={$1 > $2) ? $1 : $2; print max}' filename
for문
$ awk '{for(i=1; i<=NF; i++) print NF, $1}' filename
$ awk '{
for(i=0;i<2;i++)
print( "for loop :" i "\t" $1, $2, $3)
}' filename
while문
$ awk '{i=1; while(i<=NF) {print NF, $1; i++}}' filename
반복문 제어
- break : 조건을 만족하면 반복문을 탈출
- continue : 남아있는 문장을 실행하지 않고, 바로 반복문의 시작 부분으로 제어를 이동
$ awk '{
for(x=3; x<=NF; x++)
if($x<0) {print "Bottomed out!"; break}
for(x=3; x<=NF; x++)
if($x==0) {print "Get next item"; continue}
}' filename
프로그램 제어
- next : 입력 파일로부터 다음 입력행을 가져와서 awk 스크립트의 맨 처음부터 다시 실행
- exit : awk 프로그램을 종료시킬 때 사용. exit 문은 레코드의 처리를 중단시키지만, END 문 너머로 건너뛰지 않음
# 1번째 필드가 Peter를 포함하면 이 행을 지나고 다음행을 입력받고, 스크립트는 처음부터 다시 실행
$ awk '{
if($1~/Peter/) {next}
else {print}
}' filename
$ cat file.txt
1 ppotta 30 40 50
2 soft 60 70 80
3 prog 90 10 20
$ awk '{ print $0; exit }' file.txt # 첫 번째 레코드만 출력하고 실행 중지.
1 ppotta 30 40 50
$ awk 'NR == 2 { print $0; exit }' file.txt # 두 번째 레코드만 출력하고 실행 중지.
2 soft 60 70 80
NR은 출력 순번을 나타내는 awk 정용 내장변수이다.
awk 사용자 변수
여타 프로그래밍 언어와 같이 변수를 만들어서 사용할 수 있다.
$ awk '
BEGIN {
sum = 0
cnt = -1
}
{
sum += $5
cnt++
}
END {
avg = sum/cnt
print ("sum :" sum ", average :" avg)
}' filename
awk 리다이렉션
awk결과를 리눅스 파일로 리다이렉션할 경우 쉘 리다이렉션 연산자를 사용한다.
다만 > 기호가 논리연산자인지 리다이렉션인지 모호할때가 있는데, 그냥 action 부분에 > 기호가 쓰이면 리다이렉션, pattern 부분에 쓰이면 논리 연산이라고 치부하면 된다.
파일명은 큰따옴표로 둘러쌰아 인식 된다.
$ awk -F: '$4 >= 60000 {print $1, $2 > "new_file"}' awkfile5
.awk 파일로 만들어서 실행
쉘 스크립트를 .sh 파일로 만들어서 실행할 수 있듯이, awk역시 가능하다. -f 명령어 옵션을 사용하면 .awk 확장자 파일을 실행 할 수 있다.
문법은 다음과 같다. 쉘스크립트에서 #!/bin/bash와 같이 awk 전용 shabang을 작성해주고, 지금까지 배운 문법을 그대로 적어주기면 하면 된다. (단, 참조할 파일은 적지 않는다. 파일은 터미널 CLI명령으로 적는다)
#!/bin/awk -f
BEGIN { print "File\tOwner" }
{ print $8, "\t", $3}
END { print " - DONE -" }
# 실행방식 : awk [-f옵션] [awk스크립트파일] [참조할파일]
$ awk -f script.awk filename
awk 연산자
연산자 | 설명 |
산 술 | =, +=, -=, *=, /=, %= |
조 건 | ?, : |
논 리 | ||, &&, ! |
패 턴 | ~, !~ |
비 교 | <, <=, >, >=, !=, == |
증 감 | ++, -- |
필드참조 | $ |
awk 내장변수
키워드 | 설명 |
FILENAME | 현재 입력파일의 이름 |
$0 | 입력 레코드 |
$n | 입력 레코드의 N번째 필드 |
ENVIRON | 환경변수를 모아둔 관계형 배열 |
NR | 출력 순번 |
NF | 현재 줄의 필드수 |
ARGC | 명령줄 인자의 개수 |
ARGV | 명령줄 인자들의 배열 |
FNR | 현재파일에서의 레코드 번호 |
FS | 입력 필드 구분자 |
OFMT | 숫자들의 표현형식 |
OFS | 출력필드 구분자 |
ORS | 출력 레코드 구분자 |
RS | 입력코드 구분자 |
EP | 서브스크립트의 구분자 |
RLENGTH | match 함수로 일치하는 문자열의 길이 |
RSTART | match 함수로 일치하는 문자열의 오프셋 |
awk 내장함수
$ awk '{ print ("name leng : " length($1), "substr(0,3) : " substr($1,0,3)) }' filename
awk 문자열 함수
함수명 | 설명 |
gsub(r, s) | 입력 문자열 전부에 걸쳐 정규식표현 r을 문자열 s로 치환한다. |
gsub(r, s1, s2) | 입력문자열 s2 에서 정규식 표현 r을 문자열 s1 으로 치환한다. |
index(s1, s2) | s1에서 s2의 위치를 넘겨준다. 없다면 0 |
length(arg) | 인자의 길이를 넘겨준다. |
match(s, r) | 문자열 s에서 정규식표현 r과 매칭 되는 부분의 위치를 넘겨준다. |
split(string, array[, seperator]) | 구분자를 기준으로(기본:공백)해서 지정한 문자열을 배열로 만든다. |
sub(r, s) , sub(r, s1, s2) | gsub 와 동일 정규식식표현과 일치하는 문자열이 여러개라도 처음 한 문자열만 치환 |
substr(s, m) | 문자열 s 에서 m번째 위치에서 끝까지 문자열을 넘겨준다. |
substr(s, m, n) | 문자열 s 에서 m번째 부터 n번째까지의 문자열을 넘겨준다 |
tolower(string) | 대문자를 소문자로 바꾼다. |
toupper(string) | 소문자를 대분자로 바꾼다. |
awk 수학 함수
함수명 | 설명 |
atan2(x, y) | archtangent 값 |
cos(x) | cos 값 |
exp(x) | 자연대수 e의 제곱 |
int(x) | 정수형으로 반환 |
log(x) | 로그 |
rand() | 0에서 1까지의 랜덤수 |
sin(x) | sin 값 |
sqrt(x) | 제곱근 |
srand(expr) | 인자를 가진 난수(없으면 시간을 가지고 난수 발생) |
awk 입출력 함수
함수명 | 설명 |
close(filename) | 지정한 파일을 닫는다. |
close(cmd) | 지정한 명령어 파이프를 닫는다. |
delete array[element] | 지정한 배열요소를 지운다. |
getline() | 다음 레코드를 읽어 들인다. |
getline [variable] [< "filename"] | 파일에서 읽어드린다. |
next | 다음 레코드를 입력받는다. |
print [args] [> "filename"] | 인자를 출력한다 |
printf "format" [,exp] [> "filename"] | 형식에 맞춰 인자를 출력한다. |
sprintf(format [,exp]) | printf와 마찬가지로 사용되지만 값을 리턴하기만 하고 출력은 하지 않는다. |
system(cmd) | 시스템 내부 명령어를 실행한다. |
AWK 명령 사용 예제
필드 값 출력 하기 ($숫자)
$ cat file.txt
1 ppotta 30 40 50
2 soft 60 70 80
3 prog 90 10 20
$ awk '{ print $1,$2 }' ./file.txt # 첫 번째, 두 번째 필드 값 출력.
1 ppotta
2 soft
3 prog
$ awk '{ print $0 }' ./file.txt # 레코드 출력.
1 ppotta 30 40 50
2 soft 60 70 80
3 prog 90 10 20
$ cat file.txt
1 ppotta 30 40 50
2 soft 60 70 80
3 prog 90 10 20
$ awk '{print "no:"$1, "user:"$2}' ./file.txt # 필드 값에 문자열을 함께 출력
no:1 user:ppotta
no:2 user:soft
no:3 user:prog
패턴 이용 (정규식 / 비교식)
$ cat file.txt
1 ppotta 30 40 50
2 soft 60 70 80
3 prog 90 10 20
$ awk '/pp/' ./file.txt # "pp" 가 포함된 레코드만 유효.
1 ppotta 30 40 50
$ awk '/[2-3]0/' ./file.txt # 20, 30 이 포함된 레코드만 유효.
1 ppotta 30 40 50
3 prog 90 10 20
$ cat file.txt
1 ppotta 30 40 50
2 soft 60 70 80
3 prog 90 10 20
$ awk '$1 == 2 { print $2 }' ./file.txt # 첫 번째 필드가 2인 레코드의 두 번째 필드 출력.
soft
$ awk '$3 > 70 { print $0 }' ./file.txt # 세 번째 필드가 70보다 큰 레코드 출력.
3 prog 90 10 20
$ awk '$3 == 30 && $4 ==40 { print $2 }' file.txt # 세 번째 필드가 30이고 네 번째 필드가 40인 레코드의 두 번째 필드 출력.
ppotta
$ cat str.txt
a b c
d e f
g h i
$ cat c.txt | awk '$1=="a"' str.txt # 특정 레코드만 출력
a b c
$ cat c.txt | awk '$1!="a"' str.txt # 특정 레코드 제외 출력
d e f
g h i
$ cat c.txt | awk '$2~/^e/' str.txt # 패턴비교. $2필드가 문자e 로 시작하는 레코드만 출력
d e f
CSV파일 처리하기 (-F 옵션)
쉼표로 구분된 값을 처리하기 위해서는 -F 옵션에서 구분 기호를 재지정 하면 된다.
$ cat test.csv
1,2,3
4,5,6
7,8,9
$ cat b.txt | awk -F, '{print $1}' # -F옵션을 통해 , 로 구분기호를 재지정하고 $1필드만 출력
1
4
7
행 번호 출력하기 (NR 내장변수)
행 번호를 출력하기 위해서는 print 에 NR 옵션을 추가한다.
- NR은 awk 내장변수로서 몇번째 레코드인지 순번 출력한다
- $0 은 레코드 한 줄 출력
$ cat test.csv
1,2,3
4,5,6
7,8,9
$ cat test.csv | awk -F, '{print NR " " $0;}'
1 1,2,3
2 4,5,6
3 7,8,9
$ awk_example]$ cat test.csv | awk -F, '{print NR-1 " " $0;}' # 0부터 시작
0 1,2,3
1 4,5,6
2 7,8,9
산술 계산
$ cat num.txt
100
200
300
400
500
$ awk '{sum+=$1} END {print sum}' num.txt # 합계 계산. END패턴을 통해 레코드를 모두 돌도 난 후 마지막에 합을 출력
1500
$ awk '{sum+=$1} END {print sum/NR}' num.txt # 평균 계산. 내장변수 NR은 출력순번값. 즉 레코드 갯수를 의미
300
## NR==1 {max=$1} 이라는 뜻은 최대/최소를 구하기 위해 초깃값을 설정하는 로직. 출력순번이 첫번째 일떄만 max변수에 $1값 저장
$ awk 'NR==1 {max=$1} {if($1 > max) max = $1} END {print max}' num.txt # 최댓값 계산
500
$ awk 'NR==1 {min=$1} {if($1 < max) min = $1} END {print min}' d.txt # 최솟값 계산
100
$ cat file.txt
1 ppotta 30 40 50
2 soft 60 70 80
3 prog 90 10 20
$ awk '{ sum = 0 } {sum += ($3+$4+$5) } { print $0, sum, sum/NR }' ./file.txt # 합계와 평균 구하기
1 ppotta 30 40 50 120 40
2 soft 60 70 80 210 70
3 prog 90 10 20 120 40
printf 포맷팅
C언어의 printf() 함수에는 포맷지정자가 있는데, 이를통해 출력 칸 수 너비를 조정할 수 있다.
awk에서도 똑같이 사용이 가능하다.
$ cat file.txt
1 ppotta 30 40 50
2 soft 60 70 80
3 prog 90 10 20
$ awk '{ printf "%-3s %-8s %-4s %-4s %-4s\n", $1, $2, $3, $4, $5}' file.txt
1 ppotta 30 40 50
2 soft 60 70 80
3 prog 90 10 20
조건문
$ cat file.txt
name phone birth sex score
reakwon 010-1234-1234 1981-01-01 M 100
sim 010-4321-4321 1999-09-09 F 88
$ awk '{ if ( $5 >= 80 ) print ($0) }' file.txt # score가 80점 이상 레코드만 출력
$ awk '$5 >= 80 { print $0 }' ./awk_test_file.txt # action부분에 if를 쓰든 pattern부분에 비교연산자를 쓰든 결과는 동일
루프문
NF : awk 내장변수. 현재 줄의 필드 갯수
$ cat file.txt
1 ppotta 30 40 50
2 soft 60 70 80
3 prog 90 10 20
$ awk '{ for (i=3; i<=NF; i++) total += $i }; END { print "TOTAL : "total }' ./file.txt # 각 레코드의 $3 ~ $5 필드 총합
TOTAL : 450
내장함수 사용
$ cat e.txt
a
ab
abc
abcd
abcde
$ cat e.txt | awk '{print length();}' # 각 행의 문자수 구하기
1
2
3
4
5
파이프 조합
$ cat file.txt
1 ppotta 30 40 50
2 soft 60 70 80
3 prog 90 10 20
$ awk '{ print $0 }' file.txt | sort -r # awk 출력 레코드를 파이프로 보내 역순으로 정렬.
3 prog 90 10 20
2 soft 60 70 80
1 ppotta 30 40 50
# 참고자료
https://zzsza.github.io/development/2017/12/20/linux-6/
https://reakwon.tistory.com/163
https://recipes4dev.tistory.com/171
http://egloos.zum.com/bosoa/v/3932702
https://blog.muabow.com/185
https://techgoeasy.com/unix-awk-command/
https://jhnyang.tistory.com/494
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.