Profile picture

[Linux] AWK 명령어 문법 살펴보기

JaehyoJJAng2023년 03월 15일

▶︎ AWK

  • Aho Weinberger Kernighan

AWK는 텍스트가 저장되어 있는 파일을 원하는 대로 필터링하거나 추가하거나 기타 가공을 통해서 나온 결과를 행과 열로 출력해주는 프로그램이다. 쉽게 말하자면 awk는 awk programming language 라는 프로그래밍 언어로 작성된 프로그램을 실행 하는 명령어라고 이해하면 좋다.

즉, 리눅스에서 쉘 스크립트(Shell Script)로 작성된 파일이 리눅스 쉘(Shell)에 의해 실행되는 것처럼, awk가 "awk programming language" 문법으로 작성된 코드를 이해하고 실행 한다는 의미로 보면 된다.


예를 들어 확인해보자. 다음과 같이 test.txt 파일이 있다고 하자. 파일 내용은 아래와 같다

1 2
3 4

만일 1열의 데이터와 2열의 데이터를 각각 곱한 값을 얻는 로직을 (1 * 2 = 2 / 3 * 4 = 12) awk 언어로 작성된 awk 명령어를 통해 구현한다고 하면 아래와 같이 작성할 수 있다

$ cat test.txt | awk '{print $1 * $2;}'
$ cat test.txt
2
12

awk는 파일로부터 레코드(record)를 선택하고, 선택된 레코드에 포함된 값을 조작하거나 데이터화 한다.

file: person.txt

name phone birth sex score
john 010-1234-5678 1988-01-01 M 100
anna 010-3333-4444 1989-01-01 F 90

위와 같은 텍스트 파일이 있을 때, 여기서 각 단어들은 공백으로 이루어져 있다 그리고 여기서 각 줄(line)은 레코드(Record)라고 칭하고 그 안에 각각의 단어들이 필드(Field)라고 칭해진다.

AWK 프로그래밍 문법에서는 레코드가 $0, 그리고 $1, ... $N은 각 필드 인자를 나타내게 프로그래밍 되어있다.

위에서 예시를 든 코드를 다시 보자면, 이런식 으로 해석이 된다.
$0는 한 행을 의미하고 $1는 그 행의 첫 번째 필드, $2는 두 번째 필드가 되는 것이다.

$ cat test.txt | awk '{print $0 " | " $1 * $2;}'
1 2 | 2
3 4 | 12

간단하게 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/ : hello 문자열을 포함한 모든 레코드 출력
# action {print $1} : 각 행(레코드)에서 첫번째 필드를 출력 
$ awk -F ":" '/hello/ {print $1}' test.txt

💥 Info

pattern 과 { action }은 반드시 명령어에 모두 써줘야 되는 건 아니다.

아래와 같이 pattern이 생략되는 경우,

매칭 여부를 검사할 문자열 패턴 정보가 없기 때문에 모든 레코드가 선택되는 거고,

action을 생략하면, 기본 액션인 print가 실행된다.

# pattern 생략.
$ awk '{ print }' ./file.txt      # file.txt의 모든 레코드 출력.
 
# action 생략.
$ awk '/p/' ./file.txt            # file.txt에서 p를 포함하는 레코드 출력.

‣ AWK 필드

awk는 입력 텍스트를 레코드(줄)와 필드(공백이나 특정 구분자로 구분된 단위)로 나눈다.

기본적으로 공백이나 탭 문자를 필드 구분자로 사용한다. 각 필드는 $1, $2, ... $NF와 같이 참조할 수 있다.

  • $1: 첫 번째 필드
  • $2: 두 번째 필드
  • $NF: 마지막 필드 (NF는 현재 줄의 필드 개수를 나타냄)

‣ 비교연산 패턴

조건문 처럼 해당 조건에 부합한 데이터 라인들을 뽑아낼 수 있다.

💡 TIP

숫자, 알파벳 모두 비교 연산이 가능

# /etc/passwd 파일에서 3번째 필드인 $3의 값이 0보다 작거나 같을 경우 나머지 필드들을 print
$ cat /etc/passwd | awk -F ":" '$3 <=0 {print $1, $3}'
0

# 복잡한 논리식 또한 사용 가능
$ awk '$3 > $5 && $3 <= 100 {print $1}' filename.txt

‣ 정규표현식 패턴

grep 명령어와 같이 패턴 부분에 정규식 /regex/를 넣어 라인을 분별할 수도 있다

# 대소문자 구분 없는 알파벳으로 시작하고 뒤에 어느 한 문자가 오는 라인 매칭
$ awk '/^[A-Z][a-z]+ /' filename

# "이" 자로 시작하는 라인 골라서 print
$ awk '/^정/{print $1,$2,$3}' filename

‣ 패턴 매칭 연산

match 연산자(~) : 표현식과 매칭되는 것이 있는지 검사하는 연산자

  • ~ 일치하는 부분
  • !~ 일치하지 않는 부분

새로운 문법이지만, 이렇게 이해하면 간단하다. 보통 값을 비교할때, == 연산자를 쓰는데, 패턴매칭을 비교하기 위해선 == 대신 match연산자(~)를 쓰는 것이다.

# 2번 필드 문자열이 문자 g로 끝나지 않는 라인 출력
$ awk '$2 !~ /g$/' filename

‣ AWK 제어문

# 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는 개행 문법을 지원하고 있다

$ 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";
}
'

삼항 연산자 또한 사용 가능

# 삼항 연산자 역시 사용이 가능하다
$ 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

‣ awk 리다이렉션

awk결과를 리눅스 파일로 리다이렉션할 경우 쉘 리다이렉션 연산자를 사용한다.

다만 > 기호가 논리연산자인지 리다이렉션인지 모호할때가 있는데, 그냥 action 부분에 > 기호가 쓰이면 리다이렉션, pattern 부분에 쓰이면 논리 연산이라고 치부하면 된다.

파일명은 큰따옴표로 둘러쌰아 인식 된다.

  $ awk -F: '$4 >= 60000 {print $1, $2 > "new_file"}' awkfile5

▶︎ 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

‣ CSV 파일 처리

  • CSV 파일 처리 (-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 내장변수

  • 행 번호 출력 (NR 내장변수)
$ 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

‣ 조건문

$ 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부분에 비교연산자를 쓰든 결과는 동일

‣ 루프문

$ 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 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

‣ 행 데이터 세기

  • 텍스트 파일을 처리하고 첫 번째 행을 제외한 나머지 행의 데이터 수 세기
#!/bin/bash

# 파일명 설정
file="파일명.txt"

# awk를 사용하여 데이터 수 계산
count=$(awk 'NR > 1 {print}' "$file" | wc -l)

echo "데이터 수 : $count"

위 스크립트는 주어진 파일에서 첫 번째 행을 제외하고 나머지 행의 수를 세는 데 사용됩니다. 여기서 NR > 1는 현재 행 번호가 1보다 큰 경우를 의미하며, 이를 통해 첫 번째 행을 건너뛰고 두 번째 행부터 처리합니다. 그런 다음 wc -l은 출력된 행 수를 계산합니다.

이 스크립트를 실행하면 셸에서 텍스트 파일의 첫 번째 행을 제외한 데이터 수가 표시됩니다.


‣ 메모리 사용 계산

  • 메모리 사용 현황 계산
free -m | awk 'NR==2{printf "%.2f", $3 * 100 / $2}'

예를 들어, 만약 free -m 명령어의 출력이 다음과 같다고 가정할 때

              total        used        free      shared  buff/cache   available
Mem:           7855        3022        2448         102        2384        4255
Swap:          2047           0        2047

그러면 awk 'NR==2{printf "%.2f", $3 * 100 / $2}' 부분은 3022 * 100 / 7855를 계산하여 메모리 사용량의 백분율을 출력할 것이다.

결과는 38.50이 된다.

옵션 설명
NR==2 출력의 두 번째 줄에 해당하는 행을 선택. 여기서는 메모리 사용량을 나타내는 줄
'{printf "%.2f", $3 * 100 / $2}' 선택된 행에서 세 번째 필드($3,즉 사용중인 메모리)와 두번째 필드($2, 즉 총 메모리)를 사용하여 사용 중인 메모리의 백분율을 계산하고 소수점 둘째 자리까지 출력함.

Loading script...