grep

명령의 출력이나 파일에서 regex 을 이용해 매칭 되는 라인, 스트링을 찾을 때 자주 사용되는 명령이 grep 입니다. grep 명령은 ed 라인 에디터를 만든 Ken Thompson 이 스트링 매칭 기능을 분리해서 별도의 명령으로 만든 것으로 grep 이라는 이름은 ed 명령의 g/re/p 에서 유래합니다. ( g 는 global search 를 re 는 regular expression, p 는 print 를 의미 )

이후에 extended regular expression 을 사용하는 egrep, 단순 fixed 스트링( regex 가 아닌 )을 검색하는 fgrep 같은 명령이 추가되었지만 지금은 grep 하나의 명령에서 -E, -F 옵션을 통해 모두 제공됩니다. 하위 호환성을 위해 /bin/egrep, /bin/fgrep 명령이 아직 존재하지만 단순히 wrapper script 에 불과합니다.

grep 명령을 잘 사용하기 위해서는 먼저 regex 에 대해서 알고 활용할 수 있어야 합니다. 여기서는 regex 를 이용한 여러 가지 활용 방법보다는 명령에서 제공하는 전반적인 기능에 대해서 알아보겠습니다. 다음은 옵션 설정을 통해서 grep 명령에서 사용할 수 있는 regex 종류입니다. ( 각각의 차이점에 대해서는 이곳 을 참고하세요 )

-G, --basic-regexp    # 기본값 입니다.
              basic regular expression (BRE).

-E, --extended-regexp
              extended regular expression (ERE).

-P, --perl-regexp
              Perl-compatible regular expression  (PCRE).

-F, --fixed-strings   # regex 를 사용하지 않고 문자 그대로 매칭하는 것으로 속도가 제일 빠름
              fixed  strings

매칭할 스트링에 regex 메타문자가 포함될 경우 escape 해야 합니다

. 문자는 regex 에서 사용되는 특수 문자로 임의의 한 문자에 해당됩니다. 따라서 . 문자 자체를 매칭하기 위해서는 escape 해야 합니다.

여러 개의 매칭 패턴을 사용

-e PATTERN, --regexp=PATTERN

-e 옵션을 사용하면 여러 개의 매칭 패턴을 적용할 수 있습니다.

$ grep -e 'harry' -e 'pulse' /etc/passwd
pulse:x:116:124:PulseAudio daemon,,,:/var/run/pulse:/bin/false
harry:x:1000:1000:NetworkManager:/home/harry:/bin/bash

또한 검색할 스트링이 - 문자로 시작할 경우 grep 명령의 옵션과 구분하기 위해서 사용할 수 있습니다.

# 검색할 스트링 '--width' 가 grep 명령의 옵션으로 인식돼 오류 발생
$ grep '--width' *.cpp
grep: unrecognized option '--width'
Usage: grep [OPTION]... PATTERN [FILE]...
Try 'grep --help' for more information.

$ grep -e '--width' *.cpp
OK
$ grep -- '--width' *.cpp
OK

대, 소문자 구분없이 매칭

-i, --ignore-case

대, 소문자 구분 없이 매칭하려면 이 옵션을 사용하면 됩니다.

invert 매칭

-v, --invert-match

invert 매칭은 반대로 매칭되지 않는 라인을 프린트 합니다.

Context line control

grep 명령을 사용하다 보면 매칭 되는 라인을 표시하는 것 외에 더해서 이전, 이후 n 라인을 표시하는 것이 필요할 때가 있습니다. 이때 사용할 수 있는 옵션입니다.

-A NUM, --after-context=NUM

-B NUM, --before-context=NUM

-C NUM, -NUM, --context=NUM

$ cat sample.txt
aaa
bbb
ccc
ddd
eee
fff
ggg

$ grep -A 2 'ddd' sample.txt     # After 2 lines
ddd   # ddd
eee
fff

$ grep -B 2 'ddd' sample.txt     # Before 2 lines
bbb
ccc
ddd   # ddd

$ grep -C 2 'ddd' sample.txt     # Context 2 lines
bbb
ccc
ddd   # ddd
eee
fff

$ grep -A2 -B3 'ddd' sample.txt
...

recursive 매칭

-r, --recursive

특정 디렉토리 이하 모든 파일을 대상으로 매칭합니다.

# ./dir1 ./dir2 디렉토리 이하 모든 파일에서 'hello' 매칭
$ grep -Iwr 'hello' ./dir1 ./dir2

# 디렉토리 인수를 주지 않으면 현재 디렉토리가 적용됩니다.
$ grep -Iwr 'hello'

$ grep -Ir 'super_block {' /usr/include 
/usr/include/linux/bfs_fs.h:struct bfs_super_block {
/usr/include/linux/minix_fs.h:struct minix_super_block {
/usr/include/linux/minix_fs.h:struct minix3_super_block {
/usr/include/linux/qnx4_fs.h:struct qnx4_super_block {
/usr/include/linux/nilfs2_ondisk.h:struct nilfs_super_block {
/usr/include/linux/romfs_fs.h:struct romfs_super_block {

-R, --dereference-recursive

디렉토리 심볼릭 링크가 있을 경우 -r 옵션은 기본적으로 follow 하지 않는데 반해 -R 옵션은 follow 합니다.
-r 옵션 사용시 특정 링크만 follow 하고 싶으면 해당 링크를 디렉토리 리스트에 같이 적어주면 됩니다.

include, exclude 설정

GLOBbing 패턴을 이용해서 특정 파일들만 포함시키거나 제외할 수 있습니다.

--include=GLOB

# 'hello' 스트링을 찾을 때 '*.c' 파일들만 검색
$ grep -r --include='*.c' 'hello'

--exclude=GLOB

# 'hello' 스트링을 찾을 때 '*.cpp' 파일을 제외하고 검색
$ grep -r --exclude='*.cpp' 'hello'

--exclude-dir=GLOB

# 'hello' 스트링을 찾을 때 '.git' 디렉토리를 제외하고 검색
$ grep -r --exclude-dir='.git' 'hello'

binary 파일은 제외하고 검색

바이너리 파일과 텍스트 파일이 혼합되어 있을 경우 이 옵션으로 바이너리 파일을 검색에서 제외할 수 있습니다.

-I

# 'hello' 스트링을 찾을 때 바이너리 파일은 제외하고 검색
$ grep -Irw 'hello'

매칭 스트링만 프린트

-o, --only-matching

이 옵션은 매칭 라인을 프린트하는 것이 아니고, 매칭 되는 스트링만 프린트합니다.

$ sensors | grep -i core | grep -E -o '\+[0-9.]+'
+46.0
+86.0
+100.0
+46.0
+86.0
+100.0
+46.0
+86.0
+100.0
+46.0
+86.0
+100.0

directory, device 파일 skip 하기

-d ACTION, --directories=ACTION

-D ACTION, --devices=ACTION

입력으로 사용되는 파일명에 directory 나 device 이름이 포함될 경우 ACTION 값을 skip 으로 설정하면 skip 할 수 있습니다.

$ grep -I 'main' *
best.c:int main()
err.c:int main() {
grep: gd: Is a directory       <--- directory
gd
grep: gmp: Is a directory      <--- directory
gmp
main.c:int main() {

$ grep -I -d skip 'main' *     # directory skip
best.c:int main()
err.c:int main() {
main.c:int main() {

n 번째 매칭까지만 프린트

-m NUM, --max-count=NUM

전체 데이터를 검색할 필요 없이 n 번째 매칭에서 연산을 종료하고자 할 때 사용할 수 있습니다.

# 첫 번째 'cache 0' 스트링 매칭에서 이후 70 라인을 프린트하고 종료
$ cpuid | grep -m1 -A70 'cache 0'

라인 넘버 프린트

-n, --line-number

매칭 라인을 프린트할 때 라인 넘버도 함께 프린트됩니다.

$ grep -nA2 foo test.c
190:int foo()
191-{
192-    int arr[] = { 100, 200, 300 };

$ grep -n foo *.c
best.c:40:int foo()
err.c:12:int foo() {
main.c:9:int foo() {
...

출력에 파일명도 포함

-H, --with-filename

-h, --no-filename

grep foo *.c 와 같이 여러 개의 파일을 검색하는 경우는 -H 가 디폴트가 되어 파일명이 함께 표시되고 grep foo hello.c 와 같이 하나의 파일만 검색하는 경우 -h 가 디폴트가 되어 파일명이 표시되지 않습니다. 이것은 find 명령의 -exec 에서 grep 명령을 실행할때 하나의 파일로 인식되어 파일명이 표시되지 않는데 이때 -H 옵션을 사용하면 파일명을 함께 출력할 수 있습니다.

$ find * -name Makefile -exec grep -Hne 'include' {} \;

매칭 라인 카운트

-c, --count

해당 파일에 몇 개의 매칭 되는 라인이 있는지를 알려줍니다.

$ grep -c 'hello' *.html
aaa.html:2
bbb.html:0
ccc.html:1
...

단어 매칭

-w, --word-regexp

스트링의 일부분이 아닌 단어 매칭을 합니다.

라인 매칭

-x, --line-regexp

스트링의 일부분이 아닌 라인 전체를 매칭 합니다.

arr1=("foo 111" "bar 222" "zoo 333")

# 실질적으로 grep -q '^bar 222$' 와 동일
if `printf '%s\n' "${arr1[@]}" | grep -x -q 'bar 222'`; then
    echo yes
fi

파일명만 프린트

-l, --files-with-matches

-L, --files-without-match

이 두 옵션은 매칭 라인을 출력하지 않고 단지 파일명만 출력합니다.
-l 옵션은 매칭이 발견되는 파일명을 프린트하고 (첫 번째 매칭에서 스캐닝이 중단됩니다.)
-L 옵션은 반대로 매칭이 발견되지 않는 파일명을 프린트합니다.

# ./dir 디렉토리 이하 모든 파일을 검색해서 'hello' 를 포함하고 있을 경우 XXX 를 YYY 로 변경
$ grep -rl 'hello' ./dir | xargs sed -i 's/XXX/YYY/g'

라인 구분자가 NUL 문자일 경우

-z, --null-data

-Z, --null

-z 옵션은 입력되는 데이터가 newline 대신에 NUL 문자를 라인 구분자로 할경우 사용합니다.
-Z 옵션은 -l 옵션을 이용해 파일명을 출력할때 NUL 문자를 라인 구분자로 출력합니다.

프린트 금지

-q, --quiet, --silent

아무 출력도 하지 않고 매칭이 발견되는 즉시 종료 상태 값으로 0 을 리턴하고 exit 합니다.

-s, --no-messages

오류가 발생해도 에러 메시지를 출력하지 않습니다.

이 옵션을 사용하면 명령 실행시 출력이 발생하지 않고 종료 상태 값만 설정됩니다. 따라서 스크립트를 작성할 때 if 문에서 사용하기 좋습니다.

$ AA="foo 12345 bar"

$ if echo "$AA" | grep -Eq '[0-9]+ bar'; then echo 111; else echo 222; fi
111

gzip 파일 grep

zgrep 명령을 이용하면 gzip 파일을 grep 할 수 있습니다.

$ zgrep –i error /var/log/syslog.2.gz

매칭에 escape sequence 를 사용하는 방법

grep 은 regex 를 작성할 때 기본적으로 escape sequence 를 사용할 수 없습니다. 따라서 매칭에 escape sequence 를 사용하려면 다음과 같은 방법을 이용해야 합니다.

# bash 의 경우 $' ' quotes 을 사용
bash$ echo -e 'hello\tworld' | grep $'\t'
hello   world

# sh 의 경우 "$(echo "\t")" 형식 을 사용
sh$ echo 'hello\tworld' | grep "$(echo "\t")"
hello   world

# 또는 PRE 를 사용
$ echo -e 'hello\tworld' | grep -P '\t'
hello   world

$ grep -P -abo '\135\0\0\0' vmlinuz-5.3.0-45-generic 
6974861:]
9128654:]
9130253:]

$ echo -e 'hello\tworld' | awk '/\t/'
hello   world

grep 은 binary 파일도 매칭에 사용할 수 있습니다.

grep 이 binary 파일을 다루는 3 가지 방법

1. --binary-files=binary

디폴트로 binary 파일 내에 존재하는 스트링을 검색해서 매칭이 있을 경우 알려줍니다.
텍스트 파일에서처럼 매칭 라인을 출력하지는 않습니다.

$ grep ELF mycomm                # mycomm 은 실행 파일
Binary file mycomm matches  <--- 매칭이 있다고 알려줌
2. --binary-files=without-match

binary 파일일 경우 매칭을 시도하지 않고 skip 합니다.
이것은 -I 옵션과 같은 것입니다.

3. --binary-files=text

binary 파일도 text 파일처럼 취급하여 매칭하고 출력합니다.
이것은 -a 옵션과 같은 것입니다.

-a, --text
-b, --byte-offset

-b 옵션과 -o 옵션을 이용하면 파일내에 매칭되는 위치의 offset 값을 구할 수 있습니다.

# ':' 왼쪽이 offset 값
$ echo helloworld | grep -bo hel
0:hel

$ echo helloworld | grep -bo ll
2:ll

$ echo helloworld | grep -bo wor
5:wor

$ echo helloworld | grep -bo l
2:l
3:l
8:l

따라서 -abo 옵션을 이용하면 binary 파일내에서 매칭되는 위치의 offset 값을 구할 수 있습니다.
활용 예는 여기 를 참고 하세요

Perl Regular Expressions

grep 명령은 -P 옵션을 사용하면 perl regex 을 사용할 수 있습니다. 여기서는 단어를 추출할때 유용하게 사용할수 있는 lookbehind, lookahead 기능에 대해서 알아보겠습니다. 추출하고자 하는 스트링이 가운데 위치한다고 할경우 lookbehind 식은 왼쪽에 lookahead 식은 오른쪽에 위치하고 다음과 같은 방식으로 작성합니다.

lookbehind : (?<=foo) (?<!foo)      # lookbehind 는 '<' 문자가 붙는다. 

lookahead  : (?=bar)  (?!bar)       # '=' 는 같을경우, '!' 는 같지않을경우

다음은 strace 출력에서 발췌한 내용인데 여기서 함수명만 추출하려고 합니다. 함수명은 바로 뒤에 붙어서 ( 문자가 오기 때문에 아래 첫번째 명령으로 쉽게 추출이 되지만 문제는 출력에도 ( 문자가 붙는다는 것입니다. 따라서 매칭 시에는 ( 문자를 포함하지만 출력 시에는 제외하는 것이 필요한데 이때 사용할 수 있는 기능이 lookahead 입니다.

실제 정확히 추출하려면 sed -En 's/^[0-9]+ (\w+).*/\1/p' 를 이용해야 합니다.

$ cat sample.txt
5751  geteuid()                         = 1000
5751  fstat(1, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
5751  write(1, "1000\n", 5)             = 5
5750  <... read resumed> "1000\n", 128) = 5
5751  close(1 <unfinished ...>
5750  read(3,  <unfinished ...>
5751  <... close resumed> )             = 0
5750  <... read resumed> "", 128)       = 0

$ grep -P -o ' \w+\(' sample.txt      
geteuid(    
fstat(       # 출력에도 `(` 문자가 붙는다.
write(
close(
read(

# lookahead 기능을 이용해 출력시에는 `(` 문자를 제거
$ grep -P -o ' \w+(?=\()' sample.txt  
geteuid
fstat
write
close
read

다음은 lookbehind 기능을 이용해 C-@ outputs 뒤에오는 단어를 추출하는 예입니다.

$ cat sample.txt
\e, outputs !#:$\e^
\C-@ outputs git 
\e[23~ outputs \C-k\C-ugit pull\C-m
\eh8 outputs !#:8\e^
\eu outputs \eb\eU1

$ grep -P -o '(?<=C-@ outputs )\w+' sample.txt
git

grep 외 다른 명령

요즘에는 grep 과 같은 기능의 신세대 명령들이 많이 소개되고 있습니다. unix 명령은 기본적으로 파이프로 여러 명령들과 연결해 사용되기 때문에 보통 single thread 로 작성되는 경우가 많은데 multi-thread 와 최신 cpu instruction 을 활용해서 성능을 높인 명령들이 있습니다. 그중에서 소개드릴 명령은 ripgrep 이라는 명령으로 사용방법도 grep 명령과 비슷하고 리눅스 소스 같은 많은 파일이 존재하는 디렉토리도 빠르게 검색 결과를 출력해 줍니다.

https://github.com/BurntSushi/ripgrep

$ rg -w bash ./linux
. . .
$ rg -w 'ptrdiff_t;' /usr/{include,lib}
. . .

Quiz

ps ax | grep firefox 명령을 실행해보면 firefox 프로세스가 출력되는 것에 더해서 grep 명령에도 firefox 스트링이 존재하기 때문에 grep 명령도 포함되는 것을 볼 수 있는데요. 어떻게 하면 grep 명령은 출력에서 제외시킬수 있을까요?

$ ps ax | grep firefox | less
 5256 ?        Sl   304:53 /usr/lib/firefox/firefox
 5333 ?        Sl   126:00 /usr/lib/firefox/firefox -contentproc ...
 5441 ?        Sl   116:04 /usr/lib/firefox/firefox -contentproc ...
14744 ?        Sl   169:55 /usr/lib/firefox/firefox -contentproc ...
14780 ?        Sl   155:18 /usr/lib/firefox/firefox -contentproc ...
17623 pts/12   S+     0:00 grep --color=auto firefox  # --> grep 명령도 출력에 포함된다.

# 다음과 같이 regex 을 활용하면 됩니다.
$ ps ax | grep 'firefo[x]' | wc -l
5

2.

다음 명령문은 어디가 잘못되었을까요?

$ cat /etc/mtab | grep cgroup

이것은 grep 명령을 사용할 때 종종 발견되는 부분인데 명령문이 틀린 것은 아니지만 위 명령문은 다음과 같이 하나의 명령으로 실행할 수 있습니다.

$ grep cgroup /etc/mtab

$ grep cgroup < /etc/mtab