Escape quotes inside su -c - linux

I would like to pass a path potentially containing whitespace into an su -c command inside a .sh script:
su -c "xdg-desktop-menu install ${DesktopFile}" -m "${Username}"
$DesktopFile is my path that needs to get escaped. I tried many variations with ' inside "", with \" and several braces, but I have not come up with a solution. Interestingly enough, xdg-desktop-icon works fine, but xdg-desktop-menu dies at the whitespace. Any suggestions?

If you're using bash, printf '%q' can be used to escape special characters when quoting is insufficient.
su -c "xdg-desktop-menu install $(printf '%q' "$DesktopFile") -m $(printf '%q' "$Username")"
Notice that $(printf...) doesn't need quotes, but "$DesktopFile" does. The variable needs to be passed to printf safely, so it has to be quoted. printf '%q''s output is guaranteed to be immune to word splitting and globbing, so it doesn't need quotes. (Although, if you wanted to quote it, you could. It would need \" on either side.)

i can't make out if this is a bug in xdg-desktop-menu, since it works in xdg-desktop-icon, nevertheless there is a workaround I am now using:
cd "$PathWithSpaces"
su -c "xdg-desktop-menu install $DesktopFilenameWithoutSpaces" -m "$Username"
PathWithSpaces contains DesktopFilenameWithoutSpaces, seems to work reliably.

Related

Bash escape character

I have this very reduced example of a bash command, where I want the $ sign escaped.
So the command :
su -m user -c "echo $test"
should print out:
$test
a simple \$test does not work unfortunately. I tried lots of other stuff but still couldn't find a solution. Any suggestions ?
Put it in single quotes rather than double quotes.
su -m user -c 'echo \$test='
The single quotes keep the variable from being expanded by the original shell. The backslash then escapes the dollar sign in the shell run by su.
See Difference between single and double quotes in Bash
In answer to the comment, you can switch to double quoting to get single quotes into the string.
su -m user -c 'echo \$test='"'1'"

Get and use a password with special characters in Bash shell

I'm getting some troubles to use a password with special characters such as $ in a bash shell script.
My shell script is :
read -s -p "Password : " bindDNPass
ldapadd -H ldap://localhost -x -w $bindDNPass -D "dn=cn=Admin" -f /tmp/file.ldif
And the password could be something like $Something18$.
Well, the command
ldapadd -H ldap://localhost -x -W -D "dn=cn=Admin" -f /tmp/file.ldif`
asks for my $Something18$, and works fine.
But if I try
ldapadd -H ldap://localhost -x -w $Something18$ -D "dn=cn=Admin" -f /tmp/file.ldif
it doesn't work. I guess it's trying to resolve the variable $Something18, so I tried with \$Something18$, \$Something18\$, \\\$Something18$, ... but it keeps on failing...
How can I do? (Without changing my password...)
Put it in double-quotes and escape the $ symbol avoid special interpretation from the shell,
ldapadd -H ldap://localhost -x -w "\$Something18\$" -D "dn=cn=Admin" -f /tmp/file.ldif
(or) [more recommended]
Enclose it within single-quote to let the shell treat it as a literal string without expanding it,
ldapadd -H ldap://localhost -x -w '$Something18$' -D "dn=cn=Admin" -f /tmp/file.ldif
From the man bash page,
Enclosing characters in double quotes preserves the literal value of all characters within the quotes, with the exception of $, , \, and, when
history expansion is enabled, !. The
characters $ and retain their special meaning within double quotes. The backslash retains its special meaning only when followed by one of the following characters: $, `, ", \, or . A double quote may be quoted within double quotes by preceding it with a backslash. If enabled, history expansion will be performed unless an ! appearing in double quotes is escaped using a backslash. The backslash preceding the ! is not removed.
I see two potential problems with how you're reading and using the password:
When you use the read command without the -r option, it'll try to interpret escape (backslash) sequences, which may cause trouble.
When you use a variable without wrapping it in double-quotes, it'll try to split the value into separate words and also try to expand any wildcards into a list of matching filenames. This can cause massive confusion, so you should almost always double-quote variable references.
Fixing these potential problems gives this script snippet:
read -rs -p "Password : " bindDNPass
ldapadd -H ldap://localhost -x -w "$bindDNPass" -D "dn=cn=Admin" -f /tmp/file.ldif
...But, while you should do both of these mods to make your script more robust, neither of these will change how it handles the password $Something18$. In fact, when I tried your original snippet with that password, it got passed to ldapadd correctly. If your actual password has some other special characters in it (or you've played with the value of IFS), these might help; otherwise, there's something else going on.
If your password still doesn't work after these fixes, try putting set -x before the ldapadd command (and set +x after) so it'll print what's actually being passed to ldapadd. Well, it'll print it in a possibly confusing form: it'll print an equivalent command to what's actually being executed, which means it'll add quotes and/or escapes to the password parameter as necessary so that you could run that command and it'll do the same thing. When I tried it with $Something18$, it printed:
+ ldapadd -H ldap://localhost -x -w '$Something18$' -D dn=cn=Admin -f /tmp/file.ldif
...where the single-quotes mean that what's inside them is passed directly, with no parsing. It could also have printed any of the following equivalent commands:
+ ldapadd -H ldap://localhost -x -w \$Something18\$ -D dn=cn=Admin -f /tmp/file.ldif
+ ldapadd -H ldap://localhost -x -w "\$Something18\$" -D dn=cn=Admin -f /tmp/file.ldif
+ ldapadd -H ldap://localhost -x -w $'$Something18$' -D dn=cn=Admin -f /tmp/file.ldif
so you have to take what it prints, and figure out how that'd be parsed by bash, in order to figure out what's actually being passed to ldapadd. But at least it'll give you some information about what's actually happening.
Oh, and you may notice that the DN argument isn't being double-quoted. That's because it doesn't contain any special characters, so the double-quotes aren't doing anything, so it just left them off.

Bash command line arguments passed to sed via ssh

I am looking to write a simple script to perform a SSH command on many hosts simultaneously, and which hosts exactly are generated from another script. The problem is that when I run the script using sometihng like sed it doesn't work properly.
It should run like sshall.sh {anything here} and it will run the {anything here} part on all the nodes in the list.
sshall.sh
#!/bin/bash
NODES=`listNodes | grep "node-[0-9*]" -o`
echo "Connecting to all nodes and running: ${#:1}"
for i in $NODES
do
:
echo "$i : Begin"
echo "----------------------------------------"
ssh -q -o "StrictHostKeyChecking no" $i "${#:1}"
echo "----------------------------------------"
echo "$i : Complete";
echo ""
done
When it is run with something like whoami it works but when I run:
[root#myhost bin]# sshall.sh sed -i '/^somebeginning/ s/$/,appendme/' /etc/myconfig.conf
Connecting to all nodes and running: sed -i /^somebeginning/ s/$/,appendme/ /etc/myconfig.conf
node-1 : Begin
----------------------------------------
sed: -e expression #1, char 18: missing command
----------------------------------------
node-1 : Complete
node-2 : Begin
----------------------------------------
sed: -e expression #1, char 18: missing command
----------------------------------------
node-2 : Complete
…
Notice that the quotes disappear on the sed command when sent to the remote client.
How do I go about fixing my bash command?
Is there a better way of achieving this?
Substitute an eval-safe quoted version of your command into a heredoc:
#!/bin/bash
# ^^^^- not /bin/sh; printf %q is an extension
# Put your command into a single string, with each argument quoted to be eval-safe
printf -v cmd_q '%q ' "$#"
while IFS= read -r hostname; do
# run bash -s remotely, with that string passed on stdin
ssh -q -o 'StrictHostKeyChecking no' "$hostname" "bash -s" <<EOF
$cmd_q
EOF
done < <(listNodes | grep -o -e "node-[0-9*]")
Why this works reliably (and other approaches don't):
printf %q knows how to quote contents to be eval'd by that same shell (so spaces, wildcards, various local quoting methods, etc. will always be supported).
Arguments given to ssh are not passed to the remote command individually!
Instead, they're concatenated into a string passed to sh -c.
However: The output of printf %q is not portable to all POSIX-derived shells! It's guaranteed to be compatible with the same shell locally in use -- ksh will always parse output from printf '%q' in ksh, bash will parse output from printf '%q' in bash, etc; thus, you can't safely pass this string on the remote argument vector, because it's /bin/sh -- not bash -- running there. (If you know your remote /bin/sh is provided by bash, then you can run ssh "$hostname" "$cmd_q" safely, but only under this condition).
bash -s reads the script to run from stdin, meaning that passing your command there -- not on the argument vector -- ensures that it'll be parsed into arguments by the same shell that escaped it to be shell-safe.
You want to pass the entire command -- with all of its arguments, spaces, and quotation marks -- to ssh so it can pass it unchanged to the remote shell for parsing.
One way to do that is to put it all inside single quotation marks. But then you'll also need to make sure the single quotation marks within your command are preserved in the arguments, so the remote shell builds the correct arguments for sed.
sshall.sh 'sed -i '"'"'/^somebeginning/ s/$/,appendme/'"'"' /etc/myconfig.conf'
It looks redundant, but '"'"' is a common Bourne trick to get a single quotation mark into a single-quoted string. The first quote ends single-quoting temporarily, the double-quote-single-quote-double-quote construct appends a single quotation mark, and then the single quotation mark resumes your single-quoted section. So to speak.
Another trick that can be helpful for troubleshooting is to add the -v flag do your ssh flags, which will spit out lots of text, but most importantly it will show you exactly what string it's passing to the remote shell for parsing and execution.
--
All of this is fairly fragile around spaces in your arguments, which you'll need to avoid, since you're relying on shell parsing on the opposite end.
Thinking outside the box: instead of dealing with all the quoting issues and the word-splitting in the wrong places, you could attempt to a) construct the script locally (maybe use a here-document?), b) scp the script to the remote end, then c) invoke it there. This easily allows more complex command sequences, with all the power of shell control constructs etc. Debugging (checking proper quoting) would be a breeze by simply looking at the generated script.
I recommend reading the command(s) from the standard input rather than from the command line arguments:
cmd.sh
#!/bin/bash -
# Load server_list with user#host "words" here.
cmd=$(</dev/stdin)
for h in ${server_list[*]}; do
ssh "$h" "$cmd"
done
Usage:
./cmd.sh <<'CMD'
sed -i '/^somebeginning/ s/$/,appendme/' /path/to/file1
# other commands
# here...
CMD
Alternatively, run ./cmd.sh, type the command(s), then press Ctrl-D.
I find the latter variant the most convenient, as you don't even need for here documents, no need for extra escaping. Just invoke your script, type the commands, and press the shortcut. What could be easier?
Explanations
The problem with your approach is that the quotes are stripped from the arguments by the shell. For example, the argument '/^somebeginning/ s/$/,appendme/' will be interpreted as /^somebeginning/ s/$/,appendme/ string (without the single quotes), which is an invalid argument for sed.
Of course, you can escape the command with the built-in printf as suggested in other answer here. But the command becomes not very readable after escaping. For example
printf %q 'sed -i /^somebeginning/ s/$/,appendme/ /home/ruslan/tmp/file1.txt'
produces
sed\ -i\ /\^somebeginning/\ s/\$/\,appendme/\ /home/ruslan/tmp/file1.txt
which is not very readable, and will look ugly, if you print it to the screen in order to show the progress.
That's why I prefer to read from the standard input and leave the command intact. My script prints the command strings to the screen, and I see them just in the form I have written them.
Note, the for .. in loop iterates $IFS-separated "words", and is generally not preferred way to traverse an array. It is generally better to invoke read -r in a while loop with adjusted $IFS. I have used the for loop for simplicity, as the question is really about invoking the ssh command.
Logging into multiple systems over SSH and using the same (or variations on the same) command is the basic use case behind ansible. The system is not without significant flaws, but for simple use cases is pretty great. If you want a more solid solution without too much faffing about with escaping and looping over hosts, take a look.
Ansible has a 'raw' module which doesn't even require any dependencies on the target hosts, and you might find that a very simple way to achieve this sort of functionality in a way that frees you from the considerations of looping over hosts, handling errors, marshalling the commands, etc and lets you focus on what you're actually trying to achieve.

Ubuntu Bash Script executing a command with spaces

I have a bit of an issue and i've tried several ways to fix this but i can't seem to.
So i have two shell scripts.
background.sh: This runs a given command in the background and redirect's output.
#!/bin/bash
if test -t 1; then
exec 1>/dev/null
fi
if test -t 2; then
exec 2>/dev/null
fi
"$#" &
main.sh: This file simply starts the emulator (genymotion) as a background process.
#!/bin/bash
GENY_DIR="/home/user/Documents/MyScript/watchdog/genymotion"
BK="$GENY_DIR/background.sh"
DEVICE="164e959b-0e15-443f-b1fd-26d101edb4a5"
CMD="$BK player --vm-name $DEVICE"
$CMD
This works fine when i have NO spaces in my directory. However, when i try to do: GENY_DIR="home/user/Documents/My Script/watchdog/genymotion"
which i have no choice at the moment. I get an error saying that the file or directory cannot be found. I tried to put "$CMD" in quote but it didn't work.
You can test this by trying to run anything as a background process, doesn't have to be an emulator.
Any advice or feedback would be appreciated. I also tried to do.
BK="'$BK'"
or
BK="\"$BK\""
or
BK=$( echo "$BK" | sed 's/ /\\ /g' )
Don't try to store commands in strings. Use arrays instead:
#!/bin/bash
GENY_DIR="$HOME/Documents/My Script/watchdog/genymotion"
BK="$GENY_DIR/background.sh"
DEVICE="164e959b-0e15-443f-b1fd-26d101edb4a5"
CMD=( "$BK" "player" --vm-name "$DEVICE" )
"${CMD[#]}"
Arrays properly preserve your word boundaries, so that one argument with spaces remains one argument with spaces.
Due to the way word splitting works, adding a literal backslash in front of or quotes around the space will not have a useful effect.
John1024 suggests a good source for additional reading: I'm trying to put a command in a variable, but the complex cases always fail!
try this:
GENY_DIR="home/user/Documents/My\ Script/watchdog/genymotion"
You can escape the space with a backslash.

Variables and escaping in RPM SPEC file macros?

I want to define a macro that will replace some placeholders in makefiles and systemd unit files with the results of RPM variables (macros). However, I don't know if the way expansion works will make the following behave correctly:
%define repl_vars() (sed -e "s:\${LIBEXECDIR}:%{_libexecdir}:g" -e "s:\${LOCALSTATEDIR}:%{_localstatedir}:g" -e "s:\${SYSCONFIGDIR}:%{_sysconfdir}:g" %{1} > %{1}.new && mv %{1}.new %{1})
Where the capitalized ${...} are the placeholders to be replaced with the actual paths held by the standard RPM variables (macros).
Also, escaping the $ of the placeholders with \ works in Bash and stuff I put in the %install section of the SPEC file, but is that still valid in a macro? And is the %{1} valid, as I've never seen an example -- and if not, how do I concatenate .new to %1?
If this is wrong, how do I do it?
tl;dr Yes, that looks basically correct to me. See http://www.rpm.org/wiki/PackagerDocs/Macros for some (somewhat outdated but still largely relevant) documentation.
Macros that are to be expanded in a shell context in a spec file just want to expand to the literal lines that you would write in the spec file section manually.
So assuming you want something to the effect of:
%install
....
sed -e 's:${LIBEXECDIR}:%{_libexecdir}:g' -e 's:${LOCALSTATEDIR}:%{_localstatedir}:g' -e 's:${SYSCONFIGDIR}:%{_sysconfdir}:g' 'some_file' > 'some_file.new' && mv 'some_file.new' 'some_file'
....
And you want to call your macro as %repl_vars some_file then you want a macro roughly like this:
%define repl_vars() sed -e 's:${LIBEXECDIR}:%{_libexecdir}:g' -e 's:${LOCALSTATEDIR}:%{_localstatedir}:g' -e 's:${SYSCONFIGDIR}:%{_sysconfdir}:g' '%{1}' > '%{1}.new' && mv '%{1}.new' '%{1}'
Notice I switched to single quotes instead of double quotes to avoid $ from being evaluated and needing to be escaped. I also dropped the wrapping () because this didn't seem to need the forced sub-shell.

Resources