What Happens When I Sudo Bash -C

What happens when I sudo bash -c?

sudo switches users and then executes bash, passing it the other arguments. bash running as the new user runs the command in the argument after -c.

How can I allow bash -c for sudoers (followed by multiple commands)?

At least with my sudo (OS X 10.9, sudo 1.7.10p7), quote marks in the /etc/sudoers are matched literally. That is, specifying

/bin/bash -c "echo foo && echo bar"

means that the users literally have to run

sudo /bin/bash -c '"echo foo && echo bar"'

i.e. the quote marks have to be passed to the program.

Therefore, all you have to do is just drop the quote marks in /etc/sudoers:

%wheel myhostname =NOPASSWD: /bin/bash -c echo foo && echo bar

While this looks kind of weird, it completely works on my machine: users can execute /bin/bash -c "echo foo && echo bar" without a password. This works because, according to man sudoers, the only characters that must be escaped are ',', ':', '=' and '\'.

Note that this implies that sudo is basically concatenating all the command-line args together with only spaces to determine a match: users can also execute /bin/bash -c "echo foo" "&&" "echo bar". Therefore, you must take care that none of the arguments could individually be a security risk (e.g. that foo, && and bar aren't things that could be used to exploit your computer).

Echo variable using sudo bash -c 'echo $myVariable' - bash script

  1. You're not exporting the variable into the environment so that it can be picked up by subprocesses.

  2. 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"'"'

Running a command with bash -c vs without

cat test.txt in the shell tells it to execute the command cat with the argument test.txt

bash -c "cat text.txt" tells it to execute the command bash with the arguments -c and cat test.txt. The latter obviously ends up repeating what was stated above once the new shell instance has been loaded.

(I believe Bash has some internal optimizations to somewhat simplify what actually happens when you ask it to run a second copy of itself, but I guess that's beside the point here really.)

There are situations where you need to specify a single command for some reason (maybe a hook in another program lets you run a command, but only one) and you need to run more than one command; or when you want to use shell features like redirection or wildcard expansion in a separate shell instance.

sudo bash -c "whoami; test [ -w / ] && echo 'Fun to be privileged!'"
sudo bash -c 'echo 1 >/proc/privileged/something' # though see also "sudo tee"
find . -name '*.txt' -exec bash -c 'mv "$1" "${1/.txt/.csv}"' _ {} \;

The final example uses the Bash-only parameter expansion ${variable/pattern/replacement} to change .txt to .csv; neither find nor mv has any facility for doing this sort of replacement, so calling a shell to do it is easy and convenient (though maybe first check if you have a rename command which would be able to do it in a single simple command like rename '/\.txt$/.csv/' **/*.txt)

Also, if you are running another shell, but want to use Bash for something (there are many Bash-only features which are handy; see also Difference between sh and bash), this is a way to do that; though if you are doing this in a script, obviously then it would usually make more sense to write the entire script in Bash.

In isolation, just running cat test.txt in a subshell is merely wasteful, and costs you an extra process for no actual benefit.

Perhaps tangentially note also that the double quotes cause the calling shell to perform variable substitutions in the quoted string before passing it as an argument to the executable. Single quotes are probably better if you don't specifically require this behavior.

Notice also that

bash -c ./script foo bar baz

is not equivalent to

./script foo bar baz

In fact, the former puts foo in "$0"! The general convention if you want to pass in arguments with bash -c is to put _ or bash in the first argument.

bash -c ./script bash foo bar baz

Another convention to be aware of is that login puts a dash in front when the shell is interactive, so that you can check whether you are running an interactive shell simply by checking whether $0 starts with -.

what alternative methods for sudo echo without bash -c

The problem you are encountering here is that the output redirection >/file.tmp has to happen with elevated privileges. This means that you either have to launch the script as it is with sudo my.sh, or perform the redirection in a way that gives it elevated privileges. It is not enough to run sudo echo because only echo would have elevated privileges, but the output redirection would run as your non-root user because it is a shell builtin feature, not an external command.

There is a standard tool called tee that can help with this. This method is commonly used when running curl to fetch data that needs to be fed to root, such as installing debian apt repository configurations and signing keys.

$ whoami
dho
$ whoami | sudo tee whoami.txt >/dev/null
$ ls -la whoami.txt
-rw-r--r-- 1 root root 4 2022-06-03T09:08:22 whoami.txt

So you can see that I launched whoami with my own user, but the file ended up being written with root permissions because tee, which replaces the output redirection from your original example, was launched as sudo tee.

How can I execute a bash function using sudo?

Starting from the answer of bmargulies, I wrote a function to cover this issue, which basically realizes his idea.

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
# EXESUDO
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
#
# Purpose:
# -------------------------------------------------------------------- #
# Execute a function with sudo
#
# Params:
# -------------------------------------------------------------------- #
# $1: string: name of the function to be executed with sudo
#
# Usage:
# -------------------------------------------------------------------- #
# exesudo "funcname" followed by any param
#
# -------------------------------------------------------------------- #
# Created 01 September 2012 Last Modified 02 September 2012

function exesudo ()
{
### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
#
# LOCAL VARIABLES:
#
### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##

#
# I use underscores to remember it's been passed
local _funcname_="$1"

local params=( "$@" ) ## array containing all params passed here
local tmpfile="/dev/shm/$RANDOM" ## temporary file
local content ## content of the temporary file
local regex ## regular expression


### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##
#
# MAIN CODE:
#
### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##

#
# WORKING ON PARAMS:
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#
# Shift the first param (which is the name of the function)
unset params[0] ## remove first element
# params=( "${params[@]}" ) ## repack array


#
# WORKING ON THE TEMPORARY FILE:
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

content="#!/bin/bash\n\n"

#
# Write the params array
content="${content}params=(\n"

regex="\s+"
for param in "${params[@]}"
do
if [[ "$param" =~ $regex ]]
then
content="${content}\t\"${param}\"\n"
else
content="${content}\t${param}\n"
fi
done

content="$content)\n"
echo -e "$content" > "$tmpfile"

#
# Append the function source
echo "#$( type "$_funcname_" )" >> "$tmpfile"

#
# Append the call to the function
echo -e "\n$_funcname_ \"\${params[@]}\"\n" >> "$tmpfile"


#
# DONE: EXECUTE THE TEMPORARY FILE WITH SUDO
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sudo bash "$tmpfile"
rm "$tmpfile"
}


Example of usage:
running the following snippet

#!/bin/bash

function exesudo ()
{
# copy here the previous exesudo function !!!
}

test_it_out ()
{
local params=( "$@" )
echo "Hello "$( whoami )"!"
echo "You passed the following params:"
printf "%s\n" "${params[@]}" ## print array
}

echo "1. calling without sudo"
test_it_out "first" "second"

echo ""
echo "2. calling with sudo"
exesudo test_it_out -n "john done" -s "done"

exit


Will output

  1. calling without sudo

    Hello yourname!

    You passed the following params:

    first

    second

  2. calling with sudo

    Hello root!

    You passed the following params:

    -n

    john done

    -s

    foo


If you need to use this in a shell calling a function which is defined in your bashrc, as asked with a similar question on serverfault by another user, then you have to put the previous exesudo function on the same bashrc file as well, like the following:

function yourfunc ()
{
echo "Hello "$( whoami )"!"
}
export -f yourfunc

function exesudo ()
{
# copy here
}
export -f exesudo


Then you have to logout and login again or use

source ~/.bashrc


Finally you can use exesudo as follow:

$ yourfunc
Hello yourname!

$ exesudo yourfunc
Hello root!

What shell does sudo use

Short answer: There is no default interpreter. Your script needs a shebang line #!/bin/bash at the top.

Long answer: sudo does not run a shell. It runs the command you pass it directly, no intermediate shell. If you want a shell you need to explicitly pass it one.

sudo echo $0

This command is misleading due to $0 being expanded by your shell before sudo is called. It's printing your current $0, not the value of $0 inside the sudo'd environment.

Let's compare some commands:

  • sudo ls

    This executes the /bin/ls executable directly. There's no shell involved. The following commands will show when the distinction comes into play.

  • sudo ls /root/*

    The current shell expands the glob. If you can't access /root/ then the glob isn't expanded and you get ls: cannot access /root/*: No such file or directory.

  • sudo 'ls /root/*'

    This attempts to execute a program called ls /root/*. Literally, it's named * in the root sub-directory of ls  (ls with a trailing space). It fails with sudo: ls /root/*: command not found.

  • sudo ls '/root/*'

    This runs /bin/ls and passes it the literal string /root/*. There's no glob expansion. It fails with ls: cannot access /root/*: No such file or directory.

  • sudo bash -c 'ls /root/*'

    This executes /bin/bash and has it interpolate the ls /root/* command-line, expanding the glob. This one, finally, works, because we have an explicit shell to expand the glob, and we're doing the expansion within sudo instead of outside of it.

call a bash function as root but gives 'unexpected end of file'

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.

execute shell command with sudo in c program

you can execute the command with pipe

FILE *fp;
fp=popen(command_result,"r");

and then you can read the command output from the pipe fp like you read from files with fgets() or fread()...

BTW you can not execute sudo command if the password is required in the sudo command

Using sudo with for loop

sudo wants a program (+arguments) as a parameter, not a piece of shell script. You can do this, though:

sudo -i -u user sh -c 'for i in /dir; do echo $i; done'

Note the single quotes. If you used double quotes, your shell would try to expand the $i before sudo (or, rather, the shell run by it) ever sees it.

PS. a separate problem, as pointed out in a comment (only six years later), is that if you want to iterate over the files in a directory, the proper syntax is for i in /dir/*. for accepts a list, and /dir is a list... with one item. /dir/* expands to a list of files in /dir due to wildcard expansion.



Related Topics



Leave a reply



Submit