47

I have a script that does a number of different things, most of which do not require any special privileges. However, one specific section, which I have contained within a function, needs root privileges.

I don't wish to require the entire script to run as root, and I want to be able to call this function, with root privileges, from within the script. Prompting for a password if necessary isn't an issue since it is mostly interactive anyway. However, when I try to use sudo functionx, I get:

sudo: functionx: command not found

As I expected, export didn't make a difference. I'd like to be able to execute the function directly in the script rather than breaking it out and executing it as a separate script for a number of reasons.

Is there some way I can make my function "visible" to sudo without extracting it, finding the appropriate directory, and then executing it as a stand-alone script?

The function is about a page long itself and contains multiple strings, some double-quoted and some single-quoted. It is also dependent upon a menu function defined elsewhere in the main script.

I would only expect someone with sudo ANY to be able to run the function, as one of the things it does is change passwords.

3
  • The fact that there are several functions involved makes it even more complicated and prone to failure. You now have to find all such dependencies (and all their dependencies too, if any...to however many levels deep) including any other functions that the menu function might call and declare them too.
    – cas
    Commented Mar 11, 2016 at 6:06
  • Agreed, and I may have to just bite the bullet and break it up (and do my best to accurately determine the path it was run from, plus hope the end user keeps the files together) if there are no better alternatives.
    – BryKKan
    Commented Mar 11, 2016 at 6:33
  • While calling a shell function looks syntactically the same as executing an external command (process), it makes a big difference for sudo: sudo can't execute a shell function (other than sudo sh -c "shell_function" when sh knows shell_function by some means).
    – U. Windl
    Commented Feb 3, 2022 at 23:00

13 Answers 13

43

I will admit that there's no simple, intuitive way to do this, and this is a bit hackey. But, you can do it like this:

function hello()
{
    echo "Hello!"
}

# Test that it works.
hello

FUNC=$(declare -f hello)
sudo bash -c "$FUNC; hello"

Or more simply:

sudo bash -c "$(declare -f hello); hello"

It works for me:

$ bash --version
GNU bash, version 4.3.42(1)-release (x86_64-apple-darwin14.5.0)
$ hello
Hello!
$
$ FUNC=$(declare -f hello)
$ sudo bash -c "$FUNC; hello"
Hello!

Basically, declare -f will return the contents of the function, which you then pass to bash -c inline.

If you want to export all functions from the outer instance of bash, change FUNC=$(declare -f hello) to FUNC=$(declare -f).

Edit

To address the comments about quoting, see this example:

$ hello()
> {
> echo "This 'is a' test."
> }
$ declare -f hello
hello ()
{
    echo "This 'is a' test."
}
$ FUNC=$(declare -f hello)
$ sudo bash -c "$FUNC; hello"
Password:
This 'is a' test.
20
  • 3
    This only works by accident, because echo "Hello!" is effectively the same as echo Hello! (i.e. double-quotes make no difference for this particular echo command). In many/most other circumstances, the double-quotes in the function are likely to break the bash -c command.
    – cas
    Commented Mar 11, 2016 at 5:21
  • 1
    This does answer the original question, so if I don't get a better solution I'll accept it. However, it does break my particular function (see my edit) since it's dependent on functions defined elsewhere in the script.
    – BryKKan
    Commented Mar 11, 2016 at 6:20
  • 1
    i did some testing earlier this afternoon (using bash -xc rather than just bash -c) and it looks like bash is smart enough to re-quote things in this situation, even to the extent of replacing double-quotes with single quotes and changing ' to '\'' if necessary. I'm sure there will be some cases it can't handle, but it definitely works for at least simple and moderately complex cases - e.g. try function hello() { filename="this is a 'filename' with single quotes and spaces" ; echo "$filename" ; } ; FUNC=$(declare -f hello) ; bash -xc "$FUNC ; hello"
    – cas
    Commented Mar 11, 2016 at 12:07
  • 5
    @cas declare -f prints out the function definition in a way that can be re-parsed by bash, so bash -c "$(declare -f)" does work correctly (assuming that the outer shell is also bash). The example you posted shows it working correctly — where the quotes were changed is in the trace, because bash prints out traces in shell syntax, e.g. try bash -xc 'echo "hello world"' Commented Mar 11, 2016 at 19:57
  • 3
    Excellent answer. I implemented your solution - I would note that you can import the script itself from within the script, provided you nest it within an conditional which checks if against sudo yourFunction being found (otherwise you get a segmentation error from the recursion)
    – GrayedFox
    Commented Feb 10, 2017 at 12:38
13

I've written my own Sudo bash function to do that, it works to call functions and aliases :

function Sudo {
        local firstArg=$1
        if [ $(type -t $firstArg) = function ]
        then
                shift && command sudo bash -c "$(declare -f $firstArg);$firstArg $*"
        elif [ $(type -t $firstArg) = alias ]
        then
                alias sudo='\sudo '
                eval "sudo $@"
        else
                command sudo "$@"
        fi
}
3
  • 5
    This should be the accepted answer, Thank you so much @SebMa It is a brilliant solution!! Commented Feb 6, 2022 at 18:40
  • @ADProgress Well, it was about 2 years too late to be the accepted answer, but a wonderful contribution nonetheless.
    – BryKKan
    Commented Jun 7, 2023 at 10:54
  • 1
    Here an Equiv function for macos where -t option is not supported in type command. unix.stackexchange.com/a/756361/318478 based on @SebMa function Commented Sep 12, 2023 at 22:08
7

The "problem" is that sudo clears the environment (except for a handful of allowed variables) and sets some variables to pre-defined safe values in order to protect against security risks. in other words, this is not actually a problem. It's a feature.

For example, if you set PATH="/path/to/myevildirectory:$PATH" and sudo didn't set PATH to a pre-defined value then any script that didn't specify the full pathname to ALL commands it runs (i.e. most scripts) would look in /path/to/myevildirectory before any other directory. Put commands like ls or grep or other common tools in there and you can easily do whatever you like on the system.

The easiest / best way is to re-write the function as a script and save it somewhere in the path (or specify the full path to the script on the sudo command line - which you'll need to do anyway unless sudo is configured to allow you to run ANY command as root), and make it executable with chmod +x /path/to/scriptname.sh

Rewriting a shell function as a script is as simple as just saving the commands inside the function definition to a file (without the function ..., { and } lines).

6
  • This does not answer the question in any way. He specifically wants to avoid putting it in a script.
    – Will
    Commented Mar 11, 2016 at 5:37
  • 2
    Also sudo -E avoids clearing the environment.
    – Will
    Commented Mar 11, 2016 at 5:37
  • I understand to some extent why it is happening. I was hoping there was some means to temporarily override this behavior. Somewhere else a -E option was mentioned, though that didn't work in this case. Unfortunately, while I appreciate the explanation of how to make it a standalone script, that specifically doesn't answer the question, because I wanted a means to avoid that. I have no control over where the end user places the script and I'd like to avoid both hard-coded directories and the song and dance of trying to accurately determine where the main script was run from.
    – BryKKan
    Commented Mar 11, 2016 at 5:41
  • 1
    it doesn't matter whether that's what the OP asked for or not. If what he wants either won't work or can only be made to work by doing something extremely insecure then they need to be told that and provided with an alternative - even if the alternative is something they explicitly stated they don't want (because sometimes that's the only or the best way to do it safely). It would be irresponsible to tell someone how to shoot themselves in the foot without giving them warning about the likely consequences of pointing a gun at their feet and pulling the trigger.
    – cas
    Commented Mar 11, 2016 at 5:43
  • 1
    @cas That is true. It can't be done securely is an acceptable answer in some circumstances. See my last edit though. I'd be curious to know if your opinion on the security implications is the same given that.
    – BryKKan
    Commented Mar 11, 2016 at 6:30
3

My take on this, built upon other answers, but as far as I can see the only one properly handling function arguments and quoting:

sudo-function() {
    (($#)) || { echo "Usage: sudo-function FUNC [ARGS...]" >&2; return 1; }
    sudo bash -c "$(declare -f "$1");$(printf ' %q' "$@")"
}
$ args() { local i=0; while (($#)); do echo "$((++i))=$1"; shift; done; }
$ sudo-function args a 'b c' "d 'e'" 'f "g"'
1=a
2=b c
3=d 'e'
4=f "g"

And expanding it to also run on aliases, builtins, and executables in user's but not root's $PATH:

super-sudo() {
    (($#)) || { echo "Usage: super-sudo CMD [ARGS...]" >&2; return 1; }
    local def ftype; ftype=$(type -t $1) ||
    { echo "not found: $1" >&2; return 1; }
    if [[ "$ftype" == "function" ]]; then def=$(declare -f "$1")
    else def=$(declare -p PATH); fi  # file or builtin
    sudo bash -c "${def};$(printf ' %q' "$@")"
}
alias super-sudo='super-sudo '  # so it runs aliases too

As most (all?) answers, it has a few limitations:

  • Does not work if FUNC calls other functions
  • As sudo, it might not work as expected if mixed with redirections and process substitutions < >>, <(), etc.

And a small bonus: bash-completion!

complete -A function sudo-function
complete -c super-sudo
2

You can combine functions and aliases

Example:

function hello_fn() {
    echo "Hello!" 
}

alias hello='bash -c "$(declare -f hello_fn); hello_fn"' 
alias sudo='sudo '

then sudo hello works

2

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 "
3
  • +1 for a great approach on running aliases, but notice this will not properly quote arguments to functions
    – MestreLion
    Commented Jun 4, 2022 at 19:56
  • @MestreLion Is there a way we can make it better?
    – Rucent88
    Commented Jun 27, 2022 at 7:37
  • Using printf ' %q' "$@", see my answer
    – MestreLion
    Commented Jun 27, 2022 at 7:47
2

Entirely transparent, it has the same behavior as the real sudo command, apart from adding an additional parameter:

enter image description here

It is 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

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 $?
}
1

Here's a variation on Will's answer. It involves an additional cat process, but offers the comfort of heredoc. In a nutshell it goes like this:

f () 
{
    echo ok;
}

cat <<EOS | sudo bash
$(declare -f f)
f
EOS

If you want more food for thought, try this:

#!/bin/bash

f () 
{ 
    x="a b"; 
    menu "$x"; 
    y="difficult thing"; 
    echo "a $y to parse"; 
}

menu () 
{
    [ "$1" == "a b" ] && 
    echo "here's the menu"; 
}

cat <<EOS | sudo bash
$(declare -f f)
$(declare -f menu)
f
EOS

The output is:

here's the menu
a difficult thing to pass

Here we've got the menu function corresponding with the one in the question, which is "defined elsewhere in the main script". If the "elsewhere" means its definition has already been read at this stage when the function demanding sudo is being executed, then the situation is analogous. But it might not have been read yet. There may be another function that will yet trigger its definition. In this case declare -f menu has to be replaced with something more sophisticated, or the whole script corrected in a way that the menu function is already declared.

1
  • Very interesting. I'll have to try it out at some point. And yes, the menu function would have been declared before this point, as fis invoked from a menu.
    – BryKKan
    Commented Jan 25, 2018 at 23:54
1

I have a program which needs to be run as root for part of it and not for the other part. I have a function inside it which tests the environment if root is the user. If not, it invokes sudo and recalls the script with all input parameters.

This keeps the first instance of the script (with the original user) on hold while it runs a second instance with root as user. The next time it comes into this function it runs the functions that need to be run as root. You could use the if statement to run that part of the script which you don't want to have root access. When it exits the 2nd instance of the program as root, it comes back to this point in the 1st instance, and complete the script and exits.

The advantage of this method is that all variables are redefined again in the 2nd root instance (Those which need to be preserved can be passed as input parameters). The variables in the 2nd instance in root space are entirely separate and not visible to the 1st instance in user space, and vice versa.

The only hiccup is in the 2nd instance the original user is now defined by the environmental variable ${SUDO_USER}, rather than ${USER}, and the user's home directory needs to be constructed rather than using ${HOME}, which now points to /root, but that's all easy enough to get past.

I don't know if this is the best way to do it, but it works for me, and I've been using it for years.

{

    function run_as_root {
        local function_name="${FUNCNAME[0]}";
        local return_value=0;
                
        # notify entry to function
        
        Echo-enter ${function_name}; 

        # make sure we're running as root. Recall program as root if not.

        if (( $EUID != 0 )); then
        
            echo "Will now run as root...."; # - ${gc_sl_file}"

            play_norm_sound;

            # need to preserve HOME environmental variable as it's used for  
            # directory referencing, per change in Ubu 20.04

            sudo --preserve-env=HOME -u root ${@}; 
            return_value=$?;
            
            if [ ${g_debug_mode} = true ]; then 
                echo "Returned from sudo call to self with return value: "  \
                        "${return_value};" >> ${gc_logfile};
                
            fi  

            # might not need this
            # ensure return value is nonzero so we don't run program operations again
            
            if [[ ${return_value} -ne 0 ]]; then 

                echo "We had a nonzero return value from root:${return_value}." | tee -a ${gc_logfile};
                play_err_sound;
            fi
            
        else    # we are already in root
                
            # shift input parameters to get rid of script call as first parameter
            
            shift
            
            # run rest of program. 
            ##### NOTE: This should be standard for all programs that use this function.

            main ${@};
            return_value=$?;
            
        fi
        
        Echo-return "${function_name}" "${return_value}";
        
        return ${return_value}
    }
}
1

The equiv of @SebMa answer. For macos no -t option for type command.

function sudoRun {
    local firstArg=$1
    if [[ $(type $firstArg) == *function* ]]; then

        shift && command sudo bash -c "$(declare -f $firstArg);$firstArg $*"
    elif [[ $(type $firstArg) = *alias* ]]; then
        alias sudo='\sudo '
        eval "sudo $@"
    else
        command sudo "$@"
    fi
}
0

Assuming that your script is either (a) self-contained or (b) can source its components based on its location (rather than remembering where your home directory is), you could do something like this:

  • use the $0 pathname for the script, using that in the sudo command, and pass along an option which the script will check, to call the password updating. As long as you rely on finding the script in the path (rather than just run ./myscript), you should get an absolute pathname in $0.
  • since the sudo runs the script, it has access to the functions it needs in the script.
  • at the top of the script (rather, past the function declarations), the script would check its uid and realize that it was run as the root user, and seeing that it has the option set to tell it to update the password, go and do that.

Scripts can recur on themselves for various reasons: changing privileges is one of those.

0

My solution is to throw sudo as the last argument of the function or use the empty one:

f () {
local arg1=$1
local arg2=$2
local mysudo="$3" # can be empty if not given
$mysudo mycommand $arg1 $arg2
}

Function call:

f 1 2 # without sudo
f 1 2 sudo # with sudo

That is quite straightforward.

0

Just for the sake of showing more possibilities

Creating sudopass script command

  • We can create a script command sudopass in /usr/local/bin/sudopass
  • source our aliases and functions scripts, in my case it's in ~/.shellrc a shared script between bash and other shells like zsh
#!/bin/bash

source ~/.shellrc

# Check if a command was provided as an argument
if [ $# -eq 0 ]; then
  echo "Usage: $0 <alias or function> [arguments]"
  exit 1
fi

eval "$@"
  • Set as executable
sudo chmod +x /usr/local/bin/sudopass

usage

sudo sudopass update_sudo_timeout 70

enter image description here

  • execute perfectly
  • The same function (this one is a function) that wasn't running. Now it does run with sudo just pretty well

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .