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
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
-
Another good tool is Ansible for running commands on multiple servers.Charlie Dalsass– Charlie Dalsass2021年05月24日 22:29:02 +00:00Commented May 24, 2021 at 22:29
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
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'
-
This will give wrong result if the output of
%q
contains two consecutive spaces e.g. it happens forecho $'\x01 12 3'
in my bash versionuser202729– user2027292022年06月28日 09:54:41 +00:00Commented 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 be01 20 31 32 20 33 0a
.osexp2000– osexp20002022年06月28日 23:14:51 +00:00Commented 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, tryssh.sh host echo $'\x07 * * * IMPORTANT * * *'
.dave_thompson_085– dave_thompson_0852022年06月29日 02:14:45 +00:00Commented Jun 29, 2022 at 2:14 -
@dave_thompson_085 nice catch, thanks for telling me this, I will find a workaround.osexp2000– osexp20002022年06月29日 02:40:53 +00:00Commented Jun 29, 2022 at 2:40
-
2No need, overcomplicated. Just change
ssh $(escape "$@")
tossh "$(escape "$@")"
in the first script (didn't test but should work.user202729– user2027292022年06月29日 03:34:53 +00:00Commented Jun 29, 2022 at 3:34