Я пытаюсь создать скрипт для создания зеркального бэкапа бесплатного 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
выход:
- для каждой виртуальной машины, в имени которой нет пробелов, выполнено успешно.
- для каждой виртуальной машины с пробелами в имени (последнее слово в имени виртуальной машины): Нет такого файла или каталога
Я пробовал все, чтобы заставить этот SCP получить полный путь, но он все игнорирует: ставил одинарные кавычки, двойные кавычки, экранирующий символ на пробел, двойные, тройные экранирующие символы. Помещал аргументы непосредственно в SCP, помещал все аргументы SCP в переменную и передавал ее после.
При запуске вне скрипта команда выполняется безупречно. При запуске в скрипте выдает ошибку и занимает только последнюю часть после пробела.
решение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
. Последний подход пропагандируетсяэтот ответ, но ответ все равно не решает другие проблемы.
Ваши переменные dir1
и, dir2
похоже, вводят какое-то громоздкое экранирование для работы с пробелами. Вам не следует полагаться на экранирование таким образом. Даже если вам удалось заставить его работать с пробелами, есть другие символы, которые вам нужно будет экранировать в общем случае. Правильный способ —цитироватьправильно.
Существует как минимум три уровня цитирования:
- в исходной оболочке, где
find
вызывается; - в оболочке, порожденной
-exec sh
или в оболочке, интерпретирующейhelper_script
; - в оболочке, созданной на удаленной стороне
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
s, а затем вызывать ваш скрипт, передавая экранированное/заключенное в кавычки имя файла в качестве первого аргумента. Просто получите к нему доступ с помощью $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
$
Проблема помещения всех данных в простую переменную заключается в том, что теперь никто не может сказать, что представляет собой каждый аргумент.