Systemd string escaping - systemd

If I run this command
/bin/bash -c 'while true;do /usr/bin/etcdctl set my-container "{\"host\": \"1\", \"port\": $(/usr/bin/docker port my-container 5000 | cut -d":" -f2)}" --ttl 60;sleep 45;done'
I get back from etcd what I expect {"host":"1", "port":49155}
But if I put it in a systemd file
ExecStart=/bin/bash -c 'while true;do /usr/bin/etcdctl set my-container "{\"host\": \"1\", \"port\": $(/usr/bin/docker port my-container 5000 | cut -d":" -f2)}" --ttl 60;sleep 45;done'
I get back {host:1, port:49155}
Any idea of why the escaping is different inside of the file? How can I fix it? Thanks!!

systemd-escape '\"a fun thing"\'
output: \x5c\x22a\x20fun\x20thing\x22\x5c
[Service]
ExecStart=/bin/sh -c 'echo "\x5c\x22a\x20fun\x20thing\x22\x5c"'
will print a fun thing

Systemd is doing isn't like bash as you now know, hence the escaping problem. In fact, systemd removes single and double quotes after parsing them. That fact is right out of the documentation (I went thru this too, then read :D).
The solution, call a script that echo back that info you need (with escaped quotes) if your purpose allows that.

In short -- it's different because systemd does its own string-splitting, unescaping and expansion, and the logic it uses isn't POSIX-compliant.
You can still do what you want, but you'll need more backslashes:
ExecStart=/bin/bash -c '\
while :; do \
port=$(/usr/bin/docker port my-container 5000 | cut -d: -f2); \
/usr/bin/etcdctl set my-container "{\\\"host\\\": \\\"1\\\", \\\"port\\\": $port}" --ttl 60; \
sleep 45; \
done'
Note the use of \\\" for every literal " character in the desired output.
By the way -- personally, I advise against trying to generate JSON through string concatenation -- it's prone to injection vulnerabilities (if someone could put content of their choice in the output of the docker port command, they could potentially insert other key/value pairs into your data by having , "evil": true be in the port variable). This class of issues is avoided by using jq:
ExecStart=/bin/bash -c '\
while :; do \
port=$(/usr/bin/docker port my-container 5000 | cut -d: -f2); \
json=$(jq -nc \
--arg host 1 \
--arg port "$port" \
'{} | .host=$host | .port=($port | tonumber)'); \
/usr/bin/etcdctl set my-container "$json" --ttl 60; \
sleep 45; \
done'
As a happy side effect, the above avoids needing any literal double-quote characters (the only ones used are syntactic to the copy of sh), so we don't need any backslashes to be passed through from systemd to the shell.

Related

Nested quoting in bash

I'm connecting to a remote server via SSH:
ssh -i ~/.ssh/pk.pem user#server
and then, on that server, open bash within a Docker container:
docker exec -it $(docker ps | grep ecs-worker-low | cut -d ' ' -f1) bash
This works fine. (Note that I need to get the container ID like this. I'm not able to name the container.)
I would like to combine the two commands, so that I only run one command and get the shell within the container. This can be done with something like this:
ssh -i ~/.ssh/pk.pem user#server -t "bash -c 'docker exec -it $(docker ps | grep ecs-worker-low | cut -d ' ' -f1) bash'"
However this doesn't work because of the nested single quotes. I haven't found any way around this. Can you please help me? Thank you.
You can avoid the use of cut with --filter and --format
ssh -t -i ~/.ssh/pk.pem user#serve 'docker exec -it $(docker ps --filter ancestor=ecs-worker-low --format {{.ID}}) bash'
It's probably easiest to use a heredoc:
ssh -i ~/.ssh/pk.pem user#server -t << \EOF
docker exec -it $(docker ps | grep ecs-worker-low | cut -d ' ' -f1)
EOF
Make sure you use a non-interpolating heredoc. If you omit the backslash on the initial delimiter, the process substitution will be made on the local host.
Swap the quotes:
ssh -i ~/.ssh/pk.pem user#server -t 'bash -c "docker exec -it $(docker ps | grep ecs-worker-low | cut -d " " -f1) bash"'
All the double quotes are literal characters as far as ssh is concerned, and the command substitution creates a new context so that the first inner quote does not close the first outer quote. That said...
... Simplifying matters, you likely don't need the outer bash; ssh can run docker for you directly:
ssh -i ~/.ssh/pk.pem user#server -t 'docker exec -it $(docker ps | grep ecs-worker-low | cut -d " " -f1) bash'

Read variables in nested quotes

I want to ssh into a host and start a container and run some commands. So the code will be like this:
ssh $host 'screen -L -d -m bash -c "docker run "\
"--network=host -v ~/data:/data myimage:${TAG_NAME}"\
" /bin/bash -c \" some command.... \""'
The question is simple, since I was using single quote, I can't read the ${TAG_NAME}. Is there any way to write this kind of nested quotes and also pass the variable?
You can stop and start your single quotes to include the environment variable, like so:
echo 'foo'"$HOME"'foo'
For your example, the way to include an env var (from your local system) in the command that runs on $host would be:
ssh $host 'screen -L -d -m bash -c "docker run'\
' --network=host -v ~/data:/data myimage:'"$TAG_NAME"\
' /bin/bash -c \" some command.... \""'

Properly Escape $ in a nested remote command

I would like to execute a command on a remote host from another remote host.
HOST1=host1.domain.tld
HOST2=host2.domain.tld
HOST1 is used to connect to HOST2 and the command executes on HOST2. The remote command depends a variable that is calculated on HOST2.
ssh -A $HOST1 -C "x=wrong; ssh -A $HOST2 -C "x=right; echo \$x""
Strangely, the above returns $x while the next command returns wrong instead of an empty line.
ssh -A $HOST1 -C "x=wrong; ssh -A $HOST2 -C "echo \$x""
Question 1: Why is the first command giving me $x?
Question 2: Keeping the double quotes, how do I have it print right?
Section 1: Literal Answers
...to the question precisely as-asked.
Why is the first command giving me $x?
Keep in mind that this command is executed multiple times, and is thus transformed by multiple shells. That transformation looks something like the following (assuming HOST1 of 1.1.1.1 and HOST2 of 2.2.2.2):
ssh -A 1.1.1.1 -C "x=wrong; ssh -A 2.2.2.2. -C "x=right; echo \$x""
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^
...note the arrows? Those are showing where your quoted regions begin and end: Your quote just before x=right is ending your quote that started before x=wrong!
Thus, this tokenizes to two separate commands, written out with one shell word per line below:
# command one: ssh
ssh \
-A \
1.1.1.1 \
-C \
"x=wrong; ssh -A 2.2.2.2. -C "x=right;
# command two: echo
echo \
\$x""
Keeping the double quotes, how do I have it print right?
Backslash-escape the nested quotes so they don't close the quotes you intend to be outer.
ssh -A $HOST1 -C "x=wrong; ssh -A $HOST2 -C \"x=right; echo \$x\""
Section 2: Best-Practice Alternatives
SSH - ProxyCommand
In practice, don't do this kind of explicit nested SSH invocation at all -- just use the ProxyCommand ssh config option:
ssh \
-o "ProxyCommand ssh $HOST1 netcat -w 120 %h %p' \
"$HOST2" 'x=right; echo "$x"'
Bash - nestable eval-safe quote generation
In general, trying to escape things by hand is much more error-prone than telling the shell to do it for you.
host2_cmd='x=right; echo "$x"'
printf -v host1_cmd '%q ' ssh -A "$HOST2" -C "$host2_cmd"
ssh "$HOST1" bash -s <<<"$host1_cmd"
To demonstrate, we could even do this with a third host in the way:
host3_cmd='x=right; echo "$x"'
printf -v host2_cmd '%q ' ssh -A "$HOST3" -C "$host3_cmd"
printf -v host1_cmd '%q ' ssh -A "$HOST2" -C "$host2_cmd"
ssh "$HOST1" bash -s <<<"$host1_cmd"
This works because in ksh and bash, printf %q quotes a string in such a way that it'll evaluate to its current contents when parsed by that same shell.

curl -F line break not interpreted correctly

I'm trying to send a notification via pushover using curl in a bash script.
I cannot get curl -F to interpret the line break correctly though.
curl -s \
-F "token=TOKEN" \
-F "user=USER" \
-F "message=Root Shell Access on HOST \n `date` \n `who` " \
https://api.pushover.net/1/messages.json > NUL
I've tried:
\n
\\\n
%A0
I'd rather push the message out directly, not through a file.
curl doesn't interpret backslash escapes, so you have to insert an actual newline into the argument which curl sees. In other words, you have to get the shell (bash in this case) to interpret the \n, or you need to insert a real newline.
A Posix standard shell does not interpret C escapes like \n, although the standard utility command printf does. However, bash does provide a way to do it: in the quotation form $'...' C-style backslash escapes will be interpreter. Otherwise, $'...' acts just like '...', so that parameter and command substitutions do not take place.
However, any shell -- including bash -- allows newlines to appear inside quotes, and the newline is just passed through as-is. So you could write:
curl -s \
-F "token=$TOKEN" \
-F "user=$USER" \
-F "message=Root Shell Access on $HOST
$(date)
$(who)
" \
https://api.pushover.net/1/messages.json > /dev/null
(Note: I inserted parameter expansions where it seemed like they were missing from the original curl command and changed the deprecated backtick command substitutions to the recommended $(...) form.)
The only problem with including literal newlines, as above, is that it messes up indentation, if you care about appearances. So you might prefer bash's $'...' form:
curl -s \
-F "token=$TOKEN" \
-F "user=$USER" \
-F "message=Root Shell Access on $HOST"$'\n'"$(date)"$'\n'"$(who)" \
https://api.pushover.net/1/messages.json > /dev/null
That's also a little hard to read, but it is completely legal. The shell allows a single argument ("word") to be composed of any number of quoted or unquoted segments, as long as there is no whitespace between the segments. But you can avoid the multiple quote syntax by predefining a variable, which some people find more readable:
NL=$'\n'
curl -s \
-F "token=$TOKEN" \
-F "user=$USER" \
-F "message=Root Shell Access on $HOST$NL$(date)$NL$(who)" \
https://api.pushover.net/1/messages.json > /dev/null
Finally, you could use the standard utility printf, if you are more used to that style:
curl -s \
-F "token=$TOKEN" \
-F "user=$USER" \
-F "$(printf "message=Root Shell Access on %s\n%s\n%s\n" \
"$HOST" "$(date)" "$(who)")" \
https://api.pushover.net/1/messages.json > /dev/null

Escaping in wget bash command

wget -q -T 60 --retry-connrefused -t 5 --waitretry=60 --user=ftp2.company.com|company2013 --password=!company2013 -N -P data/parser/company/ ftp://ftp2.company.com/Production/somedata.zip
I'm having trouble with this command, because the password contains an exclamation mark. I tried escaping with \, tried single quotes, and it either gives the output:
wget: missing URL
or
bash: !company2013: event not found
This is really demotivating...
Perhaps this part needs to be quoted to prevent it from being seen as a pipe to another command.
--user='ftp2.company.com|company2013'
And this one too to prevent history expansion with !:
--password='!company2013'
Final:
wget -q -T 60 --retry-connrefused -t 5 --waitretry=60 --user='ftp2.company.com|company2013' --password='!company2013' -N -P data/parser/company/ ftp://ftp2.company.com/Production/somedata.zip
And it's also a good idea to quote the other parts if on later time they have spaces:
wget -q -T 60 --retry-connrefused -t 5 --waitretry=60 --user='ftp2.company.com|company2013' --password='!company2013' -N -P "data/parser/company/" "ftp://ftp2.company.com/Production/somedata.zip"

Resources