• 이전 문서: [[/mac/terminal-guide/02]]
  • 다음 문서: [[/mac/terminal-guide/04]]

find 명령

엄청 오래된 find 명령

[[/cmd/find]]는 1971년에 나온 Version 1 AT&T UNIX부터 있었으므로 50년이 넘는 역사적인 명령이라 할 수 있습니다. [[/cmd/find#email-1995]]{C 언어의 창시자인 데니스 리치에 의하면} 벨 연구소에서 켄 톰슨과 데니스 리치가 UNIX를 만들고 있을 때, 옆 부서의 딕 하이트가 [[/cmd/find]]를 만들고 있었다고 합니다.

워낙 오래된 명령이다 보니 [[/cmd/find]]는 여러 차례 재개발되었고, 그 와중에 계속해서 기능이 추가되었으며, 결국 50년 넘게 살아남아 전 세계의 UNIX, Linux 에서 아직까지 흔하게 사용되는 애플리케이션이 되었습니다.

그래서 [[/cmd/find]]는 좀 이상한 면도 있고, 사용하기 어렵기도 하고… 만능 애플리케이션들이 다 그렇듯 옵션도 너무 많습니다. 그래서 그냥 오래된 물건이라 그러려니 하고 몇 가지 자주 쓰는 옵션들만 기억해두면 그럭저럭 잘 쓸 수 있습니다.

[[/cmd/find]]의 현대적인 대안으로 나름 이름을 얻고 있는 CLI 애플리케이션들도 몇 가지 나오긴 했습니다만, 그런 것들을 쓸 수 없는 환경도 있으니 [[/cmd/find]]의 기본적인 사용법을 익혀둬서 손해볼 것은 없습니다.

조회 연습

첫 [[/cmd/find]] 명령을 입력해 봅시다.

다음과 같이 특정 경로를 지정하면 해당 경로의 모든 파일과, 그 하위의 모든 파일을 재귀적으로 탐색해 출력합니다.

find ~/Downloads
find ~/Desktop

-type 옵션

--type 옵션을 사용하면 파일 타입으로 조회 결과를 필터링할 수 있습니다.

 # 일반 파일만 조회한다
find ~/Downloads -type f
  • -type f: 일반 파일
  • -type d: 디렉토리
  • -type l: 심볼릭 링크

f, d는 자주 사용하기 때문에 외워두면 좋습니다.

f, d, l 외에도 b, c, s 등의 다른 타입 옵션이 있긴 하지만 시스템 디바이스를 검색할 일이 드물기 때문에 굳이 외울 필요는 없습니다. …하지만 궁금하니까 b를 지정해서 bloc special 파일들만 찾아 봅시다. 이런 것도 해보면 경험이 되겠죠.

$ find /dev -type b 2>/dev/null | head
/dev/disk0
/dev/disk0s1
/dev/disk0s3
/dev/disk0s2
/dev/disk1
/dev/disk1s2
/dev/disk1s1
/dev/disk1s3
/dev/disk2
/dev/disk1s4
  • find /dev: 디바이스 디렉토리에서
  • -type b: 블록 특수 파일을 찾아라
  • 2>/dev/null: 에러 메시지는 /dev/null로 보내서 스크린에 출력되지 않게 한다
  • | head: 결과를 첫 10줄만 출력한다

스크린샷 파일들은 기본적으로 ~/Desktop에 생성 날짜를 이름으로 해서 저장되니 예제로 적절할 것 같습니다.

이번에는 -type d를 사용해 ~/Desktop 디렉토리의 모든 디렉토리를 찾아봅시다.

$ find ~/Desktop -type d
/Users/johngrib/Desktop
/Users/johngrib/Desktop/2023
/Users/johngrib/Desktop/2024

이번에는 나의 ~/Desktop 디렉토리의 모든 하위 경로에 파일이 몇 개나 있는지 알아봅시다.

$ find ~/Desktop -type f | wc -l
53

53개의 파일이 있네요.

  • find ~/Desktop: ~/Desktop 디렉토리에서 찾아라
  • -type f : 파일만 찾아라
  • | wc -l: 출력된 결과가 몇 줄인지 출력해라

-name 과 -iname 옵션

53개는 좀 많으니까 필터링 조건을 추가해 봅시다.

-name을 써서 파일 이름의 확장자가 jpg인 것들만 골라보죠.

$ find ~/Desktop -type f -name "*.jpg"
/Users/johngrib/Desktop/2024/screenshot 2024-01-19 22.12.50.jpg
/Users/johngrib/Desktop/2024/screenshot 2024-01-20 18.31.12.jpg
/Users/johngrib/Desktop/2024/screenshot 2024-01-09 22.26.44.jpg
/Users/johngrib/Desktop/2024/screenshot 2024-01-20 16.39.12.jpg
/Users/johngrib/Desktop/2024/screenshot 2024-01-14 20.53.31.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.20.27.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-23 20.49.58 복사본.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-09-15 17.35.20.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-17 22.00.49.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-18 22.34.18.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-19 19.40.48.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-17 22.17.22.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.22.43.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-17 21.49.08.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-19 19.40.05.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-17 21.49.16.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-21 23.34.05.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-17 22.02.26.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-22 22.00.23.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.19.47.jpg
/Users/johngrib/Desktop/2023/IMG_9216.jpg

$ find ~/Desktop -type f -name "*.jpg" | wc -l
21
  • find ~/Desktop: ~/Desktop 디렉토리에서 찾아라
  • -type f : 파일만 찾아라
  • -name "*.jpg": 파일 확장자가 jpg인 파일만 찾아라
  • | wc -l: 결과가 몇 줄인지 출력해라

[[/cmd/wc]]로 한번 더 확인해보니 21개의 파일이 있었네요.

그런데 생각해보니 jpg 말고도 JPG 파일도 있을 것 같습니다.

-name 옵션은 대소문자를 구분합니다. 그래서 JPG는 나오지 않았을 것 같습니다. 대소문자를 구별하지 않는 -iname 옵션을 사용해봅시다.

$ find ~/Desktop -type f -iname "*.jpg"
/Users/johngrib/Desktop/2024/screenshot 2024-01-19 22.12.50.jpg
/Users/johngrib/Desktop/2024/screenshot 2024-01-15 22.43.45.JPG
/Users/johngrib/Desktop/2024/screenshot 2024-01-20 18.31.12.jpg
/Users/johngrib/Desktop/2024/screenshot 2024-01-20 16.39.12(2).JPG
/Users/johngrib/Desktop/2024/screenshot 2024-01-09 22.26.44.jpg
/Users/johngrib/Desktop/2024/screenshot 2024-01-20 16.39.12.jpg
/Users/johngrib/Desktop/2024/screenshot 2024-01-14 20.53.31.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.20.27.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-23 20.49.58 복사본.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-09-15 17.35.20.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-17 22.00.49.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-18 22.34.18.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-19 19.40.48.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-17 22.17.22.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.22.43.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-17 21.49.08.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-19 19.40.05.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-17 21.49.16.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-21 23.34.05.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-17 22.02.26.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-22 22.00.23.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.19.47.jpg
/Users/johngrib/Desktop/2023/IMG_9216.jpg

$ find ~/Desktop -type f -iname "*.jpg" | wc -l
23
  • find ~/Desktop: ~/Desktop 디렉토리에서 찾아라
  • -type f : 파일만 찾아라
  • -iname "*.jpg": 대소문자를 무시하고, 확장자가 jpg인 파일만 찾아라
  • | wc -l: 결과가 몇 줄인지 출력해라

JPG 파일이 2개가 더 나왔습니다.

-newermt

이번에는 -newermt옵션을 사용해 봅시다. 이 옵션을 쓰면 파일의 업데이트 메타데이터를 체크해서, 지정한 날짜 이후에 업데이트된 파일을 찾아줍니다.

$ find ~/Desktop -type f -iname '*.jpg' -newermt '2023-12-25'
/Users/johngrib/Desktop/2024/screenshot 2024-01-19 22.12.50.jpg
/Users/johngrib/Desktop/2024/screenshot 2024-01-15 22.43.45.JPG
/Users/johngrib/Desktop/2024/screenshot 2024-01-20 18.31.12.jpg
/Users/johngrib/Desktop/2024/screenshot 2024-01-20 16.39.12(2).JPG
/Users/johngrib/Desktop/2024/screenshot 2024-01-09 22.26.44.jpg
/Users/johngrib/Desktop/2024/screenshot 2024-01-20 16.39.12.jpg
/Users/johngrib/Desktop/2024/screenshot 2024-01-14 20.53.31.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.20.27.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-23 20.49.58 복사본.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.22.43.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.19.47.jpg
/Users/johngrib/Desktop/2023/IMG_9216.jpg
  • find ~/Desktop: ~/Desktop 디렉토리에서 찾아라
  • -type f : 파일만 찾아라
  • -iname '*.jpg': 파일 이름이 *.jpg인 파일만 찾도록 하고, 대소문자는 무시한다
  • -newermt '2023-12-25': 2023년 12월 25일 이후에 업데이트된 파일만 찾아라

조건을 부정하는 !

이제 조건에 NOT 조건을 씌우는 방법도 사용해 봅시다. 그냥 !를 쓰면 됩니다.

$ find ~/Desktop -type f -iname '*.jpg' -newermt '2023-12-25' ! -newermt '2024-01-01'
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.20.27.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-23 20.49.58 복사본.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.22.43.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.19.47.jpg
/Users/johngrib/Desktop/2023/IMG_9216.jpg
  • find ~/Desktop: ~/Desktop 디렉토리에서 찾아라
  • -type f : 파일만 찾아라
  • -iname '*.jpg': 파일 이름이 *.jpg인 파일만 찾도록 하고, 대소문자는 무시한다
  • -newermt '2023-12-25': 2023년 12월 25일 이후에 업데이트된 파일만 찾아라
  • ! -newermt '2024-01-01': 2024년 1월 1일 이전에 업데이트된 파일만 찾아라

[[/cmd/find]]는 이 외에도 다양한 검색 옵션들을 갖고 있습니다.

옵션에 따라 파일의 용량별, 시간별, 타입별로 다양한 방법의 조회가 가능합니다. 너무 많아서 여기서는 더 이상의 검색 옵션은 다루지 않도록 하겠습니다.

자세한 내용은 man find 명령어로 확인해 보거나, ChatGPT에게 물어보세요. [[/cmd/find]]가 수십년이나 된 명령이다 보니 ChatGPT도 이 명령을 아주 잘 알고 있더군요.

man find

조회 결과에 명령을 실행해주는 -exec

[[/cmd/find]]를 사용한 조회는 이제 충분히 익숙해졌다 치고, 다른 것을 해봅시다.

echo로 연습

[[/cmd/find#option-exec]]{-exec} 옵션을 사용하면 [[/cmd/find]]명령으로 찾은 결과에 대해 적용할 명령을 지정할 수 있습니다. 명령이 좀 길어질테니 \를 사용해서 명령을 여러 줄로 나눠서 써봅시다.

find ~/Desktop -type f -iname '*.jpg' \
  -newermt '2023-12-25' \
  ! -newermt '2024-01-01' \
  -exec echo {} \;

위의 명령을 실행해보니 다음과 같이 출력되는군요. (둘째줄부터 나타나는 >는 입력칸이라고 알려주는 프롬프트이니 따라치지 마세요.)

$ find ~/Desktop -type f -iname '*.jpg' \
>   -newermt '2023-12-25' \
>   ! -newermt '2024-01-01' \
>   -exec echo {} \;
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.20.27.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-11-23 20.49.58 복사본.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.22.43.jpg
/Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.19.47.jpg
/Users/johngrib/Desktop/2023/IMG_9216.jpg
  • find ~/Desktop: ~/Desktop 디렉토리에서 찾아라
  • -type f : 파일만 찾아라
  • -iname '*.jpg': 파일 이름이 *.jpg인 파일만 찾도록 하고, 대소문자는 무시한다
  • -newermt '2023-12-25': 2023년 12월 25일 이후에 업데이트된 파일만 찾아라
  • ! -newermt '2024-01-01': 2024년 1월 1일 이전에 업데이트된 파일만 찾아라
  • -exec echo {} \;: 찾은 파일에 하나하나에 대해 echo {} 명령을 실행해라
    • {}는 placeholder 입니다. 즉, 찾은 파일의 이름이 여기에 들어갑니다.
    • \;-exec 옵션의 끝을 알리는 표시입니다.

[[/cmd/echo]]는 그냥 출력만 하니까, 사실 위의 명령 파이프라인에서는 -exec echo {} \;는 불필요하게 덧붙인 옵션이라고 할 수 있습니다.

wc, du로 연습

하지만 다음과 같이 [[/cmd/wc]]를 사용한다면 byte 사이즈를 알 수 있을 것입니다.

$ find ~/Desktop -type f -iname '*.jpg' \
>   -newermt '2023-12-25' \
>   ! -newermt '2024-01-01' \
>   -exec wc -c {} \; \
>   | sort -n
  191081 /Users/johngrib/Desktop/2023/IMG_9216.jpg
  220545 /Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.20.27.jpg
  250355 /Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.19.47.jpg
  425308 /Users/johngrib/Desktop/2023/screenshot 2023-11-23 20.49.58 복사본.jpg
 1140371 /Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.22.43.jpg
  • -exec wc -c {} \;: 찾은 파일에 하나하나에 대해 wc -c {} 명령을 실행해 용량을 출력해라
  • | sort -n: [[/cmd/sort]] 명령를 사용해 오름차순으로 정렬해라

지정한 날짜 범위 내에서 어떤 jpg 파일이 제일 용량이 큰 지를 조사한 셈이 되었습니다.

물론 [[/cmd/du]]를 써서 사람이 읽기 좋은 용량 사이즈를 표시해보는 것도 괜찮은 연습이 되겠네요.

$ find ~/Desktop -type f -iname '*.jpg' \
>   -newermt '2023-12-25' \
>   ! -newermt '2024-01-01' \
>   -exec du -h {} \;
216K    /Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.20.27.jpg
416K    /Users/johngrib/Desktop/2023/screenshot 2023-11-23 20.49.58 복사본.jpg
1.1M    /Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.22.43.jpg
248K    /Users/johngrib/Desktop/2023/screenshot 2023-12-27 21.19.47.jpg
188K    /Users/johngrib/Desktop/2023/IMG_9216.jpg

cp로 연습

이번에는 이렇게 조회한 파일들을 ~/test 경로로 복사해보겠습니다.

먼저 ~/test 디렉토리를 만들어 줍시다.

mkdir ~/test

그리고 [[/cmd/find]]의 -exec옵션에 파일을 복사하는 cp 명령을 써서 파일을 복사해봅시다.

$ find ~/Desktop -type f -iname '*.jpg' \
>    -newermt '2023-12-25' \
>    ! -newermt '2024-01-01' \
>    -exec cp {} ~/test \;
  • -exec cp {} ~/test \;: 찾은 파일에 하나하나에 대해 cp {} ~/test 명령을 실행해라

이제 ~/test 디렉토리를 확인해 보면 복사된 파일들이 있습니다.

$ ls -alh ~/test
total 2.2M
drwxr-xr-x   8 johngrib staff  256 2024-01-21 Sun 17:50:22  .
drwxr-x---+ 84 johngrib staff 2.7K 2024-01-21 Sun 17:46:14  ..
-rw-r--r--   1 johngrib staff 187K 2024-01-21 Sun 17:50:22  IMG_9216.jpg
-rw-r--r--   1 johngrib staff 416K 2024-01-21 Sun 17:50:22 'screenshot 2023-11-23 20.49.58 복사본.jpg'
-rw-r--r--   1 johngrib staff 245K 2024-01-21 Sun 17:50:22 'screenshot 2023-12-27 21.19.47.jpg'
-rw-r--r--   1 johngrib staff 216K 2024-01-21 Sun 17:50:22 'screenshot 2023-12-27 21.20.27.jpg'
-rw-r--r--   1 johngrib staff 1.1M 2024-01-21 Sun 17:50:22 'screenshot 2023-12-27 21.22.43.jpg'

-exec의 종결 표시자 ; 와 +

바로 위에서 -exec 옵션의 종결 표시자로 \;를 사용했습니다.

그런데 이 옵션의 종결 표시자는 \;만 있는 게 아니라 +도 있습니다.

  • \; : 찾은 파일 하나하나에 대해 -exec로 지정한 명령을 실행합니다.
  • + : -exec로 지정한 명령 하나에 대해 찾은 파일을 모두 넘겨줍니다.

\;를 사용했을 때 검색된 파일이 100개라면 -exec로 지정한 명령이 100번 실행되지만, +를 사용했을 때는 -exec로 지정한 명령이 1번 실행되고, 찾은 파일 100개를 한꺼번에 넘겨줍니다.

다음 두 명령을 실행해보며 결과를 비교해 봅시다.

find ~/Desktop -type f -exec echo {} \;
find ~/Desktop -type f -exec echo {} +

만약 [[/cmd/find]]로 찾은 결과가 a, b, c 이렇게 세가지라 할 때, -exec echo {} \;를 사용하면 다음과 같이 3개의 명령을 연달아 실행한 것과 똑같습니다.

echo a
echo b
echo c

반면 -exec echo {} +를 사용하면 다음과 같이 명령을 하나 실행한 것과 똑같습니다.

echo a b c

따라서 여러 파일을 한꺼번에 입력 인자로 받을 수 있는 명령이라면 \;가 아니라 +를 사용하면 더 나은 성능을 보이는 경우가 많습니다.

파일의 형식을 판별해주는 [[/cmd/file]]로 실험을 해봅시다.

find ~/Downloads -type f -exec file {} \;
find ~/Downloads -type f -exec file {} +

~/Downloads에는 보통 많은 파일이 있기 때문에 위의 두 명령을 실행해봤을 때 체감상 차이를 느낄 수 있을 것입니다.

하지만 파일의 수가 적은 사람도 있을 수 있고, 느낌은 확실하지 않으니까 time 명령을 사용해서 시간을 측정해봅시다.

$ time find ~/Downloads -type f -exec file {} \; > /dev/null 2> /dev/null

real    0m0.147s
user    0m0.044s
sys     0m0.084s
$ time find ~/Downloads -type f -exec file + > /dev/null 2> /dev/null

real    0m0.007s
user    0m0.001s
sys     0m0.004s

\;를 썼을 땐 0.147초가 걸렸지만 +를 썼을 땐 0.007초가 걸렸습니다.

이번에는 소요시간만 보고 싶고, [[/cmd/file]]가 출력한 결과는 관심이 없어서 표준출력과 표준에러출력을 /dev/null로 리다이렉트했습니다.

한편 1, 2를 모두 /dev/null로 리다이렉팅했는데도 측정한 시간이 출력되는 것을 보면 time 명령이 측정 결과를 [[/cmd/tty]]{/dev/tty}로 보내고 있음을 추측할 수 있습니다.

명령의 퍼포먼스를 체크하는 time의 특성을 잘 생각해보면 [[/cmd/tty]]{/dev/tty}로 출력을 보내는 이유를 알 수 있습니다. time은 측정하려는 명령 파이프라인의 퍼포먼스를 측정하기만 해야 하고, 1번 출력이나 2번 출력으로 측정한 결과를 내보내서 해당 명령의 출력을 오염시키면 안 되기 때문입니다.

만약 time이 1번 출력으로 측정 결과를 내보낸다면, 명령 파이프라인 마지막에 >로 파일 리다이렉팅을 할 때 측정 결과가 파일에 저장되어 버릴 것입니다.

xargs 명령

[[/cmd/xargs]]는 '수직 리스트'를 '수평 리스트'로 바꿔주는 명령입니다. 단순한 기능을 갖고 있으면서도 매우 강력하고 유용합니다.

이 훌륭한 명령은 직접 써봐야 진가를 알 수 있습니다.

먼저 [[/cmd/seq]]를 써서 1부터 10까지의 숫자를 출력해보겠습니다.

$ seq 1 10
1
2
3
4
5
6
7
8
9
10

여기에 [[/cmd/xargs]]를 파이프로 연결해주면 수직 리스트가 수평 리스트로 바뀝니다.

$ seq 1 10 | xargs
1 2 3 4 5 6 7 8 9 10

[[/cmd/xargs#option-n]]{-n 옵션}을 주면 지정한 갯수만큼 출력한 다음, 아랫줄로 내려가서 다시 지정한 갯수만큼 출력합니다.

$ seq 1 10 | xargs -n 3
1 2 3
4 5 6
7 8 9
10

$ seq 1 10 | xargs -n 4
1 2 3 4
5 6 7 8
9 10

find 와 조합해 사용하기

이제 [[/cmd/find]] 명령을 써서 다음과 같이 10개의 html 파일을 출력했다고 합시다.

$ find ~/johngrib.github.io -name '*.html' | head
/Users/johngrib/johngrib.github.io/_includes/post-tags.html
/Users/johngrib/johngrib.github.io/_includes/createLink.html
/Users/johngrib/johngrib.github.io/_includes/latex.html
/Users/johngrib/johngrib.github.io/_includes/pagination.html
/Users/johngrib/johngrib.github.io/_includes/createTable.html
/Users/johngrib/johngrib.github.io/_includes/comment.html
/Users/johngrib/johngrib.github.io/_includes/footer.html
/Users/johngrib/johngrib.github.io/_includes/header.html
/Users/johngrib/johngrib.github.io/_includes/searchbox.html
/Users/johngrib/johngrib.github.io/index.html

만약 파일들 각각이 몇 줄로 되어있는지 조사하고 싶다면 어떻게 하면 좋을까요?

위에서 공부한 것과 같이 [[/cmd/find]]의 -exec와 [[/cmd/wc]]를 응용하는 방법이 있을 것입니다.

$ find ~/johngrib.github.io -type f -name '*.html' -exec wc -l {} \; 2>/dev/null | head
       3 /Users/johngrib/johngrib.github.io/_includes/post-tags.html
       2 /Users/johngrib/johngrib.github.io/_includes/createLink.html
       2 /Users/johngrib/johngrib.github.io/_includes/latex.html
      96 /Users/johngrib/johngrib.github.io/_includes/pagination.html
      61 /Users/johngrib/johngrib.github.io/_includes/createTable.html
      20 /Users/johngrib/johngrib.github.io/_includes/comment.html
       6 /Users/johngrib/johngrib.github.io/_includes/footer.html
     107 /Users/johngrib/johngrib.github.io/_includes/header.html
       5 /Users/johngrib/johngrib.github.io/_includes/searchbox.html
      68 /Users/johngrib/johngrib.github.io/index.html

각 파일이 몇 줄로 되어 있는지를 조사하는 데 성공했습니다.

그런데 이걸 실행해보면 [[/cmd/find]]가 좀 느리다는 것을 실감할 수 있습니다. -exec 때문입니다.

$ time find ~/johngrib.github.io -type f -name '*.html' 2>&1 >/dev/null | head

real    0m0.121s
user    0m0.014s
sys     0m0.107s

$ time find ~/johngrib.github.io -type f -name '*.html' -exec wc -l {} \; 2>&1 >/dev/null | head

real    0m1.477s
user    0m0.271s
sys     0m0.963s

-exec를 쓰지 않았을 때에 비해 10배 이상 느려졌습니다. -exec를 쓰지 않고 라인 카운트를 할 방법을 생각해 봅시다.

이럴 때 사용하는 것이 바로 [[/cmd/xargs]]입니다.

$ find ~/johngrib.github.io -type f -name '*.html' | head | xargs wc -l
       3 /Users/johngrib/johngrib.github.io/_includes/post-tags.html
       2 /Users/johngrib/johngrib.github.io/_includes/createLink.html
       2 /Users/johngrib/johngrib.github.io/_includes/latex.html
      96 /Users/johngrib/johngrib.github.io/_includes/pagination.html
      61 /Users/johngrib/johngrib.github.io/_includes/createTable.html
      20 /Users/johngrib/johngrib.github.io/_includes/comment.html
       6 /Users/johngrib/johngrib.github.io/_includes/footer.html
     107 /Users/johngrib/johngrib.github.io/_includes/header.html
       5 /Users/johngrib/johngrib.github.io/_includes/searchbox.html
      68 /Users/johngrib/johngrib.github.io/index.html
     370 total

똑같은 결과가 나왔지만… 속도는 훨씬 빠릅니다.

$ time find ~/johngrib.github.io -type f -name '*.html' | head | xargs wc -l
       3 /Users/johngrib/johngrib.github.io/_includes/post-tags.html
       2 /Users/johngrib/johngrib.github.io/_includes/createLink.html
       2 /Users/johngrib/johngrib.github.io/_includes/latex.html
      96 /Users/johngrib/johngrib.github.io/_includes/pagination.html
      61 /Users/johngrib/johngrib.github.io/_includes/createTable.html
      20 /Users/johngrib/johngrib.github.io/_includes/comment.html
       6 /Users/johngrib/johngrib.github.io/_includes/footer.html
     107 /Users/johngrib/johngrib.github.io/_includes/header.html
       5 /Users/johngrib/johngrib.github.io/_includes/searchbox.html
      68 /Users/johngrib/johngrib.github.io/index.html
     370 total

real    0m0.037s
user    0m0.007s
sys     0m0.038s

[[/cmd/xargs]]는 표준입력으로 받은 목록을 [[/cmd/wc]]{wc -l} 오른쪽에 수평 리스트로 붙여준 것입니다.

-I 옵션과 placeholder

[[/cmd/xargs]]는 -I 옵션과 placeholder 기호를 통해 입력할 곳을 지정할 수 있습니다.

placeholder 기호는 보통 {}를 사용합니다. 다른 것을 쓸 수도 있는데, 대부분이 {}를 씁니다. 그래서 같이 일하는 다른 사람들을 헷갈리지 않게 하고 싶다면 그냥 {}를 쓰는 것이 좋습니다.

$ seq 10 | xargs -I {} echo hello, {} ...
hello, 1 ...
hello, 2 ...
hello, 3 ...
hello, 4 ...
hello, 5 ...
hello, 6 ...
hello, 7 ...
hello, 8 ...
hello, 9 ...
hello, 10 ...

$ seq 10 | xargs -I {} echo {} is {} ...
1 is 1 ...
2 is 2 ...
3 is 3 ...
4 is 4 ...
5 is 5 ...
6 is 6 ...
7 is 7 ...
8 is 8 ...
9 is 9 ...
10 is 10 ...
  • seq 10: 1부터 10까지의 숫자를 출력합니다.
  • xargs -I {}: placeholder로 {}를 사용하겠다고 선언합니다.
  • echo hello, {} ...: [[/cmd/echo]]를 구성합니다. placeholder가 1개 있어서 인자가 한 번 입력됩니다.
    • 출력된 부분을 강조하기 위해 ...를 붙였습니다.
  • echo {} is {} ...: [[/cmd/echo]]를 구성합니다. placeholder가 2개 있어서 인자가 두 번 입력됩니다.

물론 {} 말고 다른 걸 써도 됩니다.

$ seq 3 | xargs -I @ echo ... @ ...
... 1 ...
... 2 ...
... 3 ...

@를 써봤습니다.

$ seq 3 | xargs -I number echo ... number ...
... 1 ...
... 2 ...
... 3 ...

문자 한 개가 아니라 여러개여도 된다는 것을 보여주기 위해 number를 써봤습니다.

하지만 역시 {}가 좀 덜 헷갈리네요. 매일 {}로 써서 그런 것 같습니다.

-P 옵션으로 병렬 처리하기

[[/cmd/xargs]]의 훌륭한 점은 -P 옵션으로 병렬 처리를 할 수 있다는 것입니다. 물론 병렬처리가 늘 그렇듯이 순서는 보장되지 않습니다.

$ seq 10 | xargs -P4 -I {} echo hello, {} ...
hello, 1 ...
hello, 2 ...
hello, 4 ...
hello, 3 ...
hello, 5 ...
hello, 6 ...
hello, 7 ...
hello, 8 ...
hello, 9 ...
hello, 10 ...
  • seq 10: 1부터 10까지의 숫자를 출력합니다.
  • xargs -P4: 4개의 프로세스를 사용해서 병렬 처리합니다.
  • xargs -I {}: placeholder로 {}를 사용하겠다고 선언합니다.
  • echo hello, {} ...: [[/cmd/echo]] 출력문을 구성합니다.

-P 옵션은 병렬 처리할 프로세스의 개수를 지정합니다.

-P4는 4개의 프로세스를 사용해서 병렬 처리하겠다는 뜻입니다. -P4로 붙여서 써도 되고 -P 4처럼 띄어서 써도 됩니다.

그리고 -P0과 같이 0을 지정하면 사용 가능한 최대 프로세스 개수를 사용합니다.

다음은 -P4를 사용한 경우와 -P를 사용하지 않은 경우의 퍼포먼스를 time 명령어로 비교한 것입니다.

$ time seq 1000 | xargs -P 4 -I {} echo hello, {} ... > /dev/null

real    0m0.407s
user    0m0.165s
sys     0m0.733s

$ time seq 1000 | xargs -I {} echo hello, {} ... > /dev/null

real    0m0.953s
user    0m0.143s
sys     0m0.732s

real을 보면 -P4가 대충 두 배 이상 빨랐네요.

-p 옵션으로 confirm 을 받도록 하기

[[/cmd/xargs]]에서 정말 유용합 옵션인 -p 옵션을 소개하고 xargs에 대한 공부를 마무리하도록 하겠습니다.

-p 옵션을 쓰면 명령을 실행하기 전에 confirm을 받도록 할 수 있습니다.

프롬프트가 뜨면 yn을 입력해서 확인을 해주면 됩니다.

다음은 2에 대해서만 y를 입력하고, 나머지는 모두 n을 입력한 경우입니다.

$ seq 3 | xargs -p -I {} echo hello, {} ...
echo hello, 1 ...?...n
echo hello, 2 ...?...y
hello, 2 ...
echo hello, 3 ...?...n

hello, 2...만 출력됐습니다.

confirm을 받는 문구 때문에 좀 헷갈리니까, 표준출력을 파일로 리다이렉팅해서 확인해보겠습니다.

$ seq 3 | xargs -p -I {} echo hello, {} ... > result.txt
echo hello, 1 ...?...n
echo hello, 2 ...?...y
echo hello, 3 ...?...n

$ cat result.txt
hello, 2 ...

결과에는 hello, 2 ...만 출력됐습니다.

이전, 다음 문서

  • 이전 문서: [[/mac/terminal-guide/02]]
  • 다음 문서: [[/mac/terminal-guide/04]]