25

I tried exporting the function and then executing it with bash, but that doesn't work:

$ export -f my_func
$ sudo bash -c 'my_func' 
bash: my_func: command not found

If I try to run the function with bash without sudo (bash -c 'my_func'), it works.

Any idea?

user1729210
  • 579
  • 1
  • 8
  • 30
Miroslav
  • 528
  • 1
  • 6
  • 11

10 Answers10

18

Each time you run sudo, it forks and execs a new copy of the shell, running as root. That shell does not inherit functions from your shell (it can't) and it doesn't inherit functions from previous executions. You will have to write out a file containing the function definition and invocation and sudo the invocation of that.

bmargulies
  • 97,814
  • 39
  • 186
  • 310
  • 2
    Not only it cannot, it probably should never do that due to security reasons. –  Feb 25 '12 at 23:14
  • Runnig bash alone will exec new copy of bash as well, yet it is still able to inherit functions from the parent shell. Also, sudo is somehow able to preserve exported bash variables, so I thought there could be some trick to do the same with functions. I understand there could be some security reasons for this, but what if I'm using sudo to run function under more restricted user? – Miroslav Feb 26 '12 at 01:27
  • Variables travel through the process environment mechanism. There's no such thing for functions. – bmargulies Feb 26 '12 at 01:56
16

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!
Community
  • 1
  • 1
Luca Borrione
  • 16,324
  • 8
  • 52
  • 66
  • 2
    This is quite a piece of work! Great job. BTW, only thing I modified is the sudo invocation part. I added exit code propagation, so I can have nifty little functions under test harness. This is my modification: From this: $ sudo bash "$tmpfile" $ rm "$tmpfile" to this: $ sudo bash "$tmpfile" $ EXIT_CODE=$? $ rm "$tmpfile" $ return $EXIT_CODE – LavaScornedOven Oct 16 '13 at 15:13
  • 1
    Can't you just generate the tempfile with `{ echo '#!/bin/bash'; declare -f "$1" | head -n -1 | tail -n +3); } > "$tmpfile"`, and instead of packing the arguments into the script, pass them as part of the sudo? It would shrink the body to 5 lines – jbo5112 May 18 '17 at 18:31
11

You can do that using declare -f, as in the following example:

function myfunc() {
    whoami
    echo First parameter is $1
}

myfunc foo
DECL=`declare -f myfunc`

sudo bash -c "$DECL; myfunc bar"
Karatheodory
  • 895
  • 10
  • 16
5

An alternative to calling your function with sudo is to just move the "sudo " calls inside your function. For example, I was wanting to set up a shortcut function in OS X for forwarding localhost to a particular port.

function portforward() {
    echo "y" | sudo ipfw flush;
    sudo ipfw add 100 fwd 127.0.0.1,$1 tcp from any to any 80 in;
    echo "Forwarding localhost to port $1!";
}

The function hits sudo and asks for my password. (Then pipes "y" to a prompt for ipfw, not related to this question). After that, sudo is cached so the rest of the function executes without the need to enter a password.

Essentially, this just runs like:

portforward 8000
Password:
Forwarding localhost to port 8000!

And fills my need because I only need to enter the password once and it's taken care of. Although it's a little ugly if you fail to enter the password the first time. Extra points for detecting whether the first sudo succeeded and exiting the function if not.

timothyashaw
  • 1,309
  • 13
  • 7
1

For short and simple stuff that doesn't have single quotes in it, this works for me:

export code='
function whoAmI() {
    echo `whoami`
}

whoAmI
'
sudo bash -c "$code"

# output: root
Shavais
  • 2,476
  • 1
  • 27
  • 25
0

All you have to do is check if you are root, if yes, run the function, if not, call the script with sudo:

#!/bin/bash
# script name: 'foo.sh'

function foo(){
    whoami
}

DIR=$( cd "$( dirname "$0" )" && pwd )   # get script dir
if [ "$UID" -ne 0 ]; then                # check if you are root
    sudo $DIR/foo.sh                     # NOT: run script with sudo
else
    foo                                  # YES: run function
fi
gospes
  • 3,819
  • 1
  • 28
  • 31
0

It took me a few months to create this feature. Entirely transparent, it has the same behavior as the real sudo command, apart from adding an additional parameter:

enter image description here

It was necessary to create the following function first:

cmdnames () {
    { printf '%s' "$PATH" | xargs -d: -I{} -- find -L {} -maxdepth 1 -executable -type f -printf '%P\n' 2>/dev/null; compgen -b; } | sort -b | uniq
    return 0
}

The function, completely created by me, is the following:

sudo () {
    local flagc=0
    local flagf=0
    local i
    if [[ $# -eq 1 && ( $1 == "-h" || ( --help == $1* && ${#1} -ge 4 ) ) ]]; then
        command sudo "$@" | perl -lpe '$_ .= "\n  -c, --run-command             run command instead of the function if the names match" if /^  -C, / && ++$i == 1'
        return ${PIPESTATUS[0]}
    fi
    for (( i=1; i<=$#; i++ )); do
        if [[ ${!i} == -- ]]; then
            i=$((i+1))
            if [[ $i -gt $# ]]; then break; fi
        else
            if [[ ${!i} == --r ]]; then
                command sudo "$@" 2>&1 | perl -lpe '$_ .= " '"'"'--run-command'"'"'" if /^sudo: option '"'"'--r'"'"' is ambiguous/ && ++$i == 1'
                return ${PIPESTATUS[0]}
            fi
            if [[ ${!i} == -c || ( --run-command == ${!i}* && $(expr length "${!i}") -ge 4 ) ]]; then
                flagf=-1
                command set -- "${@:1:i-1}" "${@:i+1}"
                i=$((i-1))
                continue
            fi
            command sudo 2>&1 | grep -E -- "\[${!i} [A-Za-z]+\]" > /dev/null && { i=$((i+1)); continue; }
        fi
        cmdnames | grep "^${!i}$" > /dev/null && flagc=1
        if [[ ! ( flagc -eq 1 && flagf -eq -1 ) ]]; then
            if declare -f -- "${!i}" &> /dev/null; then flagf=1; fi
        fi
        break
    done
    if [[ $flagf -eq 1 ]]; then
        command sudo "${@:1:i-1}" bash -sc "shopt -s extglob; $(declare -f); $(printf "%q " "${@:i}")"
    else
        command sudo "$@"
    fi
    return $?
}

Disadvantages: none. Latest update: 03/02/2022.

Mario Palumbo
  • 693
  • 8
  • 32
0

New Answer. Add this to your ~/.bashrc to run functions. As a bonus, it can run aliases too.

ssudo () # super sudo
{
  [[ "$(type -t $1)" == "function" ]] &&
    ARGS="$@" && sudo bash -c "$(declare -f $1); $ARGS"
}
alias ssudo="ssudo "
Rucent88
  • 810
  • 1
  • 6
  • 12
0

Improved version of @Rucent88 solution https://stackoverflow.com/a/68859346/14167912

Append it to ~/.bashrc same way. This allows normal usage of aliased sudo for all cases(as far as I know) - with keys like -i, to call functions, to call aliases and normal commands. I included 'command' and 'builtin' keywords handling because I use them alot in my other aliases. For example, you can run normal sudo using: command sudo.

execsudo()
{
    if [[ "$1" = "-"* ]]; then
    sudo "$@"
    elif [ "$(type -t $1)" = "function" ]; then
    local ARGS="$@"; sudo bash -c "$(declare -f $1); $ARGS"
    elif [ "$1" = "command" ] || [ "$1" = "builtin" ]; then
    shift; sudo bash -i <<<"$@"
    else
    sudo bash -i <<<"$@"
    fi
}
alias sudo="execsudo "
CHERNOMOR
  • 1
  • 2
-1

If your function is in your .bashrc

Then just do sudo -i myfunc

Josh
  • 101
  • 7