22

I have an odd error that I have been unable to find anything on this. I wanted to change the user comment with the following command.

$ sudo usermod -c "New Comment" user

This will work while logged onto a server but I want to automate it across 20+ servers. Usually I am able to use a list and loop through the servers and run a command but in this case I get a error.

$ for i in `cat servlist` ; do echo $i ; ssh $i sudo usermod -c "New Comment" user ; done 
serv1
Usage: usermod [options] LOGIN

Options:
lists usermod options

serv2
Usage: usermod [options] LOGIN

Options:
lists usermod options
.
.
.

When I run this loop it throws back an error like I am using the command incorrectly but it will run just fine on a single server.

Looking through the ssh man pages I did try -t and -t -t flags but those did not work.

I have successfully used perl -p -i -e within a similar loop to edit files.

Does anyone know a reason I am unable to loop this?

Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
SpruceTips
  • 601
  • 1
  • 11
  • 23

3 Answers3

35

SSH executes the remote command in a shell. It passes a string to the remote shell, not a list of arguments. The arguments that you pass to the ssh commands are concatenated with spaces in between. The arguments to ssh are sudo, usermod, -c, New Comment and user, so the remote shell sees the command

sudo usermod -c New Comment user

usermod parses Comment as the name of the user and user as a spurious extra parameter.

You need to pass the quotes to the remote shell so that the comment is treated as a string. The simplest way is to put the whole remote command in single quotes. If you need a single quote in that command, use '\''.

ssh "$i" 'sudo usermod -c "Jack O'\''Brian" user'

Instead of calling ssh in a loop and ignoring errors, use a tool designed to run commands on multiple servers such as pssh, mussh, clusterssh, etc. See Automatically run commands over SSH on many servers

Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
8
for i in `cat servlist`;do echo $i;ssh $i 'sudo usermod -c "New Comment" user';done

or

for i in `cat servlist`;do echo $i;ssh $i "sudo usermod -c \"New Comment\" user";done
Mel
  • 610
  • 3
  • 6
0

You can use following convenient wrapper script ssh.sh

EDIT 2023/04/28. Finally I figure out the perfect solution, fixed the issue mentioned by @user202729, yet not over-programing.

The final ssh wrapper is:

#!/bin/bash
args=(); for v in "$@"; do args+=("$(printf %q "$v")"); done
ssh "${args[@]}"

You can create it by copy&paste run:

cat <<'EOF' > ssh.sh
#!/bin/bash
args=(); for v in "$@"; do args+=("$(printf %q "$v")"); done
ssh "${args[@]}"
EOF

chmod +x ssh.sh

Then you can safely call ssh via the ssh.sh, without worrying about escaping.

./ssh.sh host sudo usermod -c "New Comment" user

A full test:

First create a utility /tmp/show_args.sh which shows all arguments

cat <<'EOF' > /tmp/show_args.sh
#!/bin/bash
for arg in "$@"; do echo "ARG$((++i))=${arg@Q}"; done
EOF

chmod +x /tmp/show_args.sh

The do the full test:

./ssh.sh 127.0.0.1 -n /tmp/show_args.sh "a a" "'b b'" '"c c"' '*' '()' $'line1\nline2' $'\001    a' 'zz   '

The output is:

ARG1='a a'
ARG2=''\''b b'\'''
ARG3='"c c"'
ARG4='*'
ARG5='()'
ARG6=$'line1\nline2'
ARG7=$'\001    a'
ARG8='zz   '

You can see that all arguments are same as the input. Note

''\''b b'\'''

just means literal

'b b'
osexp2000
  • 452
  • 1
  • 3
  • 10
  • This will give wrong result if the output of `%q` contains two consecutive spaces e.g. it happens for `echo $'\x01 12 3'` in my bash version – user202729 Jun 28 '22 at 09:54
  • oh sorry to here that. Maybe it is because of bash version. Could you test it with following command? I have tried it, no problem. `./ssh.sh host echo $'\x01 12 3' | od -txCc`, then check the binary output, it should be `01 20 31 32 20 33 0a`. – osexp2000 Jun 28 '22 at 23:14
  • But `echo $'\x01 12 3'` executed locally is 01 20 20 31 32 20 20 33 0a, and your theory is it will be the same remotely, but it's not, because command substitution `$( )` breaks at whitespace -- even if it occurs between quotemarks, unlike shell input. This applies to all bash versions (although very old versions don't have %q and never reach the point of error). More dramatically, try `ssh.sh host echo $'\x07 * * * IMPORTANT * * *'`. – dave_thompson_085 Jun 29 '22 at 02:14
  • @dave_thompson_085 nice catch, thanks for telling me this, I will find a workaround. – osexp2000 Jun 29 '22 at 02:40
  • @user202729, thanks for letting me know this issue. I have updated my answer, added some test cases and a workaround for this issue at the end. – osexp2000 Jun 29 '22 at 03:33
  • 2
    No need, overcomplicated. Just change `ssh $(escape "$@") ` to `ssh "$(escape "$@")" ` in the first script (didn't test but should work. – user202729 Jun 29 '22 at 03:34
  • @user202729 yes, I have first tried that, but that breaks other things, it will treat the `ssh host cmd args` to `ssh 'host cmd args'`, so it did not work. That's why I split the scripts. – osexp2000 Jun 29 '22 at 03:43
  • Updated my answer, improved to a perfect solution, yet not over-programming. – osexp2000 Apr 27 '23 at 17:46