Field 분리

Field 분리에는 FS 를 이용하는 기본 방법과 FPAT, FIELDWIDTHS 를 이용하는 방법 세 가지가 있습니다.
FS 에 설정되는 값은 문자가 두 개 이상이면 regex 로 해석됩니다.

디폴트 값은 FS = " "

RS( Record Seperator ) 에서는 RS="" 가 특별히 처리된다면 FS( Field Seperator ) 에서는 FS=" " 가 특별히 처리됩니다. FS=" " 의 기능은 실제적으로 FS="[ \t\n]+" 와 같습니다. 다시 말해서 space 뿐만 아니라 tab, newline 도 같은 분리자로 취급한다는 뜻입니다.

FS="[ \t\n]+" 의 경우 한가지 단점이 있는데 첫 번째 필드 시작 전에 공백이 있으면 null 값 필드가 생긴다는 것입니다. 하지만 FS=" " 의 경우는 먼저 공백을 삭제하기 때문에 null 값 필드가 생기지 않습니다. 이것은 RS="" 와 비슷한 기능입니다.

# 디폴트 FS 값은 space
$ awk 'BEGIN{ printf FS }' | od -a
0000000  sp
0000001

# 실제적으로 FS=" " 는 FS='[ \t\n]+' 와 같은 기능을 합니다.
# 추가로 FS=" " 는 첫 번째 필드 시작 전에 있는 공백을 먼저 삭제 처리합니다.
$ printf %b '  11 \t\t22    33\n\t\n \n44' |
awk '{ printf "(%s) (%s) (%s) (%s) NF: %d\n",$1,$2,$3,$4,NF }' RS='^$' FS=' '
(11) (22) (33) (44) NF: 4

# FS=" " 와 달리 FS='[ \t\n]+' 는 첫 번째 필드 시작 전에 공백이 있을 경우 null 값 필드가 생깁니다.
printf %b '  11 \t\t22    33\n\t\n \n44' |                                        
awk '{ printf "(%s) (%s) (%s) (%s) (%s) NF: %d\n",$1,$2,$3,$4,$5,NF }' RS='^$' FS='[ \t\n]+'
() (11) (22) (33) (44) NF: 5

cut 명령과 awk 가 다른점

쉘 스크립트에서 필드값을 분리할 때 cut 명령을 사용하는데요.
cut 명령과 awk 는 다음과 같은 차이가 있습니다.

awk 의 경우 공백 개수에 상관없이 정확히 필드를 분리합니다.

$ cat file
AAA  BBB   CCC DDD
  XXX   YYY      ZZZ    KKK
 111  222    333 444
................................

$ awk '{print $1,$2,$3,$4}' file
AAA BBB CCC DDD
XXX YYY ZZZ KKK
111 222 333 444

$ awk '{print $1,$3}' file
AAA CCC
XXX ZZZ
111 333

cut 명령은 실제 space 개수에 따라서만 필드가 분리됩니다.

$ cat file
AAA BBB CCC DDD
 XXX YYY ZZZ KKK
  111 222 333 444
......................

$ cut -d' ' -f1  file
AAA


$ cut -d' ' -f2  file
BBB
XXX

$ cut -d' ' -f3  file
CCC
YYY
111

cut 명령과 같이 FS 값을 실제 space 로 설정하려면

다음과 같이 regex 표현식 [ ] or ( ) 을 이용해 설정하면 됩니다.

# space 수에 관계없이 필드 개수가 4 개로 나온다.
$ echo '11 22   33      44' | awk '{ print NF }' FS=' '
4

# FS='[ ]' 로 설정하면 모든 space 에서 필드가 분리되어 필드 개수가 11 개로 나온다
$ echo '11 22   33      44' | awk '{ print NF }' FS='[ ]'
11

sort 명령과 awk 가 다른점

다음은 ps 명령의 출력에서 4 번째 TIME 컬럼의 값을 sort -u 명령을 이용해 uniq 한 값을 구하려고 한 것인데요. 결과를 보면 uniq 한값이 아니라 0:00, 0:01 값이 중복되어 나타나고 있습니다. 이렇게 되는 이유는 앞에 흰색으로 표시한 공백이 컬럼 값에 포함되어 다른 값으로 인식되기 때문입니다.

따라서 정상적으로 결과가 출력되기 위해서는 sort 명령의 -b( ignore-leading-blanks ) 옵션을 함께 사용해야 합니다. 하지만 이것도 separator 값이 공백이 아닐 경우는 사용할 수 없습니다.

awk 의 경우는 FS 값만 변경해주면 되고 정확히 uniq 한 값을 출력합니다.

$ ps axc --no-headers | awk '{a[$4]=$0} END {for (i in a) print a[i]}' 
 5710 ?        Sl    11:35 chrome
 1617 ?        Sl     5:40 konsole
 5804 ?        Sl     1:59 chrome
 5556 ?        Sl    59:40 chrome
24408 pts/10   Ss     0:10 bash
 1181 ?        Ssl    0:12 polkitd
 1378 ?        Ss     0:13 dbus-daemon
 . . . . .
 . . . . .

RS = "" 이고 FS 가 single character 일 경우

RS = "" 이면 레코드가 공백 라인에 의해 분리가 되는데요. 이때 FS 값이 single character 이면 자동으로 newline 에서도 필드가 분리됩니다.

$ cat file
Amelia@555-5553
Anthony@555-3412
Becky@555-7685
.................

# FS='@' 이지만 newline 에서도 분리가 되어 NF 값이 6 개로 나온다.
$ awk '{print NF}' FS='@' RS='' file
6

# 실질적으로 FS='[@\n]' 와 같다.
$ awk '{print NF}' FS='[@\n]' RS='' file
6

$ awk '{printf "(%s) (%s) (%s) (%s)\n",$1,$2,$3,$4}' FS='@' RS='' file
(Amelia) (555-5553) (Anthony) (555-3412)

$ awk '{printf "(%s) (%s) (%s) (%s)\n",$1,$2,$3,$4}' FS='[@\n]' RS='' file
(Amelia) (555-5553) (Anthony) (555-3412)
---------------------------------------------------------------------------

# FS='[@]' 로 설정하면 정확히 '@' 문자에서만 분리가 된다.
$ awk '{print NF}' FS='[@]' RS='' file
4

$ awk '{printf "(%s) (%s) (%s) (%s)\n",$1,$2,$3,$4}' FS='[@]' RS='' file
(Amelia) (555-5553
Anthony) (555-3412
Becky) (555-7685)

FS = ""

FS 값을 "" 로 설정하면 문자 단위로 필드가 분리됩니다.

$ echo "foo bar" | awk '{for (i=1; i<=NF; i++) print "$" i " : " $i}' FS=''
$1 : f
$2 : o
$3 : o
$4 :  
$5 : b
$6 : a
$7 : r

FS 값에 사용되는 ^, $ regex 문자

FS 에 사용되는 ^, $ 문자는 레코드의 시작과 끝을 나타냅니다. 따라서 만약에 FS 에 ^ 를 사용한다면 레코드의 시작 부분에만 매칭이 되고 $ 를 사용한다면 레코드의 마지막 부분에만 매칭이 되게 됩니다.

FS = "^$"

이것은 레코드 값이 존재하는 한 매칭이 되지 않으므로 레코드 분리가 일어나지 않아 NF 가 1 이 되고 $0 == $1 가 됩니다.

다음 예는 레코드의 필드값이 경우에 따라서 공백으로 비어 있습니다. 이와 같이 구성되어 있을 경우 각 컬럼에 맞는 필드값을 정확히 추출하기 어려운데요. 이럴 때는 FS 를 이용하는 방법 대신에 FIELDWIDTHS 를 사용할 수 있습니다. 이 방법은 FIELDWIDTHS 에 직접 입력한 컬럼 사이즈에 따라서 필드를 분리합니다.

$ awk -v FIELDWIDTHS="6 8 7 6 3 6 4" '{ 
    printf "(%s) (%s) (%s) (%s) (%s) (%s) (%s)\n",
           $1, $2, $3, $4, $5, $6, $7 
}' file
(4051  ) (S00001  ) (31228  ) (3286  ) (0  ) (      ) (23.6)
(4050  ) (S00201  ) (       ) (4251  ) (0  ) (12.1  ) (23.6)
(4049  ) (S00301  ) (31221  ) (3021  ) (1  ) (14.4  ) ()
(4048  ) (S00213  ) (46578  ) (      ) (0  ) (23.2  ) (43.9)
(4047  ) (S00113  ) (       ) (4250  ) (0  ) (      ) (43.9)
(4046  ) (S00122  ) (31225  ) (4249  ) (0  ) (45.5  ) (21.6)

그런데 위의 출력 결과를 보면 필드값에 공백이 포함되죠. gawk 4.2 부터는 각 컬럼 사이즈에 : 를 이용해서 skip width 를 지정할 수 있습니다. 그러니까 사이즈가 6 인 컬럼을 사용하고 싶은데 앞에 공백이 2 만큼 있으면 2:6 와 같이 작성하면 됩니다.

$ awk -v FIELDWIDTHS="4 2:6 2:5 2:4 2:1 2:4 2:2" '{ 
    printf "(%s) (%s) (%s) (%s) (%s) (%s) (%s)\n",
           $1, $2, $3, $4, $5, $6, $7 
}' file
(4051) (S00001) (31228) (3286) (0) (    ) (23)
(4050) (S00201) (     ) (4251) (0) (12.1) (23)
(4049) (S00301) (31221) (3021) (1) (14.4) ()
(4048) (S00213) (46578) (    ) (0) (23.2) (43)
(4047) (S00113) (     ) (4250) (0) (    ) (43)
(4046) (S00122) (31225) (4249) (0) (45.5) (21)

또한 gawk 4.2 부터는 마지막 컬럼 사이즈로 이후 모두를 나타내는 * 도 사용할 수 있습니다.

$ awk -v FIELDWIDTHS="4 2:6 2:*" '{ 
    printf "(%s) (%s) (%s)\n", $1, $2, $3 
}' file
(4051) (S00001) (31228  3286  0        23.6)
(4050) (S00201) (       4251  0  12.1  23.6)
(4049) (S00301) (31221  3021  1  14.4)
(4048) (S00213) (46578        0  23.2  43.9)
(4047) (S00113) (       4250  0        43.9)
(4046) (S00122) (31225  4249  0  45.5  21.6)

FIELDWIDTHS 방법을 사용한 후에 다시 FS 나 FPAT 방법을 사용하려면 해당 변수값을 다시 설정해 주면 됩니다.

$ awk -v FIELDWIDTHS="6 8 7 6 3 6 4" '{ print NF } NR==3 { FS=FS }' file
7     
7
7     
6     # NR==3 에서 FS=FS 가 설정되어 4 번째 레코드부터 FS 방법이 사용된다.
5
7

다음은 CSV( comma-separated values ) 레코드의 한 예입니다. 전체 필드 개수가 7 개인 레코드인데 3 번째 필드값의 double quotes 안에서도 , 가 사용되어 실제 필드 개수가 8 개로 나오고 있습니다.

Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA
1       2       3                         4      5       6          7
.......................................................................

# 필드 개수가 총 8 개로 나온다.
$ echo 'Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA' |
awk '{ print NF }' FS=,
8

이와 같은 문제를 해결하기 위한 것이 FPAT( Field PATtern ) 인데요. 기존의 FS 를 이용하는 방법이 레코드에서 FS 값을 매칭하여 분리했다면 FPAT 을 이용하는 방법은 실제 필드값 자체를 매칭합니다.

따라서 위 예제와 같은 경우 FPAT 값은 다음 두 가지로 설정할 수 있습니다.

  • [^,]+ ( , 이외의 값 )

  • "[^"]+" ( " " 로 둘러싸인 값 )

# 필드 개수가 정확히 7 개로 나온다
$ echo 'Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA' |
awk -v FPAT='[^,]+|"[^"]+"' '{ print NF }'
7

$ echo 'Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA' |
awk '{printf "%s\n%s\n%s\n%s\n%s\n%s\n%s\n", $1,$2,$3,$4,$5,$6,$7}' FPAT='[^,]+|"[^"]+"'
Robbins
Arnold
"1234 A Pretty Street, NE"
MyTown
MyState
12345-6789
USA

예제2 )

$ echo 'ip="192.168.1.1" method="http https ftp" period="5"' |
awk '{ printf "(%s)\n(%s)\n(%s)\n",$1,$2,$3 }' FPAT='[^ ]+="[^"]*"'

(ip="192.168.1.1")
(method="http https ftp")
(period="5")

FPAT 을 이용한 필드 추출

FPAT 을 이용하면 특정 스트링 패턴을 필드로 추출하여 사용할 수 있습니다. 다음 예제를 보면 {{AAA}},{{BBB}} 이 각각 $1, $2 필드 변수에 할당된 것을 볼 수 있습니다.

$ echo "{{AAA}} design will serve as the basis for the planned Mars {{BBB}} rover." |
awk '{ print $1; print $2; print NF }' FPAT='{{[A-Z]+}}'

{{AAA}}
{{BBB}}
2

FPAT 방법을 사용한 후에 다시 FS 나 FIELDWIDTHS 방법을 사용하려면 해당 변수값을 다시 설정해 주면 됩니다. 현재 사용되고 있는 방법은 PROCINFO["FS"] 값으로 조회할 수 있습니다.

$ seq 3 | awk '{ 
    check()
    FIELDWIDTHS = FIELDWIDTHS
    check()
    FPAT = FPAT
    check()
    FS = FS
}
function check() {

    using_fs   = (PROCINFO["FS"] == "FS")
    using_fw   = (PROCINFO["FS"] == "FIELDWIDTHS")
    using_fpat = (PROCINFO["FS"] == "FPAT")

    if (using_fs)
        print "using FS"
    else if (using_fw)
        print "using FIELDWIDTHS"
    else if (using_fpat)
        print "using FPAT"
}' 
using FS
using FIELDWIDTHS
using FPAT
using FS
using FIELDWIDTHS
using FPAT
using FS
using FIELDWIDTHS
using FPAT
------------------------------------

$ seq 10 | awk '{ 

    if (NR == 4) FIELDWIDTHS = FIELDWIDTHS
    if (NR == 7) FPAT = FPAT

    check()
}
function check() {

    using_fs   = (PROCINFO["FS"] == "FS")
    using_fw   = (PROCINFO["FS"] == "FIELDWIDTHS")
    using_fpat = (PROCINFO["FS"] == "FPAT")

    if (using_fs)
        print "using FS"
    else if (using_fw)
        print "using FIELDWIDTHS"
    else if (using_fpat)
        print "using FPAT"
}' 
using FS
using FS
using FS
using FIELDWIDTHS
using FIELDWIDTHS
using FIELDWIDTHS
using FPAT
using FPAT
using FPAT
using FPAT

예제 )

objdump 명령의 -t 옵션을 이용하면 symbol table 을 조회해 볼 수 있습니다. 출력은 5 컬럼으로 구성되는데 첫 번째 컬럼의 symbol value 와 세 번째 컬럼의 section name 사이에 있는 것이 심볼에 설정되어 있는 flag 값입니다. 이 flag 값은 space 로 처리되는 경우가 있어서 FIELDWIDTHS 를 이용해 분리해야 하는데요. 그런데 문제는 1, 2 번째 컬럼은 WIDTH 가 정해져 있어서 가능하겠는데 3, 4, 5 번째 컬럼의 출력 상태를 보면 WIDTH 를 적용시킬 수가 없습니다. 이럴 때는 어떻게 분리할 수 있을까요?

$ objdump -t hello
. . . .
. . . .
0000000000000000 l    df *ABS*  0000000000000000          hello.c
0000000000000000 l    df *ABS*  0000000000000000          crtstuff.c
00000000000009a4 l     O .eh_frame      0000000000000000          __FRAME_END__
0000000000000000 l    df *ABS*  0000000000000000            
0000000000200d90 l       .init_array    0000000000000000          __init_array_end
0000000000200d98 l     O .dynamic       0000000000000000          _DYNAMIC
0000000000200d88 l       .init_array    0000000000000000          __init_array_start
0000000000000860 l       .eh_frame_hdr  0000000000000000          __GNU_EH_FRAME_HDR
0000000000200fa8 l     O .got   0000000000000000          _GLOBAL_OFFSET_TABLE_
0000000000000830 g     F .text  0000000000000002          __libc_csu_fini
0000000000000000  w      *UND*  0000000000000000          _ITM_deregisterTMCloneTable
0000000000201000  w      .data  0000000000000000          data_start
0000000000000000       F *UND*  0000000000000000          puts@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000          hello_foo

^^^^^^^^^^^^^^^^ ^^^^^^^ ^^^^^^^
  symbol value    flags  section
------------------------------------------------------------------------------------

# 다음과 같이 FIELDWIDTHS 와 FS 방법을 같이 사용합니다.
$ objdump -t hello |        
awk 'NR > 4 && NF > 0 { 
    FIELDWIDTHS="16 1:7 1:*"; $0=$0; f1=$1; f2=$2
    FS=FS; $0=$3;
    printf "(%s) (%s) (%s) (%s) (%s)\n", f1, f2, $1, $2, $3 
}'
. . . .
. . . .
(0000000000000000) (l    df) (*ABS*) (0000000000000000) (hello.c)
(0000000000000000) (l    df) (*ABS*) (0000000000000000) (crtstuff.c)
(00000000000009a4) (l     O) (.eh_frame) (0000000000000000) (__FRAME_END__)
(0000000000000000) (l    df) (*ABS*) (0000000000000000) ()
(0000000000200d90) (l      ) (.init_array) (0000000000000000) (__init_array_end)
(0000000000200d98) (l     O) (.dynamic) (0000000000000000) (_DYNAMIC)
(0000000000200d88) (l      ) (.init_array) (0000000000000000) (__init_array_start)
(0000000000000860) (l      ) (.eh_frame_hdr) (0000000000000000) (__GNU_EH_FRAME_HDR)
(0000000000200fa8) (l     O) (.got) (0000000000000000) (_GLOBAL_OFFSET_TABLE_)
(0000000000000830) (g     F) (.text) (0000000000000002) (__libc_csu_fini)
(0000000000000000) ( w     ) (*UND*) (0000000000000000) (_ITM_deregisterTMCloneTable)
(0000000000201000) ( w     ) (.data) (0000000000000000) (data_start)
(0000000000000000) (      F) (*UND*) (0000000000000000) (puts@@GLIBC_2.2.5)
(0000000000000000) (      F) (*UND*) (0000000000000000) (hello_foo)
. . . .
. . . .

E-mail 주소 분리

$ echo 'Jan 23 00:46:24 postfix/smtp[31481]: to=<wanted1918@gmail.com>, relay=...' |
awk -F'[<>]' '{print $2}'      # 또는 -F '<|>' 

wanted1918@gmail.com

필드 range 추출과 삭제

다음은 필드 range 를 인수로 받아서 추출, 삭제하는 함수입니다.

$ cat file
1,2,3,4,5,6,7,8,9
a,b,c,d,e,f,g,h,i
...................................

# 3 ~ 6 필드 range 추출
$ awk '
{ print subflds(3,6) }

function subflds(s,e,  r) {
    r = $(s++)
    while (s <= e) r = r OFS $(s++)
    return r
}
' FS=, OFS=, file

3,4,5,6
c,d,e,f

# 3 ~ 6 필드 range 삭제
$ awk '
{ print delflds(3,6) }

function delflds(s,e,  i,r) {
    for (i=1; i<=NF; i++)
        if (i < s || i > e) 
            r = r (r ? OFS : "") $i
    return r   
}
' FS=, OFS=, file

1,2,7,8,9
a,b,g,h,i