How do you stop bash from stripping quotes when running a variable as a command? - bash

I have a command stored in a variable, which I then run:
[root#cxpxwly01wel001 init.d]# cat test
#!/bin/bash
command="su - root -c 'java -Xms16m -the_rest_of_my_java_command'"
echo $command
$command
[root#cxpxwly01wel001 init.d]# ./test
su - root -c 'java -Xms16m -the_rest_of_my_java_command'
su: invalid option -- 'X'
Try `su --help' for more information.
The su error is what you get if the single quotes aren't there:
[root#cxpxwly01wel001 init.d]# su - root -c java -Xms16m -the_rest_of_my_java_command
su: invalid option -- 'X'
Try `su --help' for more information.
With the single quotes there the command works as expected:
[root#cxpxwly01wel001 init.d]# su - root -c 'java -Xms16m -the_rest_of_my_java_command'
-bash: java: command not found
Which suggests to me that when running the variable as a command the single quotes aren't preserved. How do I preserve them?
Note: I'm editing an existing script (inserting the su - around the java command), so I'd like to change as little as possible to keep it close to the source.
Let me add the output when run with set -x:
+ command='su - root -c '\''java -Xms16m -the_rest_of_my_java_command'\'''
+ echo su - root -c ''\''java' -Xms16m '-the_rest_of_my_java_command'\'''
su - root -c 'java -Xms16m -the_rest_of_my_java_command'
+ su - root -c ''\''java' -Xms16m '-the_rest_of_my_java_command'\'''
su: invalid option -- 'X'
Usage: su [options] [LOGIN]
Options:
-c, --command COMMAND pass COMMAND to the invoked shell
-h, --help display this help message and exit
-, -l, --login make the shell a login shell
-m, -p,
--preserve-environment do not reset environment variables, and
keep the same shell
-s, --shell SHELL use SHELL instead of the default in passwd

The technique I use to avoid the "quoting inside quotes" issues is to build the command in an array. For instance, instead of :
command="su - root -c 'java -Xms16m -the_rest_of_my_java_command'"
I would use :
declare -a command=(su - root -c 'java -Xms16m -the_rest_of_my_java_command')
You can still echo the command (with an array expansion) :
echo "${command[#]}"
Or run it :
"${command[#]}"
The double quotes around the array expansion are important : they tell the shell to expand items without doing any word splitting, so an item containing whitespace will keep its identity as a single string.
By doing it this way, the last argument, which is inside quotes, will remain a single string, and the su command will not see -Xms16m as an (invalid) option.
As an aside, the [#] in "${command[#]}" means "all elements in the array". Whereas, in an expression like "${command[1]}", [1] would mean "only element at index 1".

Related

Is it possible using "su -c" with multiple commands but in one session?

I am trying run the following two command in one command.
eval "$(ssh-agent)"
ssh add ~/.ssh/id_rsa
I tried with many possible solutions:
su username -c "{ eval $(ssh-agent -s) }; ssh-add ~/.ssh/id_rsa"
su username -c "eval $(ssh-agent -s)" ; ssh-add ~/.ssh/id_rsa
su username -c "eval $(ssh-agent -s)" && ssh-add ~/.ssh/id_rsa
su username -c "eval $(ssh-agent -s)" && "ssh-add ~/.ssh/id_rsa"
It seems like the first command run successfully but the second either response with Permission denied message (means it run with the current user) or cannot connect to the authentication agent (means it probably created a new session for the second command).
Error messages:
Could not open a connection to your authentication agent.
Error connecting to agent: Permission denied
If I run them separately in order, it works:///
The purpose is to create a bash script with these commands with variables, something like this:
folder=/home/q12
username=q12
su $username -c "{ eval $(ssh-agent -s) }; ssh-add $folder/.ssh/id_rsa"
Because of the variables I can not quote the whole command because it will be sensitive to ";" and "&&".
Can anyone help me with this?
Thank you!
You need single quotes. Otherwise the command substitution is evaluated in the current context.
su username -c 'eval "$(ssh-agent -s)"; ssh-add ~/.ssh/id_rsa'
Edit:
To get conditional execution of ssh-add, you can do:
su username -c 'script=$(ssh-agent -s) || exit 1; eval "$script"; ssh-add ~/.ssh/id_rsa'
# or
su username -c 'set -e; script=$(ssh-agent -s); eval "$script"; ssh-add ~/.ssh/id_rsa'
The argument in su -c STRING is a command string, similar to bash -c STRING. We can also nest double quotes inside single quotes.
The first of your tries is closest, but inside double-quotes things like $(ssh-agent -s) get evaluated by your shell before they're passed to su as part of an argument. Net result: the ssh agent is running under the current user, so the other user can't use it.
You need to delay evaluation until the other-user shell. Using single-quotes instead of double would do this, but in the actual command you have $folder, which you clearly want evaluated by your shell (it won't be defined in the other-user shell), so you don't want to delay evaluation of that. The simplest way to do this is to escape the $ that you want to delay evaluation of (your shell will remove the escape, so the other-user shell will see & evaluate it):
su "$username" -c "eval \$(ssh-agent -s); ssh-add $folder/.ssh/id_rsa"
# ^ Note the escape
(BTW, I also added double-quotes around the username, as that's generally-good scripting hygiene. Quoting $folder is more complicated, and shouldn't be necessary as long as it doesn't contain any weird characters, so I skipped it. Also, the { } weren't necessary, and if they were used there needed to be a ; before the }... so I just removed them.)
Another option is to single-quote part of the command and double-quote another part (mixed quoting looks weird, but is perfectly legal in shell syntax):
su "$username" -c 'eval $(ssh-agent -s);'" ssh-add $folder/.ssh/id_rsa"
# ^ single-quoted part ^^ double-quoted part ^
The reason the rest of your attempts didn't work is that the delimiter between the commands (; or &&) wasn't in the quoted string, and hence was treated as a delimiter by your shell, so the second command was run under your user ID rather than as part of the su command.

Running for loop using some other user

I am trying to execute a command using some other user. Here is my code
sudo -i -u someuser bash -c 'for i in 1 2 3; do echo $i; done'
I am expecting output as 1 2 3 but executed with someuser. Above code printing blank lines. I tried to add some other commands
sudo -i -u someuser bash -c 'for i in 1 2 3; do ls; done'
somefile1.txt somefile2.txt
somefile1.txt somefile2.txt
somefile1.txt somefile2.txt
If I try loop with the current user it gives expected output
for i in 1 2 3; do echo $i; done
1
2
3
Looks like bash is unable to resolve variable $i inside for loop. I tried escape character \ but not helping.
TL;DR: Don't use sudo -i with bash -c
The usual way to use sudo -i is without any arguments, in which case it simply starts an interactive login shell.
If you really must have a login shell for some reason (which isn't good practice for running scripts), it's much saner to simply add the extra arguments needed to make your shell a login shell to the bash command itself, and keep sudo out of the business of changing the arguments you pass it:
sudo -u someuser bash -lic 'for i in 1 2 3; do echo "$i"; done'
...or...
sudo -u someuser -i <<'EOF'
for i in 1 2 3; do echo "$i"; done
EOF
The Gory Details
When you use sudo -i with arguments, it rewrites the argument list given to concatenate the arguments together into a single command that can be put into the argument after -c, so you get something like {"sh", "-c", "bash -c ..."}. In concatenating arguments together, sudo uses the logic from parse_args handling for MODE_LOGIN_SHELL, adding an escape character before all characters that are not alphanumeric, _, - or $; keeping $ out of this list was introduced in commitish 6484574f, tagged as a fix for bug #564 (which was introduced by the fix to bug #413 -- personally, I think we would all be better off if bug 413 had been left in place rather than making any attempt to fix it).
See also sh -c does not expand positional parameters if I run it from sudo --login over at Unix & Linux Stack Exchange.
Since this behavior was deliberately put in place in 2013, I doubt there's any fixing it at this point -- any change to sudo's escaping behavior has the potential to modify the security properties of existing scripts.

Run a subshell as root

Consider you have a Linux/UNIX machine with Bash. You have a file secret.txt that only root can read. You want to use a command that takes a string as an argument, say,
sample-command <string>
Log in as a root user and run the command using the first line of the text file:
root ~ $ sample-command $(sed '1!d' secret.txt)
Can this be done by non-root, sudoer users?
Note. sudo sh -c "<command>" doesn't help since subshells don't carry over the root/sudo privilege. For example,
sarah ~ $ sudo sh -c "echo $(whoami)"
gives you sarah, not root.
Expansions like command substitution will be processed by the shell before executing the actual command line:
sudo sh -c "echo $(whoami)"
foouser
Here the shell will first run whoami, as the current user, replace the expansion by it's result and then execute
sudo sh -c "echo foouser"
Expansions doesn't happen within single quotes:
sudo sh -c 'echo "$(whoami)"'
root
In this example $(whoami) won't get processed by calling shell because it appears within single quotes. $(whoami) will therefore get expanded by subshell before calling echo.

Using /bin/bash -l -c with concatenated commands does not pass environment variables

I'm trying to execute a series of bash commands using /bin/bash -l -c as follows:
/bin/bash -l -c "cmd1 && cmd2 && cmd3..."
What I notice is that if cmd1 happens to export an environment variable, it is not seen by cmd2 and so on. If I run the same concatenated commands without the /bin/bash -l -c option, it just runs fine.
For example, this does not print the value of MYVAR:
$/bin/bash -l -c "export MYVAR="myvar" && echo $MYVAR"
whereas, the following works as expected
$export MYVAR="myvar" && echo $MYVAR
myvar
Can't find why using /bin/bash -l -c won't work here? Or are there any suggestion how to make this work with using /bin/bash -l -c?
variables are exported for child processes, parent process environment can't be changed by child process so consecutive commands (forking sub processes can't see variables exported from preceeding command), some commands are particular becuase don't fork new process but are run inside current shell process they are builtin commands only builtin commands can change current process environment export is a builtin, for more information about builtins man bash /^shell builtin.
Otherwise expansion is done during command parsing:
/bin/bash -l -c "export MYVAR="myvar" && echo $MYVAR"
is one command (note the nested quotes are not doing what you expect because are closing and opening quoted string, myvar is not quoted and may be split). So $MYVAR is expanded to current value before assigned to myvar.
export MYVAR="myvar" && echo $MYVAR
are two commands and $MYVAR because && is bash syntax (not literal like "&&"), is expanded after being assigned.
so that $ have a literal meaning (not expanded) use single quotes or escape with backslash.
/bin/bash -l -c 'export MYVAR="myvar" && echo "$MYVAR"'
or
/bin/bash -l -c "export MYVAR=\"myvar\" && echo \"\$MYVAR\""

where did my environment variable go?

I am trying to use an environment variable in a bash script that needs to run as sudo with source.
I have the following file (my_file.sh)
echo "this is DOMAIN = $DOMAIN"
I have the DOMAIN environment variable in my session..
and now I need to run
sudo -E bash -c "source ./my_file.sh"
but the output does not display the value for $DOMAIN. instead it is empty.
if I change the command to be
sudo -E bash -c "echo $DOMAIN"
I see the correct value..
what am I doing wrong?
With the command line:
sudo -E bash -c "source ./my_file.sh"
you are running a script that may refer to environment variables that would need to be exported from a parent shell to be visible.
On the other hand:
sudo -E bash -c "echo $DOMAIN"
expands the value of $DOMAIN in the parent shell, not inside your sudo line.
To demonstrate this, try your "working" solution with single quotes:
sudo -E bash -c 'echo $DOMAIN'
And to make things go, try exporting the variable:
export DOMAIN
sudo -E bash -c "source ./my_file.sh"
Or alternately, pass $DOMAIN on the command line:
sudo -E bash -c "source ./my_file.sh $DOMAIN"
And have your script refer to $1.

Resources