rsync; include/exclude; PATTERN 을 이해해봅시다. 그리고.. 특정 디렉토리만 선택하고, Depth 도 지정해주려면?

rsync 는 쓴 지 몇년 됐는데, 늘 볼 때마다 새롭다. 그만큼 명령어(선택사항)가 많고, 제대로 문서화가 돼 있지 못하다고 볼 수 있겠다. 이 모든게 내가 제대로 이해하지 못하고 있기 때문이다.

물론, 전문가 입장에서는 man page 로도 충분히 원하는 지식을 얻을 수 있을 지 모르지만, 나같은 무식쟁이 입장에선 그 설명서를 이해하기가 결코 쉬운 일이 아니었다.
나만 이런 느낌을 받지는 않았을 터. Gilles 라는 분은 이렇게 표현을 했다.

Rsync’s filter rules can seem daunting when you read the manual,…

daunt 라는 형용사는 좀 낯선데, 겁나다, 위축되다, 기가 죽다 등등으로 풀이할 수 있다고 한다.
Stack Exchange English 분과(?)에도 답을 올릴 정도인 이 분도 이런 분위기를 느꼈을 정도라면, 나는..??

아무튼.. Gilles 님의 글에서 도움을 받아 이 내용을 정리해본다.


현재 홈디렉토리는 이런 형태다. (tree 명령을 사용하면 이렇게 표시할 수 있다.)

/home/userA
├── Applications
├── EBooks
├── VirtualBox_VMs
├── opt
├── snap
├── 공개
├── 공부
├── 다운로드
├── 문서
├── 바탕화면
├── 비디오
├── 사진
├── 설정 및 보관
├── 음악
├── 읽을거리
├── 작업대
└── 템플릿

이 디렉토리중 몇몇 디렉토리만 골라서 복사를 하려고 한다.
물론, 스크립트를 만들고, 각각 디렉토리를 지정해서 cprsync 를 여러번 써주면 된다. 간단하고, 오류도 최소화할 수 있다.
그러나, 겉멋을 부리고 싶기에…

rsync 를 사용해서 작업을 좀 더 복잡하게(?) 해보기로 한다.

여기서 하고자 하는 바는 이렇다.

  • 몇몇 디렉토리는 하위 파일/디렉토리까지 모두 복사한다. (공부, 문서, 비디오, 사진, 음악, 읽을거리, 작업대, EBooks)
  • 다운로드 디렉토리는, 그 산하 파일만 복사한다. (즉, 다운로드 밑의 서브디렉토리는 모두 배제하고 오로지 1st Depth 에 있는 파일만 선택한다.)

‘하위 모두 선택’은 쉽다. --include 에 포함만 시켜주면 된다.
문제는 ‘다운로드’ 디렉토리다. 즉, 다운로드 하위에 있는 디렉토리는 포함시키지 않고 오로지 파일만 선택하게끔 하려면.. 어떻게 해야 하려나???
이제부터, PATTERN 을 이해해야 한다. (결과만 써놔야 나중에 보기에 편하지만, 시행착오 내용까지 함께 적었다.)

여러가지 선택사항을 이리 저리 붙여본 결과, 이 문제를 해결하기 위한 --include-from 형식은 다음과 같다.

- 다운로드/*/          다운로드 아래 서브디렉토리 모두 배제
- 다운로드/.*          다운로드 디렉토리 아래 .* 파일 배제
+ 다운로드/***         다운로드 아래 모든 파일 선택

# 물론, 아래 행은 필수다.
- /*

# 또는
- *

이렇게 하면, 다운로드 바로 밑의 디렉토리와 .으로 시작되는 파일은 모두 배제되고, 순수한 파일만 선택된다.
다운로드 밑의 서브디렉토리들이 배제되었음은 이렇게 확실하게 표시가 된다.

[sender] hiding directory 다운로드/OS 설치 이미지 because of pattern 다운로드/*/
[sender] hiding directory 다운로드/Games because of pattern 다운로드/*/
[sender] hiding directory 다운로드/PacVim-1.1.1 because of pattern 다운로드/*/
[sender] hiding directory 다운로드/CCTV because of pattern 다운로드/*/
[sender] hiding directory 다운로드/잠시 because of pattern 다운로드/*/
[sender] hiding directory 다운로드/00 Wireguard key because of pattern 다운로드/*/
[sender] hiding directory 다운로드/.anaconda_backup because of pattern 다운로드/*/
[sender] hiding directory 다운로드/anki-2.1.40-linux because of pattern 다운로드/*/
[sender] hiding directory 다운로드/teclast because of pattern 다운로드/*/
[sender] hiding directory 다운로드/plasma-applet-eventcalendar-75 because of pattern 다운로드/*/
[sender] hiding directory 다운로드/autokey-0.96.0-beta.4 because of pattern 다운로드/*/
[sender] hiding directory 다운로드/00 연습 because of pattern 다운로드/*/

헌데, 만약 순서를 바꾸면 어떻게 될까?

+ 다운로드/***         다운로드 아래 모든 파일 선택
- 다운로드/*/          다운로드 아래 서브디렉토리 모두 배제
- 다운로드/.*          다운로드 디렉토리 아래 .* 파일 배제
- /*

+, 즉 include 를 먼저 해주고 – 를 나중에 해주면 exclude 가 제대로 작동하질 않는다. 이게 왜 이런건지 확 와닿질 않는다.
아무튼, 다운로드 이하 디렉토리가 모두 선택이 됐다. 일부만 보면..

....
[sender] showing directory 다운로드/PacVim-1.1.1/gifs because of pattern 다운로드/***
[sender] showing directory 다운로드/PacVim-1.1.1/maps because of pattern 다운로드/***
[sender] showing directory 다운로드/PacVim-1.1.1/src because of pattern 다운로드/***
[sender] showing directory 다운로드/anki-2.1.40-linux/bin because of pattern 다운로드/***
[sender] showing directory 다운로드/anki-2.1.40-linux/bin/PyQt5 because of pattern 다운로드/***
.....

+ 다운로드/*** 가 먼저 나왔으니 그 이후에 나오는 - 다운로드/*/ 는 무시되는 모양이다. 그렇다면 맨 아래에 있는 - /* 가 우선시 되는건지..?


이 문제를 풀기위해 이리 저리 뒤지고 짜증내고.. 한 결과!
그래도 답은 얻을 수 있었다.

일단, 읽어도 소용없던 man page 에 이런 내용이 있었다.

As the list of files/directories to transfer is built, rsync checks each name to be transferred against the list of include/exclude  patterns in turn, and the first matching pattern is acted on: if it is an exclude pattern, then that file is skipped; if it is an include pattern then that filename is not skipped; if no matching pattern is found, then the filename is not skipped.

맨 마지막 문구에 먼저 주목할 필요가 있다.
if no matching pattern is found, then the filename is not skipped.” 이렇다는 건, 특정 파일만을 선택하려면, 그외 다른 모든 파일은 선택하지 않음을 명시해야만 한다는 뜻이 된다.

따라서, include/exclude 를 적절히 사용하기 위해선, 일단 모든 파일들을 선택하지 않게끔 해줘야 한다. 그게 바로, - * 다.

그리고 어찌보면 가장 중요한 내용이 위에 있다.
the first matching pattern is acted on.

즉, 한번 패턴이 매칭되면, 그걸로 그냥 정해진다는 의미가 되고, 다시 말해, ‘패턴 매칭은 한번 뿐, 두번째 패턴 매칭은 없다.’ (낙장불입(落張不入)?)
이건 Regex 와도 닮아 있다. (사실 같은 개념으로 보인다.)

rsync 는 소스 디렉토리 아래 모든 파일/디렉토리들을 하나씩 점검하며 패턴과 맞는게 있는지 검사하기 시작한다. 여기서도 중요한 점이 있다.

*** rsync 가 매칭 작업을 수행하는 방식!

지금, 특정 디렉토리 아래 서브디렉토리와 파일들을 복사하는 작업을 하려고 하기 때문에, 당연히 rsync -r 을 선택했다.(실제로는 -a 로써, r 을 포함한 기타 선택사항들을 모두 포함시켰다.)
-r 이 선택되었을 때, rsync 는 (어떤 순서인지는 알 수 없지만), 마치 Regex 엔진이 문자열 왼쪽에서 오른쪽으로 한칸씩 전진하며 매칭을 확인하듯, 파일/디렉토리를 순차로 검색하기 시작한다. 이 부분도 man page 에 나와있다.

Note that, when using the --recursive (-r) option (which is implied by -a), every subdir component of every path is visited left to right, with each directory having a chance for exclusion before its content. In this way include/exclude patterns are applied recursively to the pathname of each node in the filesystem’s tree (those inside the transfer). The exclude patterns short-circuit the directory traversal stage as rsync finds the files to send.

‘left to right’ 의 의미는 아마도 regex 방식처럼 문자열 왼쪽에서 오른쪽으로 이동하며 매칭을 확인한다는 뜻으로 보인다.
예를 들어, ‘다운로드/Games/’ 가 있다면, ‘다,운,로,드,/,G,a,m,e,s,/’ 순으로 한글자씩 매칭을 시도한다. 헌데, 매칭 룰에 ‘다운로드/*/’ 가 있으므로, ‘다운로드/*/’ 까지 성공(굵은 글씨), 그 이후 Games 는 ‘다운로드/*/’ 로 매칭 성공, 마지막 ‘/’ 는 ‘다운로드/*/‘로 끝.
이렇게 매칭 작업이 이뤄진다.

마지막으로, 반드시 고려해야할 사항이 있다. 선택(include)을 원하는 파일을 더해주려면, 반드시 그 상위디렉토리를 먼저 넣어야만 한다. - * 가 있다면, 상위디렉토리가 자동으로 빠져버리고, 따라서 rsync 는 해당 파일로 아예 접근할 기회를 잃기 때문이다.

For instance, to include "/foo/bar/baz", the directories "/foo" and "/foo/bar" must not be excluded. Excluding one of those parent directories prevents the examination of its content, cutting off rsync’s recursion into those paths and rendering the include for "/foo/bar/baz" ineffectual (since rsync can’t match something it never sees in the cut-off section of the directory hierarchy).

/foo/bar/baz 라는 ‘파일’을 include 시키기 위해선, 먼저 /foo, /foo/bar 디렉토리를 include 해야만 한다는 소린데..
이게 때에 따라선 꽤 복잡한 고려사항이 될 수도 있겠다.

따라서, 바로 이어지는 설명에서 이런 편법(?)을 제안하고 있다.

One solution is to ask for all directories in the hierarchy to be included by using a single rule: "+ */" (put it somewhere before the "- *" rule), and perhaps use the --prune-empty-dirs option.

이건 또 무슨 소린고 하니..
include 규칙 앞쪽에 + */ 를 넣어, 모든 디렉토리를 방문하도록 미리 명시하고, 맨 끝 규칙에 ‘- *’ 를 넣어, 디렉토리가 아닌 다른 모든 것들은 제외하도록 하라는 뜻이다. 선택받기 원하는 파일들은 이 중간에 넣어주면 된다.
그런데, 이렇게 하면, 그 안의 내용은 선택하지 않았더라도 일단 모든 디렉토리가 다 포함이 된다. 그리고 이 상태에서 rsync 가 완료되면, 불필요한, 즉 안에 내용(파일/디렉토리)이 0 인 디렉토리까지 선택되어 복사된다.
이 때, -m 을 추가로 선택하면, 이런 파일들은 최종 복사단계에서 자동으로 빠진다.
이른바 선택의 융통성이랄까.

다시 원래 내용으로 돌아가보자.


rsync 는 하위디렉토리를 하나씩 점검하다가 ~/다운로드 디렉토리를 만나게 된다. 이때, 해당되는 패턴이 있는지를 살핀 뒤, ‘+ 다운로드/***‘ 가 있으므로, 다운로드 디렉토리 자체와, 하위에 있는 모든 파일과 디렉토리를 선택(include)한다.
이렇게 한번 매칭이 되었으므로, 그 아래 있는 두 줄은 의미가 없어져 버린다. 다시 말하지만, 매칭은 딱 1회만.

성공했던 패턴을 보자면,

- 다운로드/*/
- 다운로드/.*
+ 다운로드/***
- /*

먼저, 다운로드 아래 있는 모든 디렉토리를 배제한다. 두번째로, . 으로 시작하는 모든 파일도 배제한다.
이제 + 다운로드/*** 로, 다운로드 디렉토리및 그 아래 파일들 모두를 선택한다. 이때, 위에서 배제한 디렉토리및 파일들은 당연히 배제된 상태 그대로 유지된다. (The first matching pattern is acted on.)
(다만, 여기서 조금 애매한 부분이 있는데.. 그건 글 맨 마지막에 덧붙이기로 한다.)

나머지 모든 파일들은 – /* 에 의해서 모두 배제된다.

*** 여기서 한가지. * 는 ‘파일‘을 뜻하는걸까?
흔히 생각하기에 * 는 파일이라는 선입견이 있으나, * 는 파일도 디렉토리도 아니다.
그냥, 모든 문자를 매칭해주는 와일드카드일 뿐. 단, / 가 나올 때까지만 매칭이 된다.
이 역시나 man page 에 설명이 나와있다.

a ’*’ matches any path component, but it stops at slashes

리눅스 파일 구조는 /a/b/c/d 형식이므로, * 라고만 하면 a 까지만 선택이 된다. / 를 넘겨서까지 매칭을 하고 싶다면 ‘**’ 을 써야만 한다.
또 한가지. 만약 파일만을 선택하고자 ‘+ /다운로드/*‘ 라고 패턴을 줬다 하더라도, 다운로드 아래 파일과 디렉토리 모두 선택이 된다. * 로는 그게 파일인지 디렉토리인지를 알 수 없기 때문이다. 하여, ‘다운로드/abc’ 라는 서브디렉토리도 선택 되고, ‘다운로드/가나다라.txt’ 라는 파일도 선택이 된다.

따라서 디렉토리만을 선택하거나 배제하려면 끝에 슬래시를 넣어줘야 하고, 경우에 따라서는 디렉토리까지 선택을 한 뒤 비어있는 디렉토리는 선택에서 빼버리는 추가선택사항(-m, –prune-empty-dirs)을 사용해야 할 때도 있다.

이런 저런 복잡함을 피해가려면, 차라리 find 를 사용해서 원하는 파일을 추출해낸 다음, rsync --files-from 을 사용하여 정확히 원하는 파일만 명시해주는 방법도 있다.

어쨌든, Exclude 와 Include 를 사용하려면, 패턴에 대한 적절한 구상이 필요하고, 대부분 모든 파일을 제외해주는 ‘- *’ 는 필수로 들어가야 할 듯 하다.
간단하게 생각해서, Exclude 를 먼저, Include 를 나중에. ‘- *’ 는 반드시.
이는 좀 더 경험이 쌓이면 나중에 추가해보기로..

이 정도면 쓰고 싶었던 내용은 다 썼으려나???


** 약간 애매한데??

위에서, 선택/배제할 파일이 있을 경우 그 상위 디렉토리를 먼저 고려해야한다고 했다. 그렇지 않으면 아예 그 디렉토리로 접근이 안된다고 했는데..

- 다운로드/*/
- 다운로드/.*
+ 다운로드/***
- /*

이 경우, rsync 는 다운로드/ 로 진입을 했다.
왜??? 어떻게??? 어떤 규칙이 이걸 허용했나?

‘허용’이라면 다운로드/*** 밖에 없는데, 이렇다면 다운로드 아래 모든 디렉토리와 파일이 선택되어져야만 하는데..??

rsync 메시지를 보면 이렇다.

[sender] showing directory 다운로드 because of pattern 다운로드/***
.....
[sender] hiding directory 다운로드/OS 설치 이미지 because of pattern 다운로드/*/
[sender] hiding directory 다운로드/Games because of pattern 다운로드/*/
[sender] hiding directory 다운로드/PacVim-1.1.1 because of pattern 다운로드/*/
[sender] hiding directory 다운로드/잠시 because of pattern 다운로드/*/
....
[sender] showing file 다운로드/(170630) 오픈소스 라이브러리 OSLiC 1.0.0_한글번역본_최종.pdf because of pattern 다운로드/***
[sender] showing file 다운로드/170512 서식 모음.hwp because of pattern 다운로드/***
[sender] showing file 다운로드/20175272.py because of pattern 다운로드/***
[sender] showing file 다운로드/20181209_122916.jpg because of pattern 다운로드/***
[sender] showing file 다운로드/20181209_124831.jpg because of pattern 다운로드/***
[sender] showing file 다운로드/20181211_203352.jpg because of pattern 다운로드/***
...

‘다운로드’ 디렉토리 자체는 다운로드/*** 규칙에 의해 선택(showing)되었다.
그런데 그 이하에 있는 디렉토리는 선택되지 않고 또 잘 배제(hiding)되었다.

흠…?

dir/*** 는 디렉토리 자체와 그 이하 파일/디렉토리를 모두 아우르는 표현이다.
즉, dir/ + dir/** 와 같다. (** 는 * 에 / 까지 포함하라는 뜻이 된다.)

dir/***
equals
dir/
dir/**

해서 위 규칙을 다시 풀어써보면 아래와 같다.

- 다운로드/*/
- 다운로드/.*
+ 다운로드/
+ 다운로드/*
- /*

결과는..

[sender] showing directory 다운로드 because of pattern 다운로드/
.....
[sender] hiding directory 다운로드/OS 설치 이미지 because of pattern 다운로드/*/
[sender] hiding directory 다운로드/Games because of pattern 다운로드/*/
[sender] hiding directory 다운로드/PacVim-1.1.1 because of pattern 다운로드/*/
[sender] hiding directory 다운로드/잠시 because of pattern 다운로드/*/
....
[sender] showing file 다운로드/(170630) 오픈소스 라이브러리 OSLiC 1.0.0_한글번역본_최종.pdf because of pattern 다운로드/*
[sender] showing file 다운로드/170512 서식 모음.hwp because of pattern 다운로드/*
[sender] showing file 다운로드/20175272.py because of pattern 다운로드/*
[sender] showing file 다운로드/20181209_122916.jpg because of pattern 다운로드/*
[sender] showing file 다운로드/20181209_124831.jpg because of pattern 다운로드/*
[sender] showing file 다운로드/20181211_203352.jpg because of pattern 다운로드/*
....

같다.

결과는 같게 나왔다. 이걸로 미루어짐작해보면, dir/*** 와 dir/ + dir/** 가 같긴 한데, Include/Exclude 작업 시엔 좀 더 신중하게 선택을 해야 하겠다. (그냥 내 단순한 생각엔, dir/*** 를 include 했다면, 그 디렉토리와 하위 모든파일/디렉토리가 선택(추가)되어야만 할 것 같은데, 실상은 그렇지 않았다는 게 좀..??)

따라서 결론은, dir/*** 는 조금 애매할 수도 있으므로, 가능하면 나눠쓰는 편(dir/ + dir/*)이 좀 더 정확한 결과를 얻는데 도움이 될 듯 하다.

Author: 아무도안

안녕하세요. 글 남겨주셔서 고맙습니다.