Why does the last example throw an error but the others work? Bash is invoked in any case.
#!/bin/bash
function hello {
echo "Hello! user=$USER, uid=$UID, home=$HOME";
}
# Test that it works.
hello
# ok
bash -c "$(declare -f hello); hello"
# ok
sudo su $USER bash -c "$(declare -f hello); hello"
# error: bash: -c: line 1: syntax error: unexpected end of file
sudo -i -u $USER bash -c "$(declare -f hello); hello"
It fail because of the -i or --login switch:
It seems like when debugging with -x
$ set -x
$ sudo -i -u $USER bash -c "$(declare -f hello); hello"
++ declare -f hello
+ sudo -i -u lea bash -c 'hello ()
{
echo "Hello! user=$USER, uid=$UID, home=$HOME"
}; hello'
Now if doing it manually it cause the same error:
sudo -i -u lea bash -c 'hello ()
{
echo "Hello! user=$USER, uid=$UID, home=$HOME"
}; hello'
Now lets just do a simple tiny change that make it work:
sudo -i -u lea bash -c 'hello ()
{
echo "Hello! user=$USER, uid=$UID, home=$HOME";}; hello'
The reason is that sudo -i runs everything like an interactive shell. And when doing so, every newline character from the declare -f hello is internally turned into space. The curly-brace code block need a semi-colon before the closing curly-brace when on the same line, which declare -f funcname does not provide since it expands the function source with closing curly brace at a new line.
Now lets make this behaviour very straightforward:
$ sudo bash -c 'echo hello
echo world'
hello
world
It executes both echo statements because they are separated by a newline.
but:
$ sudo -i bash -c 'echo hello
echo world'
hello echo world
It executes the first echo statement that prints everything as arguments because the newline has been replaced by a space.
It is the same code in all examples, so it should be ok.
Yes, "$(declare -f hello); hello" is always the same string. But it is processed differently by sudo su and sudo -i as found out by Lea Gris .
sudo -i quotes its arguments before passing them to bash. This quoting process seems to be undocumented and very poor. To see what was actually executed, you can print the argument passed to bash -c inside your ~/.bash_profile/:
Content of ~/.bash_profile
cat <<EOF
# is executed as
$BASH_EXECUTION_STRING
# resulting in output
EOF
Some examples of sudo -i's terrible and inconsistent quoting
Linebreaks are replaced by line continuations
$ sudo -u $USER -i echo '1
2'
# is executed as
echo 1\
2
# resulting in output
12
Quotes are escaped as literals
$ sudo -u $USER -i echo \'single\' \"double\"
# is executed as
echo \'single\' \"double\"
# resulting in output
'single' "double"
But $ is not quoted
$ sudo -u $USER -i echo \$var
# is executed as
echo $var
# resulting in output
Some side notes:
There might be a misunderstanding in your usage of su.
sudo su $USER bash -c "some command"
does not execute bash -c "echo 1; echo 2". The -c ... is interpreted by su and passed as -c ... to $USER's default shell. Afterwards, the remaining arguments are passed to that shell as well. The executed command is
defaultShellOfUSER -c "some command" bash
You probably wanted to write
sudo su -s bash -c "some command" "$USER"
Interactive shells behave differently
su just executes the command specified by -c. But sudo -i starts a login shell, in your case that login shell seems to be bash (this is not necessarily the case, see section above).
An interactive bash session behaves different from bash -c "..." or bash script.sh. An interactive bash sources files like .profile, .bash_profile, and enables history expansion, aliases, and so on. For a full list see the section Interactive Shell Behavior in bash's manual.
Related
I think the two commands below should be identical, but given the heredoc, the shell produces an error. Is it possible to pass a heredoc to the -c argument of sh?
heredoc example
/bin/sh -c <<EOF
echo 'hello'
EOF
# ERROR: /bin/sh: -c: option requires an argument
simple string example
/bin/sh -c "echo 'hello'"
# prints hello
The commands are not equivalent.
/bin/sh -c <<EOF
echo 'hello'
EOF
is equivalent to
echo "echo 'hello'" | /bin/sh -c
or, with here-string:
/bin/sh -c <<< "echo 'hello'"
but sh -c requires an argument. It would work with
echo "echo 'hello'" | /bin/sh
I tried to post this as a comment but formatting code doesn't work well in comments. Using the accepted answer by #Benjamin W. as a guide, I was able to get it to work with this snippet
( cat <<EOF
echo 'hello'
EOF
) | /bin/sh
The magic is in how cat handles inputs. From the man page:
If file is a single dash (`-') or absent, cat reads from the standard input.
So cat can redirect stdin to stdout and that can be piped to /bin/sh
I want to echo a string into the /etc/hosts file. The string is stored in a variable called $myString.
When I run the following code the echo is empty:
finalString="Hello\nWorld"
sudo bash -c 'echo -e "$finalString"'
What am I doing wrong?
You're not exporting the variable into the environment so that it can be picked up by subprocesses.
You haven't told sudo to preserve the environment.
\
finalString="Hello\nWorld"
export finalString
sudo -E bash -c 'echo -e "$finalString"'
Alternatively, you can have the current shell substitute instead:
finalString="Hello\nWorld"
sudo bash -c 'echo -e "'"$finalString"'"'
You can do this:
bash -c "echo -e '$finalString'"
i.e using double quote to pass argument to the subshell, thus the variable ($finalString) is expanded (by the current shell) as expected.
Though I would recommend not using the -e flag with echo. Instead you can just do:
finalString="Hello
World"
What is a procedure to decorate an arbitrary bash command to execute it in a subshell? I cannot change the command, I have to decorate it on the outside.
the best I can think of is
>bash -c '<command>'
works on these:
>bash -c 'echo'
>bash -c 'echo foobar'
>bash -c 'echo \"'
but what about the commands such as
echo \'
and especially
echo \'\"
The decoration has to be always the same for all commands. It has to always work.
You say "subshell" - you can get one of those by just putting parentheses around the command:
x=outer
(x=inner; echo "x=$x"; exit)
echo "x=$x"
produces this:
x=inner
x=outer
You could (ab)use heredocs:
bash -c "$(cat <<-EOF
echo \'\"
EOF
)"
This is one way without using -c option:
bash <<EOF
echo \'\"
EOF
What you want to do is exactly the same as escapeshellcmd() in PHP (http://php.net/manual/fr/function.escapeshellcmd.php)
You just need to escape #&;`|*?~<>^()[]{}$\, \x0A and \xFF. ' and " are escaped only if they are not paired.
But beware of security issues...
Let bash take care of it this way:
1) prepare the command as an array:
astrCmd=(echo \'\");
2) export the array as a simple string:
export EXPORTEDastrCmd="`declare -p astrCmd| sed -r "s,[^=]*='(.*)',\1,"`";
3) restore the array and run it as a full command:
bash -c "declare -a astrCmd='$EXPORTEDastrCmd';\${astrCmd[#]}"
Create a function to make these steps more easy like:
FUNCbash(){
astrCmd=("$#");
export EXPORTEDastrCmd="`declare -p astrCmd| sed -r "s,[^=]*='(.*)',\1,"`";
bash -c "declare -a astrCmd='$EXPORTEDastrCmd';\${astrCmd[#]}";
}
FUNCbash echo \'\"
I'd like to see both commands print hello
$ bash -l -c "/bin/echo hello"
hello
$ ssh example_host bash -l -c /bin/echo hello
$
How can hello be passed as a parameter in the ssh command?
The bash -l -c is needed, so login shell startup scripts are executed.
Getting ssh to start a login shell would solve the problem too.
When you pass extra args after -c, they're put into the argv of the shell while that command is executing. You can see that like so:
bash -l -c '/bin/echo "$0" "$#"' hello world
...so, those arguments aren't put on the command line of echo (unless you go out of your way to make it so), but instead are put on the command line of the shell which you're telling to run echo with no arguments.
That is to say: When you run
bash -l -c /bin/echo hello
...that's the equivalent of this:
(exec -a hello bash -c /bin/echo)
...which puts hello into $0 of a bash which runs only /bin/echo. Since running /bin/echo doesn't look at $0, of course it's not going to print hello.
Now, because executing things via ssh means you're going through two steps of shell expansion, it adds some extra complexity. Fortunately, you can have the shell handle that for you automatically, like so:
printf -v cmd_str '%q ' bash -l -c '/bin/echo "$0" "$#"' hello world
ssh remote_host "$cmd_str"
This tells bash (printf %q is a bash extension, not available in POSIX printf) to quote your command such that it expands to itself when processed by a shell, then feeds the result into ssh.
All that said -- treating $0 as a regular parameter is bad practice, and generally shouldn't be done absent a specific and compelling reason. The Right Thing is more like the following:
printf -v cmd '%q ' /bin/echo hello world # your command
printf -v cmd '%q ' bash -l -c "$cmd" # your command, in a login shell
ssh remotehost "$cmd" # your command, in a login shell, in ssh
I have a shell script test.sh as give below.
sudo -H sh -c '
echo $1;
'
But while I am running this script as ./test.sh abcd it is not echoing anything. Then I changed my script as given below.
sudo -H sh -c '
echo \$1;
'
But now, it is displaying output as $1. What modification shall I need to do here for getting output as abcd. Please advice as I am a very beginner in shell scripting.
Thanks.
Try this:
sudo -H sh -c "
echo $1;
"
sudo runs the command in a new shell that doesn't know anything about the argument(s) passed to its parent, so you need to expand them before running the command string in the new (sub)shell.
Try:
sudo -H sh -c '
echo "$1";
' argv0 "$1"
From the bash man page:
man bash | less -Ip '-c string'
# -c string If the -c option is present, then commands are read from
# string. If there are arguments after the string, they are
# assigned to the positional parameters, starting with $0.