20

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?

3 Answers 3

32

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

1
  • Another good tool is Ansible for running commands on multiple servers. May 24, 2021 at 22:29
7
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
0

You can use following convenient wrapper script ssh.sh

cat <<'EOF' > ssh.sh
#!/bin/bash

function escape() {
  for arg in "$@"; do
    printf "%q " "$arg"
  done
}

ssh $(escape "$@")
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

EDIT 2022-06-29: Tested cases:

$ ./ssh.sh host /tmp/show_args.sh "a   a" "'b   b'" '"c   c"' '*' '()' $'line1\nline2'

ARG1=<a   a>
ARG2=<'b   b'>
ARG3=<"c   c">
ARG4=<*>
ARG5=<()>
ARG6=<line1
line2>

EDIT 2022-06-29: CAVEATS: however, it does not work well for $'\xNN' parameter, such as $'\x01 spaces ...', the inner spaces will be shrunk to 1 space.

$ ./ssh.sh host /tmp/show_args.sh $'\x01              a'
ARG1=< a>     # the 0x1 is there but just can not be printed, you can see the space become just one now instead of multiple spaces.

To work around this issue, I have to use argument_wrapper_for_ssh.sh:

cat <<'EOF' > argument_wrapper_for_ssh.sh
#!/bin/bash
for arg in "$@"; do
  printf "%q " "$arg"
done
EOF

chmod +x argument_wrapper_for_ssh.sh

Then use it like this:

$ ./ssh.sh host "$(./argument_wrapper_for_ssh.sh SSH_COMMAND SSH_COMMAND_ARGS)"

(Of course you can insert/append any ssh related options to it anywhere)

e.g.,

$ ./ssh.sh host "$(./argument_wrapper_for_ssh.sh echo $'\x01     a')"

The result will be (in hex)

01  20  20  20  20  20  61  0a

you can see that the inner spaces are honestly kept.

Note that, to workaround above $'\xNN' issue, it can not be simply be achieved by modifying

ssh $(escape "$@")

to

ssh "$(escape "$@")"

because this ssh.sh is meant to replace ssh so it will be passed some ssh related arguments, as a result, which will be joined to ssh command itself as another big command, such as

./ssh.sh host cmd args

will become

ssh "host cmd args"

which will try to connect to a host named "host cmd args".

7
  • 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, 2022 at 9: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.
    – osexp2003
    Jun 28, 2022 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 * * *'. Jun 29, 2022 at 2:14
  • @dave_thompson_085 nice catch, thanks for telling me this, I will find a workaround.
    – osexp2003
    Jun 29, 2022 at 2:40
  • 1
    No need, overcomplicated. Just change ssh $(escape "$@") to ssh "$(escape "$@")" in the first script (didn't test but should work.
    – user202729
    Jun 29, 2022 at 3:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.