no se puede escapar del espacio en el comando scp en un bucle while

no se puede escapar del espacio en el comando scp en un bucle while

Estoy intentando crear un script para hacer una copia de seguridad reflejada de un ESXi 6.5 gratuito en otro host ESXi 6.5 gratuito. Ya casi he llegado, pero este problema me está volviendo loco. Esto es parte del guión; Estoy usando Bash para el 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

la salida es:

  • para cada VM que no tiene espacios en el nombre, tuvo éxito.
  • para cada máquina virtual con espacios en el nombre (última palabra en el nombre de la máquina virtual): no existe tal archivo o directorio

Intenté todo para que este SCP obtenga la ruta completa y lo ignora todo: poner comillas simples, comillas dobles, caracteres de escape al espacio, caracteres de escape dobles y triples. Coloque los argumentos directamente en SCP, coloque todos los argumentos de SCP en una variable y páselo después.

Cuando se ejecuta un script externo, el comando se ejecuta sin problemas. Cuando se ejecuta en un script, genera un error y solo ocupa la última parte después del espacio.

Respuesta1

Su código tiene fallas en muchos aspectos.

-name *-flat.vmdkes propenso aglobo; a qué se expande depende de los archivos en el directorio de trabajo actual. *debe citarse (p. ej -name '*-flat.vmdk'.).

Esta no es la única vez que a su código le faltan comillas. echo $linees defectuoso debido aeste(yesteen general).

read linedebería ser al menos IFS= read -r line. Aún fallaría si alguna ruta (devuelta por find) contuviera el carácter de nueva línea (que es un carácter válido en los nombres de archivos). Por eso find … -exec … \;es mejor. Puedes ir así:

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

lo que introduce otro nivel de citación; o así:

find … -exec helper_script {} \;

lo que hace que citar sea más helper_scriptfácil. Este último enfoque es defendido poresta respuesta, aún así la respuesta no soluciona otros problemas.

Sus variables dir1y dir2parecen inyectar algunos escapes engorrosos para lidiar con espacios. No debes confiar en escapar así. Incluso si lograste que funcionara con espacios, hay otros personajes de los que necesitarías escapar en general. La manera correcta escitaadecuadamente.

Hay al menos tres niveles de cotización:

  1. en el shell original donde findse invoca;
  2. en un shell generado por -exec sho en un shell que interpreta helper_script;
  3. en un shell generado en el lado remoto ssh … "whatever command"(de manera similar para las rutas procesadas por scp).

Introducir un helper_scripthace que el primer nivel no interfiera con el resto. El comando principal sería:

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

Y el 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/'"

Ahora lo importante es sshconseguirlo mkdir -p 'whatever/the var{a,b}e/expand$t*'como una cuerda. Esto se pasa al shell remoto yinterpretado. Sin las comillas simples internas, podría interpretarse de una manera que no desee; mi ejemplo exagera esto. Podrías intentar escapar de cada personaje problemático, sería difícil; así que cita.

Perosi la variable contiene comillas simples, entonces alguna subcadena puede no estar entre comillas en el lado remoto. Esto abre una vulnerabilidad de inyección de código. Por ejemplo, este camino:

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

Será muy peligroso cuando se incluya entre comillas simples y se interprete. Debes desinfectar $drctrypreviamente:

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

El ejemplo de ruta peligrosa ahora se verá así:

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

Esto es algo similar a su uso de sed, pero dado que el carácter de comilla simple es ahora el único carácter problemático, debería ser mejor.

scpnecesita cotizaciones similares en la ruta remota básicamente por la misma razón. Nuevamente, el escape adecuado con barras invertidas es más complicado (si es que es posible).


Una ligera mejora es permitir que el script auxiliar procese más de un objeto. Esto ejecutará menos procesos de shell:

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

Y el 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

Es posible crear un comando independiente (sin hacer referencia a ningún script auxiliar) con -exec sh -c '…'(o -exec sh -c "…"). Debido a las citas más externas, esto se convertiría en un frenesí de citar y/o escapar. El siguiente truco con sustitución de comandos y aquí documento es útil para evitar esto:

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 comprender completamente esto (y algunos fragmentos de fragmentos anteriores) en el contexto de la expansión de variables, necesita conocercitas dentro de comillasypor qué EOFse cita(la respuesta vinculada cita man bashpero esto es más generalComportamiento POSIX). También tenga en cuenta que agregué -type fpara descartar posibles directorios que coincidan con la expresión regular; y escribí ssh … && scp …, por lo que si el primero falla (lo que incluye cuando mkdir -pfalla), el segundo no se ejecutará.

Respuesta2

Mueva las cosas a la derecha de la tubería ( |) a un script de shell, luego haga algo como

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

Escapará {}correctamente todos y cada uno de los nombres de archivo que envíe correctamente findy luego llamará a su secuencia de comandos pasando el nombre de archivo escapado/comillado como primer argumento. Simplemente acceda a él con $1su script.

Respuesta3

Sea testigo de la magia de la matriz:

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

El problema de poner todo en una variable simple es que ya nadie puede decir cuál es cada argumento.

información relacionada