동일한 디렉토리 트리 내에서 동일한 이름을 가진 해당 폴더로 이동하고 싶은 디렉토리 트리 아래에 XML 파일이 많이 있습니다.
다음은 샘플 구조(셸)입니다.
touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
그래서 내 접근 방식은 다음과 같습니다.
find . -name "*.xml" -exec sh -c '
DST=$(
find . -type d -name "$(basename "{}" .xml)" -print -quit
)
[ -d "$DST" ] && mv -v "{}" "$DST/"' ';'
이는 다음과 같은 출력을 제공합니다.
‘./( bar ).xml’ -> ‘./bar/( bar )/( bar ).xml’
mv: ‘./bar/( bar )/( bar ).xml’ and ‘./bar/( bar )/( bar ).xml’ are the same file
‘./bar.xml’ -> ‘./bar/bar.xml’
‘./foo.xml’ -> ‘./foo/foo.xml’
하지만 대괄호( [ foo ].xml
) 안의 파일은 무시한 것처럼 이동되지 않았습니다.
나는 확인했고 basename
(예를 들어 basename "[ foo ].xml" ".xml"
) 파일을 올바르게 변환했지만 find
대괄호에 문제가 있습니다. 예를 들어:
find . -name '[ foo ].xml'
파일을 제대로 찾을 수 없습니다. 그러나 대괄호( '\[ foo \].xml'
)를 이스케이프하면 제대로 작동하지만 문제가 해결되지 않습니다. 왜냐하면 이는 스크립트의 일부이고 어떤 파일에 특수(셸?) 문자가 있는지 알 수 없기 때문입니다. BSD와 GNU로 테스트되었습니다 find
.
find
의 매개변수 와 함께 사용할 때 파일 이름을 이스케이프하는 보편적인 방법이 있습니까 -name
? 그러면 메타 문자가 있는 파일을 지원하도록 명령을 수정할 수 있습니까?
답변1
여기에서 글로브를 사용하면 훨씬 쉽습니다 zsh
.
for f (**/*.xml(.)) (mv -v -- $f **/$f:r:t(/[1]))
또는 숨겨진 xml 파일을 포함하고 다음과 같이 숨겨진 디렉터리 내부를 살펴보려는 경우 find
:
for f (**/*.xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))
그러나 .xml
, ..xml
또는 라는 파일이 ...xml
문제가 될 수 있으므로 해당 파일을 제외할 수 있습니다.
setopt extendedglob
for f (**/(^(|.|..)).xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))
GNU 도구를 사용하면 각 파일에 대해 전체 디렉토리 트리를 스캔할 필요가 없는 또 다른 접근 방식은 해당 파일을 한 번 스캔하고 모든 디렉토리와 파일을 찾아 xml
위치를 기록하고 마지막에 이동하는 것입니다.
(export LC_ALL=C
find . -mindepth 1 -name '*.xml' ! -name .xml ! \
-name ..xml ! -name ...xml -type f -printf 'F/%P\0' -o \
-type d -printf 'D/%P\0' | awk -v RS='\0' -F / '
{
if ($1 == "F") {
root = $NF
sub(/\.xml$/, "", root)
F[root] = substr($0, 3)
} else D[$NF] = substr($0, 3)
}
END {
for (f in F)
if (f in D)
printf "%s\0%s\0", F[f], D[f]
}' | xargs -r0n2 mv -v --
)
임의의 파일 이름을 허용하려는 경우 접근 방식에는 여러 가지 문제가 있습니다.
{}
쉘 코드에 삽입하는 것은언제나잘못된. 예를 들어 이라는 파일이 있으면 어떻게 될까요$(rm -rf "$HOME").xml
? 올바른 방법은 이를{}
인라인 쉘 스크립트(-exec sh -c 'use as "$1"...' sh {} \;
)에 인수로 전달하는 것입니다.- GNU를 사용하면
find
(여기서 를 사용하는 것처럼 암시됨-quit
)*.xml
유효한 문자와 그 뒤에 오는 일련의 유효한 문자로 구성된 파일만 일치하므로.xml
현재 로케일에서 유효하지 않은 문자가 포함된 파일 이름(예: 잘못된 문자 집합의 파일 이름)은 제외됩니다. 이에 대한 수정은 모든 바이트가 유효한 문자가 되도록 로케일을 수정하는 것입니다C
(즉, 오류 메시지는 영어로 표시됩니다). - 해당
xml
파일 중 디렉터리 또는 심볼릭 링크 유형이 있으면 문제가 발생할 수 있습니다(디렉터리 검색에 영향을 미치거나 이동할 때 심볼릭 링크가 끊어짐).-type f
일반 파일만 이동 하려면 를 추가할 수도 있습니다 . - 명령 대체(
$(...)
) 스트립모두후행 개행 문자. 예를 들어 파일에 문제가 발생할 수 있습니다foo.xml
. 이를 해결하는 것은 가능하지만 고통스럽습니다base=$(basename "$1" .xml; echo .); base=${base%??}
. 최소한 운영자basename
로 대체할 수 있습니다${var#pattern}
. 그리고 가능하다면 명령 대체를 피하세요. ?
와일드카드 문자( ,[
및 백슬래시) 가 포함된 파일 이름에 문제가 있습니다.*
이러한 문자는 셸에 특수한 것이 아니라 셸 패턴 일치와 매우 유사한 패턴 일치(fnmatch()
) 에 대한 것입니다.find
백슬래시를 사용하여 이스케이프 처리해야 합니다.- 위에서 언급 한 ,
.xml
의 문제입니다 ...xml
...xml
따라서 위의 사항을 모두 해결하면 다음과 같은 결론이 나옵니다.
LC_ALL=C find . -type f -name '*.xml' ! -name .xml ! -name ..xml \
! -name ...xml -exec sh -c '
for file do
base=${file##*/}
base=${base%.xml}
escaped_base=$(printf "%s\n" "$base" |
sed "s/[[*?\\\\]/\\\\&/g"; echo .)
escaped_base=${escaped_base%??}
find . -name "$escaped_base" -type d -exec mv -v "$file" {\} \; -quit
done' sh {} +
휴...
이제 그것이 전부가 아닙니다. 를 사용하면 가능한 -exec ... {} +
한 적게 실행합니다 . sh
운이 좋으면 하나만 실행하게 되지만, 그렇지 않다면 첫 번째 sh
호출 이후에 여러 xml
파일을 이동한 다음 find
계속해서 더 많은 파일을 찾고 우리가 가지고 있는 파일을 아주 잘 찾을 수도 있습니다. 다시 첫 번째 라운드로 이동했습니다(아마도 현재 위치로 이동하려고 시도할 것입니다).
그 외에는 기본적으로 zsh와 동일한 접근 방식입니다. 몇 가지 다른 주목할만한 차이점은 다음과 같습니다.
- 하나를 사용하면
zsh
파일 목록이 디렉터리 이름과 파일 이름별로 정렬되므로 대상 디렉터리가 어느 정도 일관되고 예측 가능합니다. 를 사용하면find
디렉터리에 있는 파일의 원시 순서를 기반으로 합니다. - 을 사용하면 위의 접근 방식
zsh
이 아닌 파일을 이동할 일치하는 디렉터리가 없으면 오류 메시지가 표시됩니다find
. - 를 사용하면
find
해당 디렉토리가 아닌 일부 디렉토리를 탐색할 수 없는 경우 오류 메시지가 표시됩니다zsh
.
마지막 경고입니다. 의심스러운 파일 이름을 가진 일부 파일을 얻는 이유가 디렉터리 트리가 공격자에 의해 쓰기 가능하기 때문이라면, 공격자가 해당 명령 아래에서 파일 이름을 바꿀 수 있다면 위의 해결 방법 중 어느 것도 안전하지 않으므로 주의하십시오.
예를 들어 , LXDE 를 사용하는 경우 공격자 는 악성 foo/lxde-rc.xml
. 여러 가지 방법으로) 그것을 찾는 것과 수행 하는 것 사이에 ( 다른 곳으로 이동하도록 하는 심볼릭 링크로 변경될 수도 있습니다 ).lxde-rc
lxde-rc
~/.config/openbox/
find
lxde-rc
mv
rename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")
foo
lxde-rc.xml
이 문제를 해결하는 것은 표준 유틸리티나 심지어 GNU 유틸리티를 사용하여 불가능할 수 있습니다. 적절한 프로그래밍 언어로 작성하고 안전한 디렉터리 탐색을 수행하고 renameat()
시스템 호출을 사용해야 합니다.
rename()
위의 모든 해결 방법은 디렉토리 트리가 에 의해 수행된 시스템 호출 에 제공된 경로 길이 제한에 mv
도달할 만큼 충분히 깊어도 실패합니다(로 인해 rename()
실패함 ENAMETOOLONG
). 를 사용하는 솔루션으로 renameat()
도 문제를 해결할 수 있습니다.
답변2
와 함께 인라인 스크립트를 사용하는 경우 위치 매개변수를 통해 결과를 셸에 find ... -exec sh -c ...
전달해야 합니다 . 그러면 인라인 스크립트의 모든 위치에서 find
사용할 필요가 없습니다 .{}
bash
또는 이 있는 경우 다음을 통해 출력을 zsh
전달할 수 있습니다 .basename
printf '%q'
find . -name "*.xml" -exec bash -c '
for f do
BASENAME="$(printf "%q" "$(basename -- "$f" .xml)")"
DST=$(find . -type d -name "$BASENAME" -print -quit)
[ -d "$DST" ] && mv -v -- "$f" "$DST/"
done
' bash {} +
에서는 을 bash
사용할 수 있으며 printf -v BASENAME
, 파일 이름에 제어 문자나 ASCII가 아닌 문자가 포함되어 있으면 이 접근 방식이 제대로 작동하지 않습니다.
제대로 작동하려면 , 및 백슬래시만 이스케이프하는 쉘 함수를 작성 [
해야 *
합니다 ?
.
답변3
좋은 뉴스:
find . -name '[ foo ].xml'
쉘에 의해 해석되지 않고 이 방식으로 find 프로그램에 전달됩니다. 그러나 Find는 인수를 패턴 -name
으로 해석하므로 glob
이를 고려해야 합니다.
find -exec \;
전화 를 걸고 싶다면 find -exec +
쉘이 필요하지 않습니다.
find
셸에서 출력을 처리하려면 set -f
문제의 코드 이전에 호출하여 셸에서 파일 이름 글로빙을 비활성화하고 나중에 호출하여 다시 켜는 것이 좋습니다 set +f
.
답변4
다음은 비교적 간단한 POSIX 호환 파이프라인입니다. 계층 구조를 두 번 스캔합니다. 먼저 디렉터리를 검색한 다음 *.xml 일반 파일을 검색합니다. 스캔 사이의 빈 라인은 전환의 AWK 신호를 나타냅니다.
AWK 구성요소는 기본 이름을 대상 디렉터리에 매핑합니다(동일한 기본 이름을 가진 디렉터리가 여러 개 있는 경우 첫 번째 순회만 기억됩니다). 각 *.xml 파일에 대해 두 개의 필드, 즉 1) 파일 경로와 2) 해당 대상 디렉터리가 있는 탭으로 구분된 줄을 인쇄합니다.
{
find . -type d
echo
find . -type f -name \*.xml
} |
awk -F/ '
!NF { ++i; next }
!i && !($NF".xml" in d) { d[$NF".xml"] = $0 }
i { print $0 "\t" d[$NF] }
' |
while IFS=' ' read -r f d; do
mv -- "$f" "$d"
done
읽기 직전에 IFS에 할당된 값은 공백이 아닌 리터럴 탭 문자입니다.
다음은 원래 질문의 touch/mkdir 뼈대를 사용한 기록입니다.
$ touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
$ mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
$ find .
.
./foo
./foo/[ foo ]
./bar.xml
./foo.xml
./bar
./bar/( bar )
./[ foo ].xml
./( bar ).xml
$ ../mv-xml.sh
$ find .
.
./foo
./foo/[ foo ]
./foo/[ foo ]/[ foo ].xml
./foo/foo.xml
./bar
./bar/( bar )
./bar/( bar )/( bar ).xml
./bar/bar.xml