`find` 명령을 사용하여 쉘 메타문자를 자동으로 이스케이프하는 방법은 무엇입니까?

`find` 명령을 사용하여 쉘 메타문자를 자동으로 이스케이프하는 방법은 무엇입니까?

동일한 디렉토리 트리 내에서 동일한 이름을 가진 해당 폴더로 이동하고 싶은 디렉토리 트리 아래에 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-rclxde-rc~/.config/openbox/findlxde-rcmvrename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")foolxde-rc.xml

이 문제를 해결하는 것은 표준 유틸리티나 심지어 GNU 유틸리티를 사용하여 불가능할 수 있습니다. 적절한 프로그래밍 언어로 작성하고 안전한 디렉터리 탐색을 수행하고 renameat()시스템 호출을 사용해야 합니다.

rename()위의 모든 해결 방법은 디렉토리 트리가 에 의해 수행된 시스템 호출 에 제공된 경로 길이 제한에 mv도달할 만큼 충분히 깊어도 실패합니다(로 인해 rename()실패함 ENAMETOOLONG). 를 사용하는 솔루션으로 renameat()도 문제를 해결할 수 있습니다.

답변2

와 함께 인라인 스크립트를 사용하는 경우 위치 매개변수를 통해 결과를 셸에 find ... -exec sh -c ...전달해야 합니다 . 그러면 인라인 스크립트의 모든 위치에서 find사용할 필요가 없습니다 .{}

bash또는 이 있는 경우 다음을 통해 출력을 zsh전달할 수 있습니다 .basenameprintf '%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

관련 정보