유용한 쉘 명령어 소개 Part 2: find

유용한 쉘 명령어 소개 Part 2: find

2018, Nov 29    

0. 들어가며


오랜만에 유용한 쉘 기능을 다루는 포스트다. 지금까지 파일 또는 디렉토리를 CRUD하는 필수적인 기능과 그 안의 데이터를 살펴보거나 전처리하는 데 도움을 주는 less, sort 등의 기능을 살펴보았다. 이 기능들은 쉘을 운영체제 인터페이스로 쓰기 위해 필수적이거나 매우 중요한 프로그램들로, 중요하지만 각 기능의 사용법이 까다롭지 않아 한 포스트에 여러 개의 프로그램을 다루었다.

그런데 이 포스트에서는 하나의 프로그램만을 다룬다. 내가 리눅스를 공부하면서 ‘우와’하고 감탄한 순간이 몇 번 있는데 그 중 하나는 원하는 조건의 파일을 주어진 경로에서 검색하는 find 라는 프로그램을 알고 부터다. 이런 검색 기능은 시스템의 파일과 디렉토리가 많아질수록 그 필요성이 더해지고 그렇기에 충분히 이 포스트에서 다뤄볼만하다. 우리에게 익숙한 윈도우즈에서도 폴더창에서 우측 상단에 파일 이름을 입력할 수 있는 기능이 있다. 그런데 이 프로그램은 검색 조건이 단순히 이름 이외에도 다양한 조건이 있으며, 살펴보겠지만 조건을 다양하게 조합하거나 검색 이후의 행동까지도 지정할 수 있어 유용하다. 그리고 그만큼 복잡하기 때문에 하나의 포스트를 단독으로 쓴다.

이 프로그램은 유용하기도 하고 매우 재밌기 때문에 충분히 살펴볼 가치가 있다. 전부터 ‘해야지 해야지’ 하고 있었는데 이제야 하게 됐다. find 의 세계로 들어가보자.

1. 리눅스에서 파일 검색하기


내가 거대한 리눅스 시스템을 운영하고 있는데 이 안의 파일 중 내가 지난 학기에 제출한 보고서가 어디 있는지 살펴보고 싶다고 하자. 내가 파일 시스템을 잘 구축했다면 모르겠지만 논의를 의해 매우 난잡하게 파일들이 위치하고 있다고 하자. 그러면 어떻게 해야 할까? 난 파일의 이름이 ‘보고서’로 끝난다는 것만 알고 있다.

당연히 파일을 검색하면 될 것이다. 리눅스에서 이를 지원하는 간단한 프로그램으로는 locate 가 있다. 이 프로그램은 원하는 이름의 패턴을 받아 그것에 매칭되는 모든 파일을 화면에 출력한다.

$ locate '보고서'


(생략)/coin_change_dp/알고리즘 과제 5. 동전 거스름돈(DP) 보고서.pdf
(생략)/coin_change_homework/알고리즘 hw-3 보고서.pdf
(생략)/sort-algorithm-homework/알고리즘 hw-2 보고서.pdf

locate 프로그램은 시스템의 모든 파일의 경로명 중 입력으로 받은 부분이 있으면 그 경로명을 화면에 출력한다. 위는 학교 알고리즘 과제에 제출했었던 프로그램의 목록이다. 입력으로 검색할 경로를 받지 않는데 이는 이 프로그램은 시스템의 모든 파일을 검색대상으로 하기 때문이다.

locate 는 사용하기 정말 쉬운데, 그만큼 검색할 수 있는 조건은 매우 제한적이다. 기껏해야 정규표현식을 지원하는 정도로, 이래서는 GUI를 압도하겠다는 우리의 간절한 소망을 이룰 수 없다. 이 프로그램은 검색할 조건이 이름에 한정되어 있을 때만 쓰면 된다.

참고로 locate 는 검색 시행 시에 시스템 전체를 훑는 것이 아니라 하루에 한 번 이상 시행되는 updatedb 라는 프로그램에 의해 갱신된 파일 시스템 데이터베이스에서 검색을 실시한다. 그래서 가령 내가 검색 전에 갓 만든 ‘ok계획대로-되고-있어-보고서’는 검색이 안 될 확률이 높다. 내가 최근에 변경된 내용까지 검색 DB에 적용하고 싶으면 updatedb 를 관지자 권한으로 사용한다.

$ sudo updatedb

이름 이외의 복잡한 조건을 검색에 추가하기 위해서는 find 프로그램을 사용해야 한다.

2. find 살펴보기


find 또한 조건에 맞는 프로그램을 검색하는 프로그램으로 그 사용법은 다음과 같다.

$ find [path] expression


$ find ~ -name '*.py'

~~~.py
~~~.py
~~~.py
...
(셀 수 없음)

findlocate 와 달리 경로를 지정하면 지정한 경로 내에서만 검색을 시행한다. 지정하지 않으면 현재 내가 속한 경로가 기준이 된다. 경로 다음에는 find expression 이 들어가는데 이 식은 한 개 이상의 검색의 조건들을 지정한다. 결국 우리는 이 expression 을 조작해 다양한 조건들을 다양한 방법으로 조합할 수 있다.

일단 저 예제에서는 파일 이름이 ‘.py’로 끝나는 모든 파일을 검색했다. ‘-name filename‘은 검색에 사용할 이름 조건을 지정하는데 저것도 하나의 find expression 이 된다. 참고로 findlocate 와 달리 입력된 이름이 본 파일명의 일부분이 아닌 전체라고 가정하고 작동하기 때문에 이름 자체가 ‘.py’ 파일이 아닌, 이름이 ‘.py’로 끝나는 파일을 검색하려면 ‘*‘를 붙여줘야 한다. 이는 확장 포스트에서 다뤘던 내용이기도 하다.

find expression 은 여러 개의 부분식으로 구성되는데 이 부분요소들을 크게 나누면 테스트, 연산자, 액션, 옵션으로 나눌 수 있으며, 이 요소들의 중첩을 통해 하나의 단일 expression 을 만들어 다양한 조건의 검색을 시행할 수 있다.

이 네 가지 요소들에 대해 살펴보자.

2.1. 테스트

테스트는 네 가지 요소 중 가장 중요한 요소로 검색의 가장 기본적인 조건을 지정한다. 가령 예시에서 사용했던 ‘-name *.py’는 파일의 이름 조건을 지정하는 테스트이다. find 는 기본적으로 경로 내의 모든 파일들을 훑으면서 테스트를 통과하는 파일만을 출력하는데 저 테스트에서는 파일명이 ‘.py’로 끝나는 파일만 테스트를 통과했다.

이름이 아닌 다른 테스트를 찾아보자. 파일이 아닌 디렉토리만을 출력하는 테스트를 만들고 싶다고 하자. 그때는 검색할 파일의 형식을 지정하는 ‘-type’ 테스트를 쓰면 된다.

$ find ~ -type d


...
/home/sunghwanpark/.autoenv
/home/sunghwanpark/.autoenv/.git
/home/sunghwanpark/.autoenv/.git/hooks
/home/sunghwanpark/.autoenv/.git/info
/home/sunghwanpark/.autoenv/.git/branches
...

‘-type d’는 형식이 디렉토리인 경로들만 테스트를 통과하게 한다. 예시는 홈 디렉토리안에 있는 모든 디렉토리의 경로를 출력했다. 이 ‘-type’ 테스트는 꽤 많이 사용하는데, ‘-type’에 적용할 수 있는 자주 보이는 값들은 다음과 같다.

파일 형식 설 명
b 블록 특수 파일
c 문자 특수 파일
d 디렉토리
f 파일
l 심볼릭 링크 파일

이 중에서 ‘d’, ‘f’, ‘l’은 쓸 일이 있을 것이다. 심볼릭 링크는 윈도우즈의 ‘바로 가기’ 같은 기능으로 언제 한 번 다루도록 하겠다.


find 에는 이름 조건 외에도 여러 테스트가 있는데 그 목록은 다음과 같다.

테스트명 설 명
-cmin n n 분 전에 마지막으로 내용이나 속성이 변경된 파일이나 디렉토리를 검색한다.
-cnewer file file 보다 최근에 마지막으로 내용이나 속성이 변경된 파일이나 디렉토리를 검색
-ctime n n * 24 시간 전에 마지막으로 내용이나 속성이 변경된 파일이나 디렉토리 검색
-empty 빈 파일이나 디렉토리 검색한다. 뒤에 붙는 인자가 없다.
-group name name 그룹에 속한 파일이나 디렉토리를 검색한다. 그룹은 나중에 ‘Permission’ 관련 포스트에서 다룬다.
-user name name 사용자에 속한 파일이나 디렉토리를 찾는다.
-name name 파일 이름이 name 인 파일을 검색한다. name 은 쉘 와일드카드 패턴을 지원한다.
-regex name 파일 이름이 name 인 파일을 검색한다. 정규표현식을 지원한다.
-iname name ‘-name’과 같으나 대소문자를 무시한다.
-mmin n n 분 전에 내용이 변경된 파일 또는 디렉토리를 검색한다.
-mtime n n * 24 시간 이전에 변경된 파일이나 디렉토리를 검색한다.
-newer file file 보다 최근에 내용이 변경된 파일이나 디렉토리를 검색한다.
-nouser 유효 사용자가 없는 파일이나 디렉토리를 찾는다.
-nogroup 유효 그룹이 없는 파일이나 디렉토리를 찾는다.
-perm mode 지정된 mode 로 퍼미션이 지정된 파일이나 디렉토리를 찾는다. 이후 포스트에서 다룬다.
-size n 크기가 n 인 파일을 검색한다.
-type c c 타입인 파일을 검색한다.

목록이 길어서 겁먹지 말자. 이중 이해하기 어려운 테스트는 전혀 없고, 외울 이유도 없다.

테스트에서 시간이나 일수, 파일의 크기를 나타내는 \(n\)을 사용하는 테스트들은 정확히 \(n\)분 전, 또는 \(n\)일 전을 나타내는데 이는 별로 쓸모가 없다. 그보다는 ‘\(n\)분 이내’, ‘\(n\)일 이후’라는 테스트가 더 적절하기 때문에 ‘+’, ‘-‘ 을 써서 ‘이후’, ‘이전’을 표현한다. 가령 3분 이내에 변경됐는지 테스트할 때는 ‘-cmin -\(n\)’ 같이 입력하면 된다.

‘-mtime’과 -ctime’, ‘-cnewer’, ‘-newer’과 같이 비슷한데 애매하게 다른 테스트들이 있다. ‘c’는 파일의 내용 또는 속성이 변경된 경우를 대상으로 하고 그 외는 파일의 내용만이 변경된 것을 대상으로 한다. 속성이 바뀐다는 것은 파일의 유효 사용자, 유효 그룹 등이 바뀌는 것을 말하는데 이는 ‘Permission’과 관련된 부분으로 이후에 다루도록 한다.

‘-size’ 테스트의 경우 크기를 바이트로 표현하면 값이 너무 커질 수 있기에 메가바이트, 킬로바이트 등의 표현을 지원한다.

-size 인자 크기 단위
b 512 바이트 단위의 블록(기본값, 운영체제와 관련이 있다)
c 바이트
w 워드(2 byte)
k 킬로바이트(2^10 byte)
M 메가바이트(2^20 byte)
G 킬로바이트(2^30 byte)

크기가 1기가 이상 되는 파일만 뽑아보려면 다음과 같이 입력하면 될 것이다.

$ find ~ -size +1G


테스트는 중첩될 수 있다. 다수의 조건을 넣을 수 있다는 것인데 가령 검색이 다음과 같은 조건들을 만족해야 한다고 하자.

  1. 타입이 파일이면서,
  2. 이름이 ‘n’으로 시작하고,
  3. 30일 이전에 내용이 수정된 적이 있는 파일.
$ find ~ -type f -name 'n*' -mtime -30


(생략)/background/notifications.js
(생략)/background/notifications.js
(생략)/algospot/numbergame.py

3가지 조건에 충족되는 파일은 내 홈파일에는 단 세 파일이었다. 이때 기억할 것은 테스트들은 기본적으로 AND 연산으로 평가된다. 테스트를 모두 평가하는(1. & 2. & 3. 결과가 1인) 파일만 출력된다. 이 기본적인 동작은 향후 연산자 절에서 OR 등으로 바꿀 수 있다.

그리고 find expression 안의 항목들은 왼쪽에서 오른쪽으로 순서대로 평가된다. 프로그래밍의 부울 논리식에서 참거짓을 반환하는 A, B expression이 있을 때 ‘A & B’ 연산을 한다고 하자. 이때 ‘A’가 False라면 ‘B’는 평가 자체도 안 된다는 사실을 아마 잘 알고 있을 것이다. 이는 find expression 에도 적용된다. 그 말은 find expression 의 구성요소들의 등장위치가 의미가 있다는 것을 의미한다. 더하기처럼 마냥 ‘(A+B) = (B+A)’가 아닌 것이다. 이후 연산자, 액션을 다루면 이 의미를 더 잘 살펴보게 될 것이다.


2.2. 연산자

여러 테스트들을 살펴보면서 테스트들이 중첩될 때는 AND 연산이 기본이라고 했다. 하지만 AND 이외의 조건이 필요할 수도 있다. 가령 ‘이름이 z로 시작하거나(OR) 파일 크기가 1MB 이하인 파일’, ‘파일의 소유자가 내가 (NOT)아닌 파일’을 찾고 싶을 수도 있을테니까. find 는 이렇게 테스트들의 연산 순서나 방법을 지정해주는 연산자들이 있다. 실제 부울 논리와 매우 비슷하기 때문에 이해하기 쉽다.

먼저 테스트들 사이에 -and를 넣으면 두 테스트를 AND 연산한다. 이는 기본값이기 때문에 생략 가능하다.

테스트들 사이에 -or를 주면 두 테스트를 OR 연산한다.

$ find ~ -name '*xyz' -or -name 'abcd*'

(생략)/fixtures/bar/xyz
(생략)/theme/abcdef.css

이름이 ‘xyz’로 끝나거나, ‘abcd’로 시작하는 모든 파일을 찾는다. 내 홈디렉토리에는 단 두 개 있었다.
‘-or’은 ‘-o’로 줄여쓸 수도 있다.


부정 연산도 원하는 조건식 앞에 -not 을 붙이면 할 수 있다.

$ find ~ -not -name 'Z*'


...
너무 많음
...

내 홈디렉토리에서 이름이 ‘Z’로 시작하지 않는 모든 파일을 출력하는 코드다. 위와 같이 조건식에서 ‘-not’을 붙이면 그 뒤의 조건이 부정연산이 된다. 부정연산에 참이 되는 파일들만 출력되는 것이다.


끝으로 부울 논리식에서와 마찬가지로 괄호를 통해 연산 순서를 명시해줄 수 있다.

$ find ~ \( -not -type f \) -and \( -not -type d \) -and \( -not -type l \)


/home/sunghwanpark/.gnupg/S.gpg-agent

’( )’를 통해 세 연산을 감싸고 AND 연산했다. 각각 파일의 형식이 파일이 아니고, 디렉토리도 아니며, 심볼릭 링크(바로 가기)도 아닌 파일을 찾는다. 이때 각 조건을 이루는 두 요소(‘-not’, ‘-type’)을 괄호로 감싸 하나의 조건식이라는 것을 명시했다. 위는 괄호를 안 했어도 같은 결과가 나왔겠지만 여기서는 가독성을 위해 일부러 괄호를 썼다. 또 가독성을 위해 굳이 생략되는 ‘-and’도 명시했다.

괄호에 ‘\‘가 들어간 것이 의아할 것이다. 우리가 확장을 다룬 포스트를 기억하는가? 쉘에서는 받은 입력을 프로그램에 전달하기 전에 와일드카드 패턴에 일치하면 그 입력을 확장시킨다. 확장에서 ‘( )’는 의미 있는 글자인데 우리는 쉘 확장을 피하고 우리가 입력한 온전한 find expression 전체를 find 에 전달하고 싶다. 즉, 쉘에서의 혹시 모를 확장을 막기 위해 ‘\‘를 써서 그 의미를 escape한 것이다. 이 개념은 확장을 잘 알아야 이해할 수 있다. 확장이 뭔지 대답이 안 되면 꼭 위의 포스트를 정독하도록 하자.


2.3. 액션

테스트와 연산을 잘 쓰면 조건식이 아무리 복잡해도 원하는 파일을 찾을 수 있다. 그런데 find 는 기본적으로 결과들을 화면에 출력할 뿐인데 우리가 액션을 지정해주면 단순히 화면에 출력하는 것 이상의 처리를 할 수 있다. 가령 쓰레기 파일들을 찾아서 바로 삭제한다든지, 검색된 파일들의 파일명 이상의 구체적인 정보를 파악한다든지, 심지어는 우리가 원하는 액션을 지정해줄 수도 있다.(이 얼마나 사려깊은 프로그램의 자세인가)

액션 또한 다른 테스트나 연산자와 마찬가지로 ‘-액션’의 형태를 띄는데 find 에서 기본적으로 지원하는 액션은 다음과 같다.

액션 설명
-print 만족되는 결과를 출력한다. 이것이 기본값이며 생략 가능하다.
-delete 만족되는 결과를 모두 삭제한다. 묻지도 따지지도 않고 삭제하기 때문에 매우 조심해야 한다.
-ls 결과에 모두 ‘ls -dils’와 같은 명령을 실행해 화면에 출력한다. 파일들을 자세히 들여다볼 때 유용하다.
-quit 검색 조건에 만족하는 결과가 하나라도 있으면 검색을 종료한다.

예시를 먼저 살펴보자. 앞서 연산자에서 파일도 아니고, 디렉토리도 아니며, 심볼릭 링크도 아닌 존재를 찾는 find 식을 만들었다. 솔직하게 말하면 이 포스트를 쓰면서 우연찮게 생각나서 쓴 식인데 저 단 하나뿐인 파일의 정체가 무엇인지 진심으로 궁금하다. 저 find 식에 ‘-ls’ 액션으로 정체를 살펴보자.

$ find ~ \( -not -type f \) -and \( -not -type d \) -and \( -not -type l \) -ls


2794577      0 srwxrwxr-x   1 sunghwanpark sunghwanpark        0 11월 28 22:24 /home/sunghwanpark/.gnupg/S.gpg-agent

우리가 ‘ls -alt’를 입력했을 때와 같은 결과가 나온다. 이 파일의 정체를 살피기 위해서는 3번째 열의 ‘srwxrwxr-x’를 살펴야 한다. 이 문자열은 파일의 권한과 관련된 문자열로 일단 넘어가자. 첫 ‘s’가 파일의 형식을 지정하는데 이는 ‘local socket file’, 즉 통신용 소켓을 의미한다. 일반 파일이나 디렉토리가 아니기 때문에 홈디렉토리에서 유일하게 검색되었다. 대관절 왜 저런 파일이 생겼는지는 모르겠지만 말이다.


아까 각 요소들의 위치가 중요하다고 했다. 그 의미를 정확해 살펴보자. find 에서는 표현식이 왼쪽부터 오른쪽으로 실행된다고 했다. 또 조건식의 연산자와 테스트의 위치에 따라 결과가 달라질 수 있다고 했다. 위치에 따라 결과가 달라지는 예를 액션을 통해 확인해볼 수 있다.

바로 위의 find 예제에서 액션인 ‘-ls’가 맨 뒤에 있다. 앞의 테스트들은 AND 연산자로 묶여 있는데 이를 조금만 수정해보자.

$ find ~ \( -not -type f \) -ls -or \( -not -type d \) -or \( -not -type l \)

...
넘나 많음;;
...

아까와 달리 연산자를 OR로 바꾸고, ‘-ls’ 액션을 첫 테스트 뒤에 두었을 뿐인데 결과가 무수히 나왔다.(무수한 악수의 요청이!) 왜 이럴까? 언제나 표현식은 왼쪽부터 오른쪽으로 평가되고, 액션은 맨 뒤가 아닌 첫 테스트 바로 뒤에 있다. 그렇기에 첫 테스트만 만족하는 파일들만 액션 처리 되어 파일이 아닌 모든 대상이 출력되었고, ‘-ls’ 액션 뒤에 테스트들은 공허하게 실행되었다. 그렇기에 보통 액션은 표현식의 맨 뒤에 오는 경우가 많다. 액션으로 처리도 못할 테스트를 할 이유가 없으니까.

이렇듯 find expression 을 만들 때는 산술식 만들 듯이 순서를 신경 써서 만들어줘야 한다.


마지막으로 액션을 사용자가 직접 만들 수 있는 사용자 정의 액션을 살펴보자. 사용자 정의 액션은 다음과 같은 구성요소로 표현된다.

-exec command {} ;

command 에는 원하는 명령어가 들어가는데 뒤에 그 명령어의 고유한 옵션을 써도 괜찮다. ‘{}’ 기호는 현재 경로명, 그러니까 검색된 파일 경로명에 대한 심볼릭 링크를 의미하는데 일반적으로 그대로 쓰면 된다. 세미콜론(‘;’)은 ‘command’의 끝을 말해주는 구분자로 꼭 들어가야 한다.

정의된 액션 중 하나인 ‘-delete’를 ‘-exec’으로 구현하면 다음과 같다.

-exec rm '{}' ';'

검색된 인자들, 정확히는 그에 대한 심볼릭 링크(‘{}’)를 받아 그것을 삭제한다. 이때, ‘{}’, ‘;’에는 반드시 따옴표를 붙여준다. 중괄호와 세미콜론은 쉘에서 확장과 명령어 구분자로 특수한 의미가 있기 때문에 이를 작은따옴표로 죽여준다. 그 값이 글자 그대로 find 에 전달되어야 하기 때문이다.

아까 ‘-delete’를 쓸 때는 매우 조심해야 한다고 했는데 ‘-ok’ 액션을 ‘-exec’ 액션 자리 대신 사용하면 지정된 명령을 실행하기 전, 사용자에게 확인 메시지를 띄운다.

# -ok 액션으로 사용자 정의 액션을 실행할지 대화식으로 파일 하나씩 설정할 수 있다.

$ find ~ -type f -name 'foo*' -ok ls -l '{}' ';'

2.4. 옵션

옵션은 find 의 검색 범위를 설정할 때 사용된다.

옵션 설명
-depth 검색 시 디렉토리 자체 이전에 디렉토리의 파일에 대해 검색을 우선 실행한다. 이는 ‘-delete’ 액션 사용 시 자동적으로 적용된다.
-maxdepth levels 검색 대상이 되는 디렉토리 최대 탐색 깊이를 숫자로 지정한다.
-mindepth levels 검색 대상이 되는 디렉토리 최소 탐색 깊이를 숫자로 지정한다.
-mount 다른 파일스시템에 마운트된 디렉토리의 탐색은 제외한다.

일반적인 사용에서는 위의 셋보다는 쓸모가 덜하지만 알아두는 것은 나쁘지 않다.


3. 마치며


파일 검색 시 유용한 find 프로그램을 살펴보았다. 난 이 프로그램이 정말정말 좋다. 윈도우즈로는 절대 못할 복잡한 조건을 적용해 파일을 찾을 때의 쾌감이란…

다음 포스트는 심볼릭 링크를 통해 ‘바로 가기’를 만드는 법을 살펴보자.

이상 find 포스트를 마친다.