Как написать динамически сгенерированный heredoc в bash?

У меня есть сценарий оболочки, который подключается к удаленному компьютеру для выполнения действий. Одним из таких действий является установка одного или нескольких DNS-серверов. В то время как в основном статические данные легко захватить и передать на удаленную машину через SSH:

config_ntp()
{
    ssh -T admin@server_ip <<-NTPSERVER
    sysconf ntp addserver $NTPSERVER
    NTPSERVER
}

Создание списка команд с динамическим размером сложнее, чем я думал. Что я имею:

DNSSERVERS=(8.8.8.8 8.8.8.7)

config_dns()
{
    cmd=""
    for server in ${DNSSERVERS[@]}; do
        cmd+="network dns add nameserver $server$'\n' "
    done
    cmd+="service dns restart$'\n'"
    echo -e "cmd: $cmd"
    ssh -T admin@server_ip $cmd
}

Результат вызова этого:

$ sh setup.sh
cmd: network dns add nameserver 8.8.8.8$'
' network dns add nameserver 8.8.8.7$'
' service dns restart$'
'
Syntax Error: Invalid character detected: '\'.
Command Result : 22 (Invalid argument)
Exiting...

Это было мое последнее воплощение. Я играл с $'\n', как было предложено в другом месте... раньше у меня был только \n, что приводило к той же ошибке.

Как создать переменную, содержащую список (динамически сгенерированный список переменной длины) команд для передачи через ssh на удаленную машину?


person Jon    schedule 14.12.2017    source источник
comment
Попробуйте использовать: ssh admin@server_ip /bin/sh <<-ENDOfCmd" вместо -T.   -  person F. Hauri    schedule 14.12.2017
comment
Вместо этого вы можете использовать встроенную команду: buildscript() { echo...} и ssh admin@server /bin/sh < <(buildscript)   -  person F. Hauri    schedule 14.12.2017
comment
Проблема может быть связана с тем, что формат цитирования ($'\n') находится внутри ваших двойных кавычек. Попробуйте пройти тест: echo "abc$'\n'def". Что ты видишь? Как насчет использования точки с запятой вместо новой строки? Или (вздох) &&? Нет необходимости перезапускать DNS, если предыдущие изменения не увенчались успехом.   -  person ghoti    schedule 14.12.2017
comment
Кроме того, так ли ужасно высока стоимость запуска каждой удаленной команды на отдельном SSH? Вы можете уменьшить эту боль с помощью ControlMaster в конфигурации ssh, если хотите.   -  person ghoti    schedule 14.12.2017


Ответы (3)


Зачем вам нужен здесь документ? Предоставление стандартного ввода процессу каким-либо другим способом почти определенно проще, если это не просто статический шаблон с несколькими заполнителями, которые вы заполняете переменными.

ssh <<____HERE
commands
more commands "$variable"
____HERE

точно эквивалентно

printf "commands\nmore commands \"%s\"\n" "$variable" | ssh

Если некоторые части более сложны, чем просто печать чего-либо, вы можете сделать это произвольно сложным:

{ complex_function --with --options and arguments  -o stdout
  printf "echo 'anything we send to stdout goes into the pipe'\n"
  if command; then
      while another command; do
          for arguments in $(something to drive a loop); do
               complex stuff
          done
      done
  fi
  : and etc
} | ssh

Используйте фигурные скобки ( commands ) вместо фигурных скобок { commands } перед каналом для запуска commands в подоболочке.

Ваша конкретная проблема заключается в том, что вы не можете поместить $'\n' в обычную строку с двойными кавычками. echo уже создает новую строку, поэтому вы, кажется, используете очень окольный способ построения вещей. Но попробуйте это:

# Does this really need to be in a separate variable?
DNSSERVERS=(8.8.8.8 8.8.8.7)
config_dns()
{
    {
      # To be completely correct, notice quoting around array
      for server in "${DNSSERVERS[@]}"; do
        echo "network dns add nameserver $server"
      done
      echo "service dns restart"
    } |
    # maybe add a tee /dev/stderr or something here to see what's going on
    ssh -T admin@server_ip
}

Для эстетики я бы, возможно, провел рефакторинг

config_dns()
{
    {
        printf 'network dns add nameserver %s\n' "${DNSSERVERS[@]}"
        printf 'service dns restart\n'
    } |
    ssh -T admin@server_ip
}
person tripleee    schedule 14.12.2017
comment
Вы можете сделать его произвольно сложным... О боже... это подходит. - person David C. Rankin; 14.12.2017
comment
Нет: echo cmd| ssh ... не может совпадать с ssh < <(echo cmd): позаботьтесь о STD I/O, возвратах и ​​окружающей среде! - person F. Hauri; 14.12.2017
comment
@ F.Hauri Это не утверждение, сделанное здесь, у вас есть замена процесса. - person tripleee; 14.12.2017
comment
Вы написали: ssh <<here точно эквивалентно printf | ssh... Это не так! - person F. Hauri; 14.12.2017
comment
Хотите уточнить, что вы имеете в виду, кроме очевидной синтаксической разницы? - person tripleee; 14.12.2017
comment
@tripleee: ваш последний пример был блестящим сокращением решения проблемы! Пример, с которым я работаю, - это больше, чем просто замена одной переменной содержимым... но то, что вы написали в последнем примере, решает проблему. Я не согласен, однако, с вашим heredoc точно так же, как строка printf-with-\n ... они совсем не одинаковы. Переводы строки несколько отличаются тем, что в строке printf есть видимые sash-n, которые, кажется, передаются через туннель ssh на удаленную машину. Не имеет значения, если я использую ; или && либо - отклонено. Продолжение... - person Jon; 15.12.2017
comment
...Продолжение. Отклонено, потому что мы обрабатываем данные из туннеля ssh (потому что, причины) и отклоняем то, что мы считаем плохими символами. LF из heredoc не представляет проблемы, но LF в строке printf отклоняется. Я не понимаю, в чем тонкая разница между двумя строками. - person Jon; 15.12.2017
comment
Удаленный сервер, который отклоняет некоторые команды, является существенной дополнительной сложностью. Mayoe опубликует новый вопрос с более подробной информацией, если вам нужна помощь, чтобы понять это правильно. - person tripleee; 15.12.2017
comment
Это не проблема... просто любопытно, какова очевидная разница между двумя строками? - person Jon; 15.12.2017
comment
echo "test$'\n'" | ssh remote xxd должно показать разницу. Не в том месте, где я могу проверить это прямо сейчас. - person tripleee; 16.12.2017

Прежде всего, вы смешиваете передачу текста в stdin команды с передачей его в качестве аргумента. Вы должны изменить вызов ssh на:

ssh -T admin@server_ip <<< "$cmd"

Он использует herestrings для передачи текста в stdin команды (аналогично тому, что здесьдокументы делать).

Во-вторых, чтобы нотация $'\n' работала, вам нужно поместить ее вне двойных кавычек:

cmd+="network dns add nameserver $server"$'\n'" "
person Yoory N.    schedule 14.12.2017
comment
Хм... В чем разница между blah\n и blah$'\n'? Первая форма приводит к передаче всей строки, включая косую черту-n, где наше устройство отклоняет ее. Вторая форма (я упустил тонкость размещения кавычек) работает - теперь появляется перевод строки, чтобы не быть частью строки. Что мне здесь не хватает - какая разница (по сравнению с переводом строки) между heredoc и второй формой и включением их в строку? - person Jon; 15.12.2017
comment
Последний создает настоящую новую строку (за которой следует пробел, но я думаю, что здесь это ложно и не нужно). Первый, вероятно, просто последовательность обратной косой черты-n, вероятно, с некоторыми странностями вокруг нее. - person tripleee; 15.12.2017
comment
Итак, косая черта-n не является настоящей новой строкой? - person Jon; 15.12.2017
comment
Нет, это просто \ (0x5C), за которым следует n (0x6E). Чтобы он стал настоящей новой строкой (0x0A), команда, которой он передается, должна явно преобразовывать \n в новую строку. Например, сравните echo "foo\nbar" и echo -e "foo\nbar". Конструкция $'\n' преобразует \n в новую строку до ее передачи в команду. Пример: echo "foo"$'\n'"bar". - person Yoory N.; 17.12.2017

Цитировать через ssh очень сложно. Не делайте этого, если вам не нужно.

Вместо того, чтобы пытаться разделить ваши команды с помощью \n, просто используйте ;. Он работает так же хорошо и не требует так много цитирования.

DNSSERVERS=(8.8.8.8 8.8.8.7)

config_dns()
{
    cmd=""
    for server in ${DNSSERVERS[@]}; do
        cmd+="network dns add nameserver $server;"
    done
    cmd+="service dns restart"
    echo -e "cmd: $cmd"
    ssh -T admin@server_ip "$cmd"
}
person rob mayoff    schedule 14.12.2017
comment
Я думал попробовать ;. Я получил такое же сообщение об ошибке, просто ссылаясь на ; скорее, чем \. - person Jon; 14.12.2017
comment
Вы делаете все возможное, чтобы создать полезную переменную, а затем забываете заключать ее в двойные кавычки. - person tripleee; 14.12.2017