43

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.

Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
BryKKan
  • 2,057
  • 2
  • 14
  • 18
  • 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 Mar 11 '16 at 06: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 Mar 11 '16 at 06: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 Feb 03 '22 at 23:00

11 Answers11

37

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.
Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
Will
  • 2,664
  • 18
  • 26
  • 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 Mar 11 '16 at 05:21
  • @cas, are you sure about that? When I run `declare -f functionname` on a function I have defined in my shell, I see the double quotes output just as they were when I defined them. – Wildcard Mar 11 '16 at 05:27
  • @cas The only issues that would arise are if something in the function actually needs to be quoted. I always quote string arguments out of habit; the quotes around `echo`'s arguments have nothing to do with how or why this works. – Will Mar 11 '16 at 05:33
  • yes, exactly. that's the source of the problem. because you're wrapping the `bash -c` command in double-quotes, you now have double-quotes inside double-quotes. without careful escaping, quotes don't nest, they toggle. – cas Mar 11 '16 at 05:33
  • you misunderstand what i said. the double-quotes don't make this work. they have the potential to cause it to NOT work and it's only by accident that this particular `echo` command works the same with or without double quotes that allows it to work. – cas Mar 11 '16 at 05:34
  • @cas Test it. See my edit. It does not work "by accident". – Will Mar 11 '16 at 05:36
  • try reading what i said and think it through. your example really does only work by accident (i.e. the fact that the double-quotes have no significant effect in this particular `echo` command). In other cases, where the double-quotes have a significant effect, they will break the `bash -c` command and the `sudo`. – cas Mar 11 '16 at 05:38
  • `export -f` seems to handle the quoting well, though. See my edit. Please pastebin me an example where this breaks. – Will Mar 11 '16 at 05:45
  • 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 Mar 11 '16 at 06: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 Mar 11 '16 at 12:07
  • 1
    Note how it changed the single-quotes to `'\''` and double-quotes to single-quotes. BTW, the quotes are being changed by the parent shell, not by the `bash -xc` child shell. `zsh` does pretty much the same and even `dash` does it. `ksh` needs `typeset -f` rather than `declare -f`. i didn't put in too much effort but `ash` doesn't seem to do it (and doesn't have `declare` or `typeset` anyway). – cas Mar 11 '16 at 12:15
  • 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"'` – Gilles 'SO- stop being evil' Mar 11 '16 at 19:57
  • 1
    @BryKKan You can export all functions with `declare -f`. – Gilles 'SO- stop being evil' Mar 11 '16 at 19:57
  • @Gilles - That's a) not documented in bash's man or info pages and b) not what it looks like. `FUNC=$(declare -f hello) ; echo $FUNC` shows no changes to the quotes in the function...and those changes are necessary if $FUNC is going to used inside double-quotes. – cas Mar 11 '16 at 22:54
  • 1
    @cas On the contrary, no changes are necessary. I suggest you experiment a little to see what's going on. – Gilles 'SO- stop being evil' Mar 11 '16 at 23:07
  • 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 Feb 10 '17 at 12:38
  • Additional care requires for passing arguments to functions, especially in conjunction with using aliases. I had to spend some time to figure this out, but it worked out in the end. For example: # Disk usage summary lu_fn() { if [[ $# -lt 1 ]]; then local args='.' else local args=( "$@" ) fi du -hxd1 "${args[@]}" | sort -hr } # shellcheck disable=SC2142 # shellcheck disable=SC2139 alias lu="bash -c '$(declare -f lu_fn); lu_fn \"\$@\"' -" – Dima Korobskiy Dec 16 '19 at 21:32
  • WARNING: the solution for aliasing this like I suggested above. e.g. alias lu="bash -c ' $(declare -f __lu_fn); __lu_fn \"\$@\" ' -" gets killed if the function code uses single quotes. More time has to be spend to find an aliasing solution tolerant to them. – Dima Korobskiy Mar 22 '20 at 21:30
  • I was able to get this to work but needed _another_ exporting for that `bash` to export it to its children: `sudo bash -c "$(declare -f myfunc); typeset -fx myfunc; mycommand"` – Aaron D. Marasco Dec 11 '20 at 16:53
  • I tried this and it worked, with some quirks. I adapted a script for testing spawning functions so the spawned function was run as root. The child process wasn't able to read input data from the keyboard. I couldn't issue a CTRL-C to kill the child process. I had the parent process attempt to kill it, and while this worked for an ordinary spawned child process, it did not work for one invoked by root, even preceding the kill command with sudo, tho I could let the parent process exit and then do a sudo kill on the child process, or from another window. My method below doesn't have these quirks. – Randyman99 Feb 10 '23 at 11:10
8

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
}
SebMa
  • 1,941
  • 4
  • 22
  • 37
  • 3
    This should be the accepted answer, Thank you so much @SebMa It is a brilliant solution!! – AD Progress Feb 06 '22 at 18:40
  • @ADProgress Well, it was about 2 years too late to be the accepted answer, but a wonderful contribution nonetheless. – BryKKan Jun 07 '23 at 10:54
6

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).

cas
  • 1
  • 7
  • 119
  • 185
  • This does not answer the question in any way. He specifically wants to avoid putting it in a script. – Will Mar 11 '16 at 05:37
  • 2
    Also `sudo -E` avoids clearing the environment. – Will Mar 11 '16 at 05: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 Mar 11 '16 at 05: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 Mar 11 '16 at 05: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 Mar 11 '16 at 06:30
  • Getting `sudo` to export functions is a [whole other bag of worms](https://unix.stackexchange.com/questions/549140/why-doesnt-sudo-e-preserve-the-function-environment-variables-exported-by-ex/549329#549329). – Aaron D. Marasco Dec 11 '20 at 16:51
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

hbt
  • 291
  • 3
  • 4
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 "
Rucent88
  • 1,850
  • 4
  • 24
  • 33
2

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
MestreLion
  • 1,358
  • 12
  • 19
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.

voices
  • 1,252
  • 3
  • 15
  • 30
  • 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 `f`is invoked from a menu. – BryKKan Jan 25 '18 at 23:54
1

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 $?
}
mtk
  • 26,802
  • 35
  • 91
  • 130
Mario Palumbo
  • 175
  • 12
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}
    }
}
AdminBee
  • 21,637
  • 21
  • 47
  • 71
Randyman99
  • 111
  • 2
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.

Thomas Dickey
  • 75,040
  • 9
  • 171
  • 268
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.

biocyberman
  • 206
  • 1
  • 5