디렉토리 트리 아래에 여러 개의 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
메타 문자가 있는 파일을 지원하도록 명령을 수정할 수 있도록 with 인수를 사용할 때 파일 이름을 이스케이프하는 일반적인 방법이 있습니까 ?
답변1
여기서 glob을 사용하는 것이 훨씬 쉽습니다 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
이름이 or인 파일은 ...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