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 line
deveria 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_script
mais fácil. Esta última abordagem é defendida poresta resposta, ainda assim a resposta não resolve outros problemas.
Suas variáveis dir1
parecem dir2
injetar 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:
- no shell original onde
find
é invocado; - em um shell gerado por
-exec sh
ou em um shell interpretando ohelper_script
; - em um shell gerado no lado remoto por
ssh … "whatever command"
(da mesma forma para caminhos processados porscp
).
A introdução de a helper_script
faz 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 é ssh
ficar 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 $drctry
previamente:
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.
scp
precisa 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 bash
mas isso é mais geralComportamento POSIX). Observe também que adicionei -type f
para descartar possíveis diretórios que correspondam ao regex; e eu escrevi ssh … && scp …
, portanto, se o primeiro falhar (o que inclui quando mkdir -p
falhar), 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 find
e, em seguida, chamará seu script passando o nome do arquivo escapado/entre aspas como o primeiro argumento. Basta acessá-lo $1
em 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.