Featured image of post [Bash Shell] 37. find - 파일 탐색과 조건 필터

[Bash Shell] 37. find - 파일 탐색과 조건 필터

디렉터리 트리를 순회하며 이름·타입·시간·크기 조건(predicate)을 평가해 파일을 걸러내는 find의 정신 모델과 사용법, find -print0 | xargs -0 안전 패턴, -exec {} \; 대 {} + 성능 차이, -prune, GNU·BSD 이식성 차이를 예제로 정리합니다.

find는 리눅스·유닉스에서 지정한 경로 아래를 재귀적으로 순회하며, 이름·타입·수정 시간·크기 등 조건(predicate)에 맞는 파일·디렉터리를 찾아내는 명령어다. 찾아내는 것으로 끝나지 않고 -exec·-delete로 “찾은 뒤 무엇을 할지"까지 한 명령으로 표현할 수 있어, 스크립트·백업·정리 작업에서 핵심적으로 쓰인다.

이 장을 읽기 전에

선행 챕터: 이 장은 36장: chmod, chown에서 이어진다. Part 6(파일 시스템과 권한)의 두 번째 챕터로, 36장에서 권한·소유자를 다루는 대상 자체를 어떻게 특정하는지가 필요해진다 — find는 바로 그 “대상이 될 파일들을 조건으로 골라내는” 역할을 맡는다. 21장: xargs를 먼저 읽었다면, 그 장에서 예고만 됐던 find ... -print0 | xargs -0 안전 패턴을 이 장에서 실제로 완결한다.

이 장의 깊이: 입문–중급 난이도다. 이름·타입·시간·크기 조건 조합부터 -exec·-delete로 찾은 결과를 바로 처리하는 법까지 실무에서 매일 쓰는 범위를 다루고, -prune·-regextype 같은 GNU 확장은 고급 구간으로 함께 짚는다. 다루지 않는 것: xargs의 세부 옵션(-I, -P, 배치 크기 등)은 21장: xargs에서, 찾아낸 파일을 실제로 압축·해제하는 법은 38장: gzip에서 다룬다.

당신의 수준에 맞는 경로

수준읽을 부분핵심 목표
조건에 맞는 파일을 바로 찾아 처리하고 싶은 사람개요+정신 모델, 사용법·옵션의 “이름·경로”·“타입”·“시간”, 예시의 “이름으로 검색”~“exec·삭제”이름·타입·시간 조건을 조합해 원하는 파일을 찾고 -exec·-delete로 바로 처리한다
안전하고 빠른 배치 스크립트를 작성해야 하는 사람사용법·옵션 전체, 예시의 “print0과 xargs”·“exec 배치 처리”·“prune”, 주의사항·함정 전체공백·특수문자가 섞인 파일명도 안전하게 처리하고, -exec {} +-prune으로 성능·범위를 제어한다

개요 + 정신 모델

find디렉터리 트리를 순회하며 각 항목에 술어(predicate)를 평가하는 필터링 엔진이다. -name, -type, -mtime 같은 조건은 참/거짓을 판단하는 개별 술어이고, 여러 술어를 나열하면 기본적으로 AND로 묶인 하나의 불리언 표현식이 된다. find는 트리의 각 항목마다 이 표현식을 왼쪽에서 오른쪽으로 평가해, 결과가 참인 항목에 대해서만 기본 동작인 -print(경로 출력)를 수행한다.

이 모델에서 중요한 것은 -exec-delete도 술어와 동일한 표현식 안의 한 항목이라는 점이다. find . -name "*.tmp" -delete는 “이름이 *.tmp이면서(AND) 삭제에 성공하면 참"이라는 하나의 표현식이다. 즉 find는 다른 명령(grep, xargs 등)의 도움 없이도 “조건에 맞는 것을 찾아서 즉시 무엇을 할지"까지 한 문장으로 표현할 수 있는 몇 안 되는 도구다. 이 정신 모델을 이해하면, 뒤에서 다룰 “왜 조건의 순서가 결과에 영향을 주는가”(주의사항·함정)도 자연스럽게 이해된다 — find는 옵션을 미리 다 모아서 평가하는 게 아니라, 표현식을 순서대로 하나씩 판단하며 지나가기 때문이다.

사용법 · 옵션

1
find [경로...] [표현식]
  • 경로: 검색을 시작할 디렉터리. 생략 시 현재 디렉터리(.)로 간주.
  • 표현식: 검색 조건·동작. 여러 개를 나열하면 AND로 묶인다. -o로 OR, !로 부정.

검색 범위·순서

옵션설명
-maxdepth N최대 깊이 N까지만 검색 (1이면 해당 디렉터리만). POSIX 표준에는 없지만 GNU findutils·BSD find 모두 지원하는 사실상 표준 확장
-mindepth N최소 깊이 N부터 검색
-depth디렉터리 내용을 먼저, 그 다음 디렉터리 자체 (rm 등과 조합 시 유용)
-prune항상 참으로 평가되며, 디렉터리일 경우 그 하위로 내려가지 않고 건너뛴다(POSIX 표준 술어)

이름·경로

표현식설명
-name PATTERN파일명이 PATTERN과 일치 (glob: *, ?, []). 셸이 먼저 확장하지 않도록 항상 따옴표로 감싼다
-iname PATTERN-name과 같으나 대소문자 무시
-path PATTERN경로 전체가 PATTERN과 일치
-regex PATTERN경로가 정규식 PATTERN과 일치. 기본 정규식 문법은 GNU find와 BSD find가 서로 다르다

타입

표현식설명
-type d디렉터리만
-type f일반 파일만
-type l심볼릭 링크만
-type s소켓, -type p 파이프 등

시간

표현식설명
-mmin N수정이 N분 이내(이전: -N, 초과: +N)
-mtime N수정이 N일 이내(-N 이내, +N 초과)
-amin N, -atime N접근 시간 기준
-cmin N, -ctime N상태 변경 시간 기준

크기·기타

표현식설명
-size N[cwbkMG]크기. +N 초과, -N 미만. c=바이트, k=KB, M=MB 등
-empty빈 파일·디렉터리
-perm MODE권한이 MODE와 일치
-user NAME, -group NAME소유자·그룹

동작

표현식설명
-print경로 출력 (기본 동작)
-print0널 문자로 구분 (xargs -0와 조합)
-exec CMD {} \;매칭 항목마다 CMD를 한 번씩 실행. {}는 해당 경로로 치환
-exec CMD {} +매칭 항목 여러 개를 한 번의 CMD 호출에 묶어서 전달(xargs처럼 배치 처리)
-execdir CMD \;해당 디렉터리에서 CMD 실행
-delete매칭 항목 삭제
-quit첫 매칭 후 종료

예시

이름으로 검색

1
2
3
4
5
6
7
8
# 현재 디렉터리 아래 .log 파일
find . -name "*.log"

# 대소문자 무시
find /var -iname "*.LOG"

# 이름이 config로 시작하는 파일
find . -name "config*"

타입·깊이 제한

1
2
3
4
5
# 디렉터리만, 최대 깊이 2
find . -maxdepth 2 -type d

# 현재 디렉터리 직하위 .txt 파일만 (깊이 1)
find . -maxdepth 1 -type f -name "*.txt"

시간 기준

1
2
3
4
5
6
7
8
# 7일 이내 수정된 파일
find /tmp -type f -mtime -7

# 30일 넘게 수정 안 된 파일
find /var/log -type f -mtime +30

# 1시간 이내 변경된 파일
find . -type f -mmin -60

크기·빈 파일

1
2
3
4
5
# 100MB 초과 파일
find /home -type f -size +100M

# 빈 디렉터리
find . -type d -empty

exec·삭제

1
2
3
4
5
# 매칭 파일에 chmod 적용 (파일마다 chmod 프로세스를 새로 fork)
find . -name "*.sh" -exec chmod +x {} \;

# 30일 지난 로그 삭제 (주의해서 사용)
find /var/log -name "*.log" -mtime +30 -delete

exec 배치 처리 비교 ({} ; 대 {} +)

1
2
3
4
5
# {} \; : 매칭 100개면 chmod가 100번 실행된다 — 파일마다 새 프로세스를 fork
find . -name "*.sh" -exec chmod +x {} \;

# {} + : 매칭된 파일들을 한 번(또는 ARG_MAX에 맞춰 몇 번)의 chmod 호출에 묶어 전달
find . -name "*.sh" -exec chmod +x {} +

print0과 xargs로 안전하게 처리

1
2
# 공백·줄바꿈이 있는 파일명도 안전하게 처리 ([21장: xargs] 참고)
find . -name "*.txt" -print0 | xargs -0 grep -l "keyword"

-prune으로 특정 디렉터리 건너뛰기

1
2
# .git, node_modules 디렉터리는 아예 내려가지 않고 건너뛴 채 .js 파일만 찾는다
find . \( -path "./.git" -o -path "./node_modules" \) -prune -o -type f -name "*.js" -print

주의사항·함정

파일명에 공백·특수문자가 있을 때는 find -print0 | xargs -0로 처리한다. 21장: xargs에서 이미 예고했던 이 안전 패턴을 여기서 완결한다. 기본 xargs는 입력을 공백·탭·개행으로 구분하기 때문에, find가 찾아낸 경로에 공백이 섞여 있으면(March report.log처럼) Marchreport.log라는 두 개의 별개 인자로 쪼개져 버린다. find-print0은 각 결과를 구분자로 쓸 수 없는 문자인 널 문자(\0)로 구분해 출력하고, xargs -0은 그 널 문자를 기준으로 다시 정확히 하나의 인자로 읽어들인다. find . -name "*.log" -print0 | xargs -0 rm처럼 삭제·이동같이 되돌릴 수 없는 작업일수록 이 조합을 기본값으로 삼아야 한다.

-exec CMD {} \;-exec CMD {} +는 결과는 같아도 성능이 크게 다르다. \;로 끝내면 매칭된 항목마다 CMD를 한 번씩 새로 fork·exec한다 — 파일이 10만 개면 프로세스도 10만 번 생성된다. 반대로 +로 끝내면 find가 매칭된 경로들을 모아 뒀다가 (운영체제의 ARG_MAX 한도 안에서) 최소한의 횟수로 CMD 명령줄 끝에 한꺼번에 붙여 실행한다 — 21장: xargs가 표준입력을 인자로 바꿔 배치 실행하는 방식과 동일한 아이디어다. 파일 수가 많고 CMD 자체가 여러 인자를 한 번에 받을 수 있는 명령(chmod, rm, grep 등)이라면 +가 압도적으로 빠르다.

술어의 평가 순서가 결과와 성능에 함께 영향을 준다. find는 표현식을 왼쪽에서 오른쪽으로 평가하다가 AND는 좌변이 거짓, OR는 좌변이 참인 순간 나머지를 건너뛴다(단락 평가). 이 때문에 비용이 싼 조건(-type f 같은 단순 비교)을 비용이 비싼 조건(-exec, 정규식 매칭) 앞에 두면 불필요한 실행을 줄일 수 있다. 더 중요한 것은 -prune처럼 동작을 겸하는 술어의 위치다. -prune은 반드시 -print나 다른 동작보다 앞쪽, 그리고 건너뛸 디렉터리를 판별하는 조건 바로 뒤에 와야 한다 — 예시의 \( -path ... -o -path ... \) -prune -o -type f -print처럼 순서를 지키지 않으면 디렉터리를 건너뛰지 않고 그대로 내려가 버린다.

GNU find와 BSD/macOS find는 옵션 지원 범위가 다르다. 이 글의 예시는 리눅스 배포판 대부분이 쓰는 GNU findutils 기준이다. -print0, -maxdepth, -prune, -exec {} +는 POSIX 표준이거나 GNU·BSD 양쪽이 공통으로 지원하지만, -regextype(정규식 문법을 바꾸는 옵션)이나 -executable/-readable/-writable 같은 술어는 GNU 전용 확장이라 macOS 기본 find(BSD find 계열)에서는 지원하지 않는다. macOS에서 GNU find와 동일하게 동작시키려면 Homebrew로 brew install findutils를 설치해 gfind로 실행해야 한다. -regex의 기본 정규식 문법도 GNU find는 POSIX BRE인 반면 BSD find는 자체 문법을 쓰므로, 같은 패턴이 두 환경에서 다르게 매칭될 수 있다.

흔한 오개념

"find의 조건 순서는 결과에 영향을 주지 않는다"는 정확하지 않다. find는 조건들을 한꺼번에 모아 논리적으로만 판단하는 것이 아니라, 하나의 표현식을 왼쪽에서 오른쪽으로 차례로 평가하며 지나간다. -exec·-print·-prune처럼 부수효과가 있는 항목이 섞이면 순서가 곧 실행 순서이므로, -prune을 뒤에 두거나 -o로 묶인 그룹의 괄호를 빠뜨리면 의도와 다른 디렉터리까지 내려가거나 원하는 항목이 빠질 수 있다.

"-name 패턴은 따옴표 없이 써도 괜찮다"는 오해도 흔하다. find . -name *.txt처럼 따옴표를 빼면, find가 패턴을 받기 전에 셸이 먼저 *.txt를 현재 디렉터리의 실제 .txt 파일 이름들로 확장해 버린다. 디렉터리에 .txt 파일이 하나도 없으면 패턴 문자열 그대로 전달돼 우연히 의도대로 동작하지만, 파일이 하나라도 있으면 find-name을 그 파일 이름 하나에만 적용하게 되어 나머지 .txt 파일을 놓친다. -name·-path·-regex에 넘기는 패턴은 항상 따옴표로 감싸 셸이 아니라 find가 직접 해석하게 해야 한다.

다음 장에서는

다음은 38장: gzipfind로 찾아낸 파일을 실제로 압축·해제하는 gzip을 다룬다.

평가 기준

  • find의 정신 모델(트리 순회 + 술어 평가, 찾은 뒤 무엇을 할지까지 한 표현식으로 결합)을 설명할 수 있다.
  • 이름·타입·시간·크기 조건을 조합해 원하는 파일을 필터링할 수 있다.
  • find -print0 | xargs -0 안전 패턴이 왜 필요한지, 기본 xargs가 어떤 파일명에서 깨지는지 설명할 수 있다.
  • -exec CMD {} \;-exec CMD {} +의 프로세스 생성 비용 차이를 설명하고 상황에 맞게 선택할 수 있다.
  • -prune으로 특정 디렉터리를 탐색에서 제외하는 법과, 술어 평가 순서가 결과·성능에 미치는 영향을 판단할 수 있다.
  • GNU find와 BSD/macOS find의 옵션 차이를 구분하고 스크립트를 이식할 때 무엇을 점검해야 하는지 설명할 수 있다.

참고