while 루프의 scp 명령에서 공백을 벗어날 수 없습니다

while 루프의 scp 명령에서 공백을 벗어날 수 없습니다

무료 ESXi 6.5를 다른 무료 ESXi 6.5 호스트로 미러 백업하는 스크립트를 작성하려고 합니다. 거의 다 왔는데 이 문제로 인해 미칠 지경입니다. 이것은 스크립트의 일부입니다. 스크립트에 Bash를 사용하고 있습니다.

#!/bin/sh
find /vmfs/volumes/datastore1/ -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' ! -name *-flat.vmdk | while read line; do
    dir1=$(dirname "${line}"| sed 's/ /\\ /g')
    dir2=$(dirname "${line}"| sed 's/ /\\\\ /g')
    ssh -n [email protected] "mkdir -p $dir1"
    cmd=$(echo $line "XX.XX.XX.XX:\""$dir2"/\"")
    echo $cmd
    scp -pr $cmd
done

출력은 다음과 같습니다

  • 이름에 공백이 없는 모든 VM에 대해 성공했습니다.
  • 이름에 공백이 있는 모든 VM(VM 이름의 마지막 단어): 해당 파일 또는 디렉터리가 없습니다.

이 SCP가 전체 경로를 얻도록 모든 것을 시도했지만 모든 것을 무시합니다. 작은 따옴표, 큰 따옴표, 공백에 이스케이프 문자 넣기, 이중, 삼중 이스케이프 문자 넣기. args를 SCP에 직접 넣고 SCP의 모든 args를 변수에 넣은 다음 전달하십시오.

외부 스크립트를 실행하면 명령이 완벽하게 실행됩니다. 스크립트에서 실행할 때 오류가 발생하고 공백 다음의 마지막 부분만 사용합니다.

답변1

귀하의 코드는 여러 측면에서 결함이 있습니다.

-name *-flat.vmdk경향이있다글로빙; 확장되는 내용은 현재 작업 디렉터리의 파일에 따라 다릅니다. *인용해야 합니다(예 -name '*-flat.vmdk': ).

코드에 따옴표가 부족한 경우는 이번이 유일한 것이 아닙니다. echo $line때문에 결함이 있다이것(그리고이것일반적으로).

read line최소한 이어야 합니다 IFS= read -r line. 경로( 에서 반환 find)에 개행 문자(파일 이름에 유효한 문자)가 포함되어 있으면 여전히 실패합니다. 이런 이유로 find … -exec … \;더 좋습니다. 다음과 같이 갈 수 있습니다:

find … -exec sh -c '…' sh {} \;

이는 또 다른 수준의 인용을 도입합니다. 또는 다음과 같습니다:

find … -exec helper_script {} \;

인용이 더 쉬워집니다 helper_script. 후자의 접근 방식을 옹호하는 사람은 다음과 같습니다.이 답변, 여전히 대답은 다른 문제를 해결하지 못합니다.

귀하의 변수 dir1dir2공백을 처리하기 위해 번거로운 이스케이프를 주입하는 것 같습니다. 이런 식으로 탈출에 의존해서는 안됩니다. 공백과 함께 작동하도록 만들었더라도 일반적으로 이스케이프해야 하는 다른 문자가 있습니다. 올바른 방법은인용하다제대로.

인용에는 최소한 세 가지 수준이 있습니다.

  1. 호출된 원래 쉘에서 find;
  2. 에 의해 생성된 쉘 -exec sh또는 해석하는 쉘에서 helper_script;
  3. 에 의해 원격 측에 생성된 셸에서 ssh … "whatever command"( 에 의해 처리된 경로와 유사 scp)

a를 도입하면 helper_script첫 번째 수준이 나머지 수준을 방해하지 않게 됩니다. 주요 명령은 다음과 같습니다:

find /vmfs/volumes/datastore1/ -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' ! -name '*-flat.vmdk' -exec /path/to/helper_script {} \;

그리고 helper_script:

#!/bin/sh
# no need for bash

addrs=XX.XX.XX.XX

pth="$1"
drctry="${pth%/*}"
# no need for dirname (separate executable)

ssh "root@$addrs" "mkdir -p '$drctry'"
scp -pr "$pth" "$addrs:'$drctry/'"

이제 중요한 것은 문자열로 ssh가져오는 것 입니다. mkdir -p 'whatever/the var{a,b}e/expand$t*'이는 원격 쉘로 전달되며통역. 내부 작은따옴표가 없으면 원하지 않는 방식으로 해석될 수 있습니다. 내 예는 이것을 과장했습니다. 모든 귀찮은 캐릭터에서 탈출하려고 노력할 수는 있지만 어려울 것입니다. 그래서 인용하십시오.

하지만변수에 작은따옴표가 포함되어 있으면 원격 측에서 일부 하위 문자열을 인용 해제할 수 있습니다. 이로 인해 코드 주입 취약점이 발생합니다. 예를 들어 다음 경로는 다음과 같습니다.

…/foo/'$(nasty command)'bar/baz/…

작은 따옴표에 포함되어 해석되면 매우 위험합니다. 사전에 소독해야 합니다 $drctry:

drctry="$(printf '%s' "${pth%/*}" | sed "s/'/'\"'\"'/g")"

이제 위험한 경로의 예는 다음과 같습니다.

…/foo/'"'"'$(nasty command)'"'"'bar/baz/…

이는 의 사용법과 다소 유사 sed하지만 작은따옴표 문자가 이제 문제가 되는 유일한 문자이므로 더 좋을 것입니다.

scp기본적으로 같은 이유로 원격 경로에서도 유사한 인용이 필요합니다. 다시 말하지만, 백슬래시를 사용하여 적절하게 이스케이프하는 것이 더 번거롭습니다(가능한 경우).


약간의 개선 사항은 도우미 스크립트가 둘 이상의 개체를 처리할 수 있도록 한다는 것입니다. 이렇게 하면 쉘 프로세스가 더 적게 실행됩니다.

find /vmfs/volumes/datastore1/ -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' ! -name '*-flat.vmdk' -exec /path/to/helper_script_2 {} +

그리고 helper_script_2:

#!/bin/sh

addrs=XX.XX.XX.XX

for pth; do
   drctry="$(printf '%s' "${pth%/*}" | sed "s/'/'\"'\"'/g")"
   ssh "root@$addrs" "mkdir -p '$drctry'"
   scp -pr "$pth" "$addrs:'$drctry/'"
done

-exec sh -c '…'(또는 ) 를 사용하여 (도우미 스크립트를 참조하지 않고) 독립 실행형 명령을 작성할 수 있습니다 -exec sh -c "…". 가장 바깥쪽 인용문으로 인해 이는 인용 및/또는 도주 열광으로 변할 수 있습니다. 명령 대체와 여기 문서를 사용하는 다음 트릭은 이를 방지하는 데 유용합니다.

find /vmfs/volumes/datastore1/ \
   -type f \
   -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' \
 ! -name '*-flat.vmdk' \
   -exec sh -c "$(cat << 'EOF'

addrs=XX.XX.XX.XX

for pth; do
   drctry="$(printf '%s' "${pth%/*}" | sed "s/'/'\"'\"'/g")"
   ssh "root@$addrs" "mkdir -p '$drctry'" \
   && scp -pr "$pth" "$addrs:'$drctry/'"
done

EOF
   )" sh {} +

변수 확장의 맥락에서 이 내용(및 이전 스니펫의 일부 부분)을 완전히 이해하려면 다음 사항을 알아야 합니다.따옴표 안의 따옴표그리고EOF인용됐나(링크된 답변은 인용 man bash하지만 이것은 더 일반적입니다.POSIX 동작). 또한 -type f정규식과 일치하는 가능한 디렉토리를 배제하기 위해 추가했습니다 . 을 썼 ssh … && scp …으므로 전자가 실패하면( mkdir -p실패한 경우 포함) 후자는 실행되지 않습니다.

답변2

파이프( |) 오른쪽에 있는 항목을 쉘 스크립트로 옮긴 다음 다음과 같은 작업을 수행합니다.

find /vmfs/volumes/datastore1/ -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' ! -name *-flat.vmdk -exec /path/to/shell/script {} \;

{}성공적으로 모든 파일 이름을 적절하게 이스케이프 find한 다음 이스케이프/인용된 파일 이름을 첫 번째 인수로 전달하는 스크립트를 호출합니다. $1스크립트에서 간단히 액세스하세요 .

답변3

배열의 마법을 확인하세요.

$ line="meh bleh"
$ dir="hello\ world"
$ cmd=$(echo "$line" "$dir")
$ for i in $cmd; do echo "$i"; done
meh
bleh
hello\
world
$ for i in "$cmd"; do echo "$i"; done
meh bleh hello\ world
$ cmd=("$line" "$dir")
$ for i in "${cmd[@]}"; do echo "$i"; done
meh bleh
hello\ world
$

모든 것을 간단한 변수에 넣을 때의 문제는 아무도 더 이상 각 인수가 무엇인지 알 수 없다는 것입니다.

관련 정보