2

I have a shell script that connects to a remote machine to perform actions. One of those actions, is to set one or more DNS servers. While largely static data is easy to capture and pipe to the remote machine via SSH:

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

Creating a dynamically sized list of commands, is trickier than I thought. What I have:

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
}

The result of calling this:

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

That was my latest incarnation. I was playing with $'\n' as suggested elsewhere... previously, I just had \n, which resulted in the same error.

How do I create a variable containing a list (variable length, dynamically generated) of commands to pipe, via ssh, to a remote machine?

Jon
  • 1,675
  • 26
  • 57
  • Try to use: `ssh admin@server_ip /bin/sh <<-ENDOfCmd"` instead of using `-T`. – F. Hauri - Give Up GitHub Dec 14 '17 at 06:33
  • You could use *inline command* instead: `buildscript() { echo...}` and `ssh admin@server /bin/sh < <(buildscript)` – F. Hauri - Give Up GitHub Dec 14 '17 at 06:34
  • The problem you're having may be due to the format quoting (`$'\n'`) being *inside* your double quotes. Try a test: `echo "abc$'\n'def"`. What do you see? What about using semicolons instead of newlines? Or (gasp) `&&`? No need to restart dns if the preceding changes failed. – ghoti Dec 14 '17 at 06:55
  • Also, is the cost of running each remote command on its own SSH so terribly high? You can reduce that pain with [`ControlMaster`](https://stackoverflow.com/a/26470428/1072112) in your ssh config if you like. – ghoti Dec 14 '17 at 06:59

3 Answers3

3

Why do you want or need a here document? Providing standard input to the process by some other means is almost definitely easier if it's not just basically a static template with a few placeholders you fill with variables.

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

is exactly equivalent to

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

If some parts are more complex than just printing something, you can make it arbitarily complex:

{ 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

Use curly parentheses ( commands ) instead of braces { commands } before the pipe to run commands in a subshell.

Your concrete problem is that you can't put $'\n' inside a regular double-quoted string. echo already produces a newline so you seem to be taking a very roundabout way of constructing things. But try this:

# 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
}

For aesthetics, I would perhaps refactor to

config_dns()
{
    {
        printf 'network dns add nameserver %s\n' "${DNSSERVERS[@]}"
        printf 'service dns restart\n'
    } |
    ssh -T admin@server_ip
}
tripleee
  • 175,061
  • 34
  • 275
  • 318
  • 1
    *"you can make it arbitarily complex..."* Oh boy... that qualifies. – David C. Rankin Dec 14 '17 at 06:48
  • No: `echo cmd| ssh ... ` could not be same than `ssh < <(echo cmd)`: Care about *STD I/O*, returns and environment! – F. Hauri - Give Up GitHub Dec 14 '17 at 06:54
  • @F.Hauri That's not the claim made here, yours has a process substitution. – tripleee Dec 14 '17 at 06:59
  • Care to elaborate on what you mean, other than the obvious syntactic difference? – tripleee Dec 14 '17 at 07:07
  • @tripleee: your last example was a brilliant contraction of the solution to the problem! The example I am working with, is more than simply replacing one variable with "content"... but, what you wrote up in that last example solves the problem. I disagree though, with your "heredoc is exactly the same as printf-with-\n string... they are not at all, the same. The linefeeds are somehow "different", in that, the printf string has visible shash-n's that seem to be transported via the ssh tunnel, to the remote machine. Make no difference if I use ; or && either - it's rejected. Cont'd... – Jon Dec 15 '17 at 19:55
  • ...Cont'd. Rejected because, we process the data from the ssh tunnel (because, reasons) and reject what we feel are "bad" characters". The LF from the heredoc presents no problem, but the LF in the printf string, is rejected. I don't understand what the subtle difference is, between the two strings. – Jon Dec 15 '17 at 19:57
  • A remote server which rejects some commands is a significant additional complication. Mayoe post a new question with more details if you need help figuring that out properly. – tripleee Dec 15 '17 at 20:19
  • That's not the problem... just curious now, as to the apparent difference between te two string? – Jon Dec 15 '17 at 20:23
  • `echo "test$'\n'" | ssh remote xxd` should reveal the difference. Not in a place where I can test this right now. – tripleee Dec 16 '17 at 08:44
1

First of all, you're mixing up passing text to stdin of a command with passing it as an argument. You should change your ssh invocation to:

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

It uses herestrings to pass text to stdin of a command (similar to what heredocs do).

Second, for $'\n' notation to work, you need to place it outside of double quotes:

cmd+="network dns add nameserver $server"$'\n'" "
Yoory N.
  • 4,881
  • 4
  • 23
  • 28
  • Hmm... What is the difference between "blah\n" and "blah"$'\n'? The first form results in the entire string, incl the slash-n, to be passed, where our appliance rejects it. The second form (I missed the subtlety of the quote placement) works - the linefeed appears now, to not be "part of" the string. What am I missing here - what difference is there (vis-a-vis the linefeeds) with the heredoc and the second form, vs including them in the string? – Jon Dec 15 '17 at 19:45
  • The latter produces an actual newline (followed by a space, but I think that's spurious and unnecessary here). The former, probably, just a backslash-n sequence, probably with some oddities around it. – tripleee Dec 15 '17 at 20:17
  • So, a slash-n is not an "actual newline"? – Jon Dec 15 '17 at 20:25
  • 2
    No, it is just a "\" ("0x5C") followed by "n" ("0x6E"). For it to become an actual newline ("0x0A") the command it is passed to should explicitly convert "\n" to newline. For example, compare `echo "foo\nbar"` and `echo -e "foo\nbar"`. The `$'\n'` construct converts "\n" to newline **before** it is passed to a command. Example: `echo "foo"$'\n'"bar"`. – Yoory N. Dec 17 '17 at 04:55
0

Quoting through ssh is very complicated. Don't do it if you don't have to.

Instead of trying to separate your commands with \n, just use ;. It works just as well and doesn't require so much quoting.

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"
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • I had thought to try the ;. I got the same error message, just referring to ; rather than \. – Jon Dec 14 '17 at 05:02
  • You go to great lengths to build a useful variable, then forget to put double quotes around it. – tripleee Dec 14 '17 at 07:02