невозможно экранировать пробел в команде scp в цикле while

невозможно экранировать пробел в команде scp в цикле while

Я пытаюсь создать скрипт для создания зеркального бэкапа бесплатного 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похоже, вводят какое-то громоздкое экранирование для работы с пробелами. Вам не следует полагаться на экранирование таким образом. Даже если вам удалось заставить его работать с пробелами, есть другие символы, которые вам нужно будет экранировать в общем случае. Правильный способ —цитироватьправильно.

Существует как минимум три уровня цитирования:

  1. в исходной оболочке, где findвызывается;
  2. в оболочке, порожденной -exec shили в оболочке, интерпретирующей helper_script;
  3. в оболочке, созданной на удаленной стороне 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 {} \;

Он {}будет правильно экранировать каждое имя файла, которое он успешно finds, а затем вызывать ваш скрипт, передавая экранированное/заключенное в кавычки имя файла в качестве первого аргумента. Просто получите к нему доступ с помощью $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
$

Проблема помещения всех данных в простую переменную заключается в том, что теперь никто не может сказать, что представляет собой каждый аргумент.

Связанный контент