GNU find 명령 사용법

GNU find는 디렉토리 트리를 따라 각각의 파일 이름을 주어진 표현식들에 적용하여 매칭되는 결과를 프린트하거나 이를 이용해 별도의 명령을 실행할 수 있다. 간단한 파일 찾기 명령이라고 생각할 수 있지만, find는 실제로 엄청 다양한 일을 할 수 있으며 쉘을 통한 시스템 관리에 있어서 가장 유용하고 필수적인 도구라 할 수 있다. 실제로 grep의 경우 이를 drop in으로 대체하려는 프로젝트들도 많이 있지만, GNU find의 경우에는 이를 100% 커버할 수 있는 대체품이 아직 나오지 않은 상황이다. fd라는 툴이 빠른 속도와 편의성을 개선하여 인기를 얻고 있지만 모든 기능을 대체할 수는 없다.

참고로, GNU find는 그 이름에 있어서 너무 일반적인 동사를 사용하고 있어서 관련 자료를 찾으려 할 때 상관없는 문서가 너무 많이 검색된다. 이 때에는 “GNU find”라고 검색하는 것이 도움이 될 것이다. 이 글의 이후에서는 find를 쓰도록 할 것이다. 또한 윈도에서도 find.exe 라는 도구가 포함되어 있는데, 이것은 grep 처럼 파일에서 텍스트를 검색하는 도구이다. msys를 설치하면 윈도 명령 프롬프트에서도 GNU find를 사용할 수 있지만, 둘의 이름이 갖기 때문에 한쪽의 이름을 바꿔둬야 한다.

사용법

find [-H] [-L] [-P] [-D debugopts] [-Olevel] [path...] [expression]

find는 단순히 파일을 찾는 것 뿐만 아니라, ‘어떻게 처리할 것인가’에 대한 표현식을 처리할 수 있는 작은 스크립트 언어처리기처럼 작동할 수 있다. 명령의 구조는 링크를 다룰 옵션과 디버그 옵션, 최적화수준 정도의 간단한 옵션이 있으며, 시작 위치를 기준으로 여러 표현식으로 필터링과 처리 방법을 지정하게 된다.

옵션

-H, -L, -P는 심볼릭 링크를 어떻게 다룰 것인지를 결정한다. 이후에 오는 인자들은 시험할 파일이나 디렉토리의 이름들이다. 이후에 -, (, !로 시작하는 인자를 만나면 여기서부터는 조건식으로 평가되며 이들은 찾고자 하는 조건이 된다. 만약 경로가 주어지지 않으면 현재 경로를 기준으로 찾는다. 또한 표현식이 주어지지 않으면 기본적으로 -print가 지정된다.

심볼릭링크

하나 이상의 옵션이 붙으면 맨 뒤의 옵션이 적용된다.

-P: 심볼릭 링크 무시

-L: 심볼릭 링크를 따라간다.

-H: 명령줄 인자를 제외한 심볼릭 링크를 따르지 않는다.

디버그 옵션 및 최적화 수준

-D debugopts:

  • help: 디버그 옵션을 보여준다.
  • tree: 표현식 트리를 보여준다.
  • stat: 파일들에 대한 stat 시스템 콜을 보여준다.
  • opt: 표현식 트리에 대한 분석 정보를 표시한다.
  • rates: 예측치에 대한 평가를 표시한다.

-Olevel: 최적화 정도를 지정할 수 있다. (사실 정확히 무슨 의미인지는 모르겠당…)

표현식

표현식은 탐색 조건들과 테스트 그리고 액션으로 구성된다. -and는 생략된 연산자로 취급한다.

옵션

  • -d: -depth의 약어. 해당 디렉토리를 처리하기 이전에 하위 디렉토리를 먼저 처리하도록 한다. -delete 액션은 이 옵션을 기본적으로 갖는다.
  • -daystart: 시간 관련 테스트(-amin, -atime, -cmin, -ctime, -mmin, -mtime)의 기준 시간을 24시간 이전이 아닌 오늘 자정으로 쓴다.
  • -maxdepth levels: 하위 디렉토리 처리를 제한한다.
  • -mindepth levels: 주어진 디렉토리만큼 내려간 디렉토리만 처리한다.
  • -noleaf: UNIX 파일 시스템이 아닐 때. 이는 디렉토리가 기본적으로 이름과 “.” 이라는 두 개의 노드를 가지고 있다고 가정하고 최적화하는데, 이 옵션은 이 최적화를 쓰지 않는다. MS-DOS 등에서 성능이 좋아진다.
  • -regextype type: 정규식 사용시의 정규식의 타입. emacs가 기본이며, posix-awk, posix-basic, posix-egrep, posix-extended 등에서 고르면 된다.

TEST

  • -newerXY-samefile과 같은 일부 테스트들은 현재 파일과 주어진 파일을 비교한다. 테스트는 반복되겠지만, 인자로 주어진 파일은 첫 명령 입력시에 파싱되어 한번만 쓰인다.
  • 아래에 n으로 쓰는 것은 숫자값인데 앞에 +/- 붙어서 보다 크다, 보다 작다를 의미할 수 있다.
  • -amin n : n 분 전에 최종 액세스한 파일
  • -anewer file: 주어진 파일이 수정된 시각보다 이후에 액세스된 파일.
  • -atime n: n * 24시간 이전에 최종 액세스된 파일. 따라서 atime +1 하면 이틀 전에 액세스한 파일이 된다.
  • -cmin n: n 분 전에 변경된 파일
  • -cnewer file: 주어진 파일의 수정 시간 이후에 수정되었는지
  • ctime n: n * 24 시간 이전에 변경된 파일.
  • -empty: 빈 파일 혹은 디렉토리
  • -executable: 실행가능한지
  • -false : 무조건 실패
  • fstype type: 파일 시스템 타입
  • -ipath pattern: 쓰이지 않으며 -iwholename과 동일하다.
  • -iregex pattern: 대소문자 구분없는 정규식 매칭
  • -iwholename pattern: -wholename과 같으나 대소문자 무시
  • -mmin n: 파일이 n 분 전에 수정됨
  • -mtime n: 파일이 어제기준 24 * n 시간 전에 수정됨.
  • -name pattern: 파일의 베이스 이름(디렉토리 이름 뗀)이 주어진 패턴과 매치하는지 본다.
  • -newer file: 주어진 파일보다 최근에 수정되었는지
  • -newerXY reference: 주어진 레퍼런스보다 최근인지
  • -path pattern: 전체 경로가 주어진 패턴인지 본다. 이 때, ‘/’, ‘.’ 는 특별한 의미를 가지지 않는다.
  • -regex pattern: 파일의 전체 경로를 주어진 패턴과 매치되는지 검사한다.
  • -samefile name: 주어진 이름의 파일과 같은 파일인지 검사한다.
  • -size n[cwbkMG]: 특정 크기인지 본다. 보다 크기거나 작거나를 정할 수 있다.
  • -true: 항상 참
  • -type c: 타입을 검사한다.
  • -used n : 상태가 변경된지 n 일 이후에 사용되었는지를 확인한다.
  • -wholename pattern: 전체 경로를 비교한다. -path와 같은 옵션이다.

액션

기본적으로 액션은 파일 명들을 출력한다.

  • -print : 기본 액션. 테스트를 통과한 파일명들을 출력한다.
  • -fprint FILE: 파일에 결과를 쓴다.
  • -ls: ls -dils 포맷을 출력해준다.
  • -fls FILE: 파일에 -ls 옵션의 결과를 쓴다.
  • -print0: 개행이 아닌 널 문자로 구분하여 출력한다. 이는 xargs로 결과를 넘겨서 쓸 때 유용하다.
  • -printf, -fprintf FORMAT: 주어진 포맷에 맞추어 출력한다.
  • -prune : 만약 발견한 파일이 디렉토리라면, 그 아래로 내려가지 않는다. 만약 -depth가 있다면 무시되며, -depth가 우선한다.
  • -delete: 발견된 모든 결과를 제거한다. 사용시 매우 주의가 필요하며 (까딱 하위 디렉토리 내 파일까지 다 날려버린다든지…) 꼭 출력해 본 다음에 실행할 것
  • -exec COMMAND ; : 주어진 명령을 실행한다. ;으로 꼭 끝나야 하는데 이는 이스케이프될 필요가 있다. (MS-DOS에서는 안해도 되더라) 파일의 이름이 들어갈 자리는 {} 으로 지정해주면 된다.
  • -exec COMMAND {} + : 이는 각각의 파일 이름을 뒤에다 붙이는 식으로 파일 개수보다 생성되는 명령의 수가 적다.
  • -execdir COMMAND ; : -exec는 시작 디렉토리 기준으로 명령을 실행하는데 -execdir은 매 서브 디렉토리에서 명령을 수행한다.

연산자

우선순위가 높은 것부터 쓴다. 위의 표현식들은 EXPR로 표기한다.

  • ( EXPR ): 괄호로 둘러싼 영역을 우선 평가한다.
  • ! EXPR, -not EXPR : 결과를 반대로
  • EXPR1 EXPR2: 두 표현식이 나란히 써지면 AND로 연결한다.
  • EXPR1 -a EXPR2 : AND로 연결
  • EXPR1 -o EXPR2 : OR로 연결한다. 만약 앞쪽의 내용이 참이면 뒤 쪽은 평가되지 않는다.
  • EXPR1 , EXPR2 : 두 표현식을 리스트로 만든다. 앞 표현식의 값과 상관없이 두 개의 표현식은 항상 평가되며, 앞의 결과는 그냥 무시된다.

예제

몇 가지 실행 예제를 소개하겠다. 다음은 가장 간단한 형태로, 특정 경로 아래에 있는 파일들을 모두 삭제하는 명령이다.

특정 파일들을 한꺼번에 삭제하기 (배치실행 방식과 반복실행 방식)

find /tmp -name core -type f -print | xargs /bin/rm -f

/tmp 하위의 core라는 이름의 파일을 모두 찾은 다음 이들을 몽땅 지운다. 단, 이 명령은 파일 이름에 공백이 들어있거나 하면 xargs에서 파싱을 잘못하므로 실패할 가능성이 있다. 이 문제를 방지하려면, 각각의 결과 이름을 공백으로 분리하지 않고 NULL문자로 대체하여 다음과 같이 실행한다.

find /tmp -name core -type f -print0 | xargs -0 /bin/rm -f

파이프와 xargs를 이용하면, 모든 결과를 취합하여 이를 하나의 명령으로 처리하게 되는데, 이 방식 외에도 find는 -exec 액션을 사용하여, 매 결과에 대해서 각각 명령을 처리할 수 있다.

find . -type f -exec file {} \;

위 명령은 모든 파일을 찾아서 file 명령을 적용한다.

복합 명령 적용하기

다음의 예제는 일종의 분기를 구현한 것이다. 콤마로 연결한 두 표현식(액션)은 앞 표현식의 결과에 상관없이 모두 평가되며 최종적으로 뒤의 것으로 평가된다. 이를 통해 한 번의 find 명령으로 여러 가지 액션을 선택적으로 취하게 할 수 있다.

find / \
  \( -perm -4000 -fprintf /root/suid.txt %#m %u %p\n \) , \
  \( -size +100M -frpintf /root/big.txt %-10s %p\n \)

위 명령은 ( ) 를 사용해서 두 개의 액션을 순차적으로 실행해서, 한 번의 명령으로 두 번의 find를 실행한 것과 동일한 효과를 낸다. 대신 전체 탐색은 한 번만 수행하므로 두 번 실행하는 것보다는 수행 시간이 훨씬 짧다.

  1. 첫 번째 그룹에서는 권한값이 4000 미만인지를 기준으로 필터링하고, 그 결과를 /root/suid.txt 파일에 기록하며,
  2. 두 번째 그룹에서는 크기가 100MB 이상이면 /root/big.txt에 기록한다.

find에서 모든 액션은 표현식이므로 이런식으로 조합하여 다양한 효과를 낼 수 있다. 다음 명령의 경우에는 JPG파일과 PNG 파일을 찾아서 각각 다른 파일에 그 이름을 기록해준다.

find /d/pics \( -type f -name *.jpg -fprint jpg.txt \) , \
\( -type f -name *.png -fprint png.txt \)

시간과 관련된 필터링

-*time, -*min 으로 최근에 수정/액세스한 파일을 필터링할 수 있으며, -anewer, -mnewer 등으로 특정 파일보다 이후에 액세스되었거나 수정된 파일을 찾을 수 있다.

find ~ -mtime 0 으로 홈 디렉토리 내에서 24시간 이내에 수정된 파일을 찾을 수 있다.

권한과 관련된 필터링

find /sbin /usr/sbin -executable \! -readable -print 

/sbin, /usr/sbin 에서 실행 가능하지만, 읽을 수 없는 파일을 찾아서 출력. \!-not과 같은 연산으로 -not -readable로 해석할 수 있다. 다음과 같이 -perm을 사용하여 특정 권한의 파일만 찾을 수도 있다.

find . -perm 664 

다음의 예제는 조금 복잡해 보일 수 있는데…. 특정 조건에 맞는 파일을 다른 위치로 백업하는 명령이다.

cd /source-dir
find . -name .snapshot -prune -print0 -o \( \! -name *~ -print0 \) | cpio -pmd0 /dest-dir
  1. -name .snapshot : 이름이 .snapshot 인지 체크한다.
  2. -prune : 디렉토리라면 그 아래로 내려가지 않는다. (해당 디렉토리만 선택된다)
  3. -print0 : 그리고 해당 디렉토리를 출력한다.
  4. -o : OR
  5. \( -not -name "*~" -print0 \) : .snapshot 이 아니라면 이름이 ~으로 끝나지 않는 모든 파일을 NULL문자종결로 출력한다.
-name .snapshot  # 이름이 .snapshot 인가?
      -prune     # .snapshot 이 디렉토리라면 더 이상 하위 레벨로 들어가지 마라.
      -o         # 이름이 .snapshot이 아닐 때만 true 이다.
       \( \! -name *~  # 백업파일이 아니면 (이름이 ~로 끝나지 않는다면)
             -print0   # 널문자로 구분하여 출력한다. 
       \)

즉 이 명령은 현재 위치에서 하위 경로의 .snapshot 디렉토리와, 그 외의 파일 중에서는 백업 파일이 아닌 모든 파일을 다른 곳으로 복사하여 백업하는 명령이다. 전체적으로 상당히 복잡한데, 조금 더 간단한 예를 들어보자.

만약 .vim 디렉토리에서 번들의 .git 을 제외하고 탐색하는 방법을 이를 응용해보면 다음과 같이 쓸 수 있다. (다만, 각각의 .git 은 출력된다.)

find ~/.vim -name .git -prune -o -type f -name "*.vim"

.git 디렉토리들을 화면에 출력하지 않으려면 -fprint로 임의의 파일로 보내어 화면에는 .vim 파일들만 보이게 할 수 있다.

find ~/.vim -name .git -prune -fprint skipped.txt -o -type f -name *.vim -print

물론, .git 내부에는 .vim 파일이 들어있지는 않겠지만, -prune을 사용하여 가드하면 해당 디렉토리 아래를 훑어보는 과정을 생략하기 때문에 전체 수행 시간을 매우 단축할 수 있다.

복해서 말하지만, 각각의 액션들은 모두 평가식이며 -a , -o 및 , 연산자를 통해서 조합할 수 있다. 따라서 다음과 같은 식으로도 명령을 구성할 수 있다.

find repo/ \
-exec \test -d {}/.svn -o -d {}/.git -o -d {}/CVS ; \
-print -prune

두 번째 라인 전체가 -exec 평가식이며, 여기서 쓰인 -otest 명령의 옵션이다. (역시 의미는 OR) 액션은 명령이 에러 없이 끝나면 항상 참으로 평가되므로,  -exec 절에 의해서 test 명령이 true (0)를 리턴했다면, 이 절은 참으로 평가된다. 이어지는 -print -prune은 암묵적으로 and로 연결된다.

따라서 test 명령으로 조건을 체크하고 참인 경우에만 뒤의 절들이 실행되며, 이들 디렉토리는 이름만 출력하고 내부로 내려가지는 않게 될 것이다.