não é possível escapar do espaço no comando scp em um loop while

não é possível escapar do espaço no comando scp em um loop while

Estou tentando criar um script para fazer um backup espelhado de um ESXi 6.5 gratuito para outro host ESXi 6.5 gratuito. Estou quase lá, mas esse problema está me deixando louco. Isso faz parte do roteiro; Estou usando o Bash para o script:

#!/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

a saída é:

  • para cada VM que não possui espaços no nome, foi bem-sucedido.
  • para cada VM com espaços no nome, (última palavra no nome da VM): Arquivo ou diretório inexistente

Tentei de tudo para fazer esse SCP obter o caminho completo e ele ignora tudo: colocar aspas simples, aspas duplas, caracteres de escape para espaço, caracteres de escape duplos, triplos. Coloque args diretamente no SCP, coloque todos os args do SCP em uma variável e passe depois.

Ao executar script externo, o comando é executado perfeitamente. Ao executar em script, está dando erro e ocupa apenas a última parte depois do espaço.

Responder1

Seu código é falho em muitos aspectos.

-name *-flat.vmdké propenso aglobulando; o que ele expande depende dos arquivos no diretório de trabalho atual. *deve ser citado (por exemplo -name '*-flat.vmdk').

Esta não é a única vez que faltam aspas em seu código. echo $lineé falho por causaesse(eesseem geral).

read linedeveria ser pelo menos IFS= read -r line. Ainda falharia se qualquer caminho (retornado por find) contivesse o caractere de nova linha (que é um caractere válido em nomes de arquivos). Por esse motivo find … -exec … \;é melhor. Você pode fazer assim:

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

o que introduz outro nível de cotação; ou assim:

find … -exec helper_script {} \;

o que torna a citação helper_scriptmais fácil. Esta última abordagem é defendida poresta resposta, ainda assim a resposta não resolve outros problemas.

Suas variáveis dir1​​​​parecem dir2injetar algumas fugas complicadas para lidar com espaços. Você não deve confiar em escapar assim. Mesmo que você conseguisse fazer funcionar com espaços, existem outros personagens dos quais você precisaria escapar em geral. O caminho certo écitarapropriadamente.

Existem pelo menos três níveis de cotação:

  1. no shell original onde findé invocado;
  2. em um shell gerado por -exec shou em um shell interpretando o helper_script;
  3. em um shell gerado no lado remoto por ssh … "whatever command"(da mesma forma para caminhos processados ​​por scp).

A introdução de a helper_scriptfaz com que o primeiro nível não interfira no resto. O comando principal seria:

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

E a 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/'"

Agora o importante é sshficar mkdir -p 'whatever/the var{a,b}e/expand$t*'como string. Isso é passado para o shell remoto einterpretado. Sem as aspas simples internas, isso poderia ser interpretado de uma maneira que você não deseja; meu exemplo exagera isso. Você poderia tentar escapar de todos os personagens problemáticos, seria difícil; então cite.

Masse a variável contiver aspas simples, alguma substring poderá ser sem aspas no lado remoto. Isso abre uma vulnerabilidade de injeção de código. Por exemplo, este caminho:

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

será muito perigoso quando incorporado entre aspas simples e interpretado. Você deve higienizar $drctrypreviamente:

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

O exemplo de caminho perigoso agora ficará assim:

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

Isso é um pouco semelhante ao uso de sed, mas como o caractere de aspas simples é agora o único caractere problemático, deveria ser melhor.

scpprecisa de cotações semelhantes no caminho remoto basicamente pelo mesmo motivo. Novamente, escapar adequadamente com barras invertidas é mais complicado (se possível).


Uma pequena melhoria é permitir que o script auxiliar processe mais de um objeto. Isso executará menos processos de shell:

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

E a 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

É possível construir um comando independente (sem referência a nenhum script auxiliar) com -exec sh -c '…'(ou -exec sh -c "…"). Por causa das citações mais externas, isso se transformaria em um frenesi de citação e/ou fuga. O seguinte truque com substituição de comando e aqui documento é útil para evitar isso:

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 {} +

Para entender isso completamente (e alguns fragmentos em trechos anteriores) no contexto da expansão de variáveis, você precisa saber sobrecitações dentro de citaçõesepor que EOFé citado(a resposta vinculada cita, man bashmas isso é mais geralComportamento POSIX). Observe também que adicionei -type fpara descartar possíveis diretórios que correspondam ao regex; e eu escrevi ssh … && scp …, portanto, se o primeiro falhar (o que inclui quando mkdir -pfalhar), o último não será executado.

Responder2

Mova o que está à direita do pipe ( |) para um script de shell e faça algo como

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

O {}escapará corretamente de cada nome de arquivo obtido com sucesso finde, em seguida, chamará seu script passando o nome do arquivo escapado/entre aspas como o primeiro argumento. Basta acessá-lo $1em seu script.

Responder3

Testemunhe a magia do array:

$ 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
$

O problema de colocar tudo em uma variável simples é que ninguém consegue mais dizer o que é cada argumento.

informação relacionada