Ich versuche, ein Skript zu erstellen, um ein Spiegel-Backup eines kostenlosen ESXi 6.5 auf einem anderen kostenlosen ESXi 6.5-Host zu erstellen. Ich bin fast fertig, aber dieses Problem macht mich verrückt. Dies ist ein Teil des Skripts; ich verwende Bash für das Skript:
#!/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
Die Ausgabe ist:
- für jede VM, deren Name keine Leerzeichen enthält, erfolgreich.
- für jede VM mit Leerzeichen im Namen (letztes Wort im VM-Namen): Keine solche Datei oder kein solches Verzeichnis
Ich habe alles versucht, damit dieser SCP den vollständigen Pfad erhält, und er ignoriert alles: Setzen Sie einfache Anführungszeichen, doppelte Anführungszeichen, Escape-Zeichen durch Leerzeichen, doppelte, dreifache Escape-Zeichen. Setzen Sie Argumente direkt in SCP, setzen Sie alle Argumente von SCP in eine Variable und übergeben Sie sie anschließend.
Wenn der Befehl außerhalb eines Skripts ausgeführt wird, läuft er fehlerfrei. Wenn er im Skript ausgeführt wird, tritt ein Fehler auf und nur der letzte Teil nach dem Leerzeichen wird übernommen.
Antwort1
Ihr Code ist in vielerlei Hinsicht fehlerhaft.
-name *-flat.vmdk
Ist anfällig fürGlotzen; wohin es erweitert wird, hängt von den Dateien im aktuellen Arbeitsverzeichnis ab. *
sollte in Anführungszeichen gesetzt werden (z. B. -name '*-flat.vmdk'
).
Dies ist nicht das einzige Mal, dass Ihrem Code Anführungszeichen fehlen. echo $line
ist fehlerhaft wegenDas(UndDasIm Algemeinen).
read line
sollte mindestens sein IFS= read -r line
. Es würde immer noch fehlschlagen, wenn ein Pfad (der von zurückgegeben wird find
) das Newline-Zeichen (ein gültiges Zeichen in Dateinamen) enthielte. Aus diesem Grund find … -exec … \;
ist es besser. Sie können so vorgehen:
find … -exec sh -c '…' sh {} \;
was eine andere Ebene des Zitierens einführt; oder so:
find … -exec helper_script {} \;
was das Zitieren erleichtert helper_script
. Letzterer Ansatz wird befürwortet vondiese Antwort, die Antwort behebt immer noch keine anderen Probleme.
Ihre Variablen dir1
scheinen dir2
einige umständliche Escape-Zeichen einzufügen, um mit Leerzeichen umzugehen. Sie sollten sich nicht auf Escape-Zeichen wie dieses verlassen. Selbst wenn Sie es mit Leerzeichen hinbekommen, gibt es andere Zeichen, die Sie generell escapen müssen. Der richtige Weg ist,Zitatrichtig.
Es gibt mindestens drei Zitierebenen:
- in der ursprünglichen Shell, wo
find
aufgerufen wird; - in einer Shell, die von erzeugt wurde
-exec sh
oder in einer Shell, die das interpretierthelper_script
; - in einer Shell, die auf der Remote-Seite von erzeugt wird
ssh … "whatever command"
(ähnlich für Pfade, die von verarbeitet werdenscp
).
Durch die Einführung von a helper_script
wird sichergestellt, dass die erste Ebene nicht mit dem Rest in Konflikt gerät. Der Hauptbefehl wäre:
find /vmfs/volumes/datastore1/ -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' ! -name '*-flat.vmdk' -exec /path/to/helper_script {} \;
Und das 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/'"
Jetzt ist das Wichtigste, es als String ssh
zu bekommen . Dieser wird an die Remote-Shell übergeben undmkdir -p 'whatever/the var{a,b}e/expand$t*'
interpretiert. Ohne die inneren einfachen Anführungszeichen könnte es auf eine Weise interpretiert werden, die Sie nicht möchten; mein Beispiel übertreibt dies. Sie könnten versuchen, jedes störende Zeichen zu maskieren, das wäre schwierig; also zitieren Sie.
AberWenn die Variable ein einfaches Anführungszeichen enthält, können auf der Remote-Seite einige Teilzeichenfolgen entfernt werden. Dies öffnet eine Schwachstelle für Code-Injection. Beispiel: dieser Pfad:
…/foo/'$(nasty command)'bar/baz/…
wird sehr gefährlich, wenn es in einfache Anführungszeichen eingebettet und interpretiert wird. Sie sollten $drctry
vorher bereinigen:
drctry="$(printf '%s' "${pth%/*}" | sed "s/'/'\"'\"'/g")"
Der gefährliche Beispielpfad sieht nun folgendermaßen aus:
…/foo/'"'"'$(nasty command)'"'"'bar/baz/…
Dies ähnelt in gewisser Weise Ihrer Verwendung von sed
, aber da das einfache Anführungszeichen nun das einzige problematische Zeichen ist, sollte es besser sein.
scp
erfordert aus im Wesentlichen demselben Grund ähnliche Anführungszeichen im Remote-Pfad. Auch hier ist das korrekte Escapen mit Backslashs umständlicher (falls überhaupt möglich).
Eine kleine Verbesserung besteht darin, dem Hilfsskript zu erlauben, mehr als ein Objekt zu verarbeiten. Dadurch werden weniger Shell-Prozesse ausgeführt:
find /vmfs/volumes/datastore1/ -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' ! -name '*-flat.vmdk' -exec /path/to/helper_script_2 {} +
Und das 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 '…'
Es ist möglich, mit (oder ) einen eigenständigen Befehl (ohne Bezug auf ein Hilfsskript) zu erstellen -exec sh -c "…"
. Wegen der meisten äußeren Anführungszeichen würde dies in einem Anführungszeichen- und/oder Escape-Wahnsinn münden. Der folgende Trick mit Befehlsersetzung und diesem Dokument ist nützlich, um dies zu vermeiden:
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 {} +
Um dies (und einige Fragmente in den vorherigen Snippets) im Kontext der Variablenerweiterung vollständig zu verstehen, müssen Sie Folgendes wissen:Anführungszeichen in AnführungszeichenUndwarum EOF
wird zitiert(die verlinkte Antwort zitiert man bash
, aber das ist allgemeinerPOSIX-Verhalten). Beachten Sie auch, dass ich hinzugefügt habe, -type f
um mögliche Verzeichnisse auszuschließen, die dem regulären Ausdruck entsprechen; und ich habe geschrieben ssh … && scp …
, sodass, wenn Ersteres fehlschlägt (was auch den Fall einschließt, dass es mkdir -p
fehlschlägt), Letzteres nicht ausgeführt wird.
Antwort2
Verschieben Sie den Inhalt rechts vom Pipe-Zeichen ( |
) in ein Shell-Skript und führen Sie dann Folgendes aus:
find /vmfs/volumes/datastore1/ -regex '.*\.\(vmx\|nvram\|vmsd\|vmdk\)$' ! -name *-flat.vmdk -exec /path/to/shell/script {} \;
Das {}
wird jeden einzelnen Dateinamen, der erfolgreich ist, ordnungsgemäß maskieren find
und dann Ihr Skript aufrufen und den maskierten/in Anführungszeichen gesetzten Dateinamen als erstes Argument übergeben. Greifen Sie einfach $1
in Ihrem Skript darauf zu.
Antwort3
Erleben Sie die Magie des Arrays:
$ 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
$
Das Problem, wenn man alles in eine einfache Variable packt, ist, dass niemand mehr erkennen kann, was jedes Argument ist.