mora mora - 22 days ago 9
Bash Question

How can I export function in a function in bash?

I am learning bash. And I would like to make a function which wrap another function in a temporal script file and execute it with sudo -u command in sub-shell.
The problem I encountered is the generated script cannot find the wrapped function although it is exported in the wrap function.
I append test cords below. Someone who finds problems, please let me know. Thank you very much.

main.sh

source "./display.sh"
source "./sudo_wrap.sh"

display_func "load success"

sudo_wrap_func '' 'display_func' '3' '4 5'


output, display.sh, sudo_wrap.sh and generated temporal file are appended below,

output

display_func : load success
export -f display_func
30481: line 5: display_func: command not found


display.sh

function display_func() {
echo "display_func : $@"
}


sudo_wrap.sh

function sudo_wrap_func() {

local sudo_user="${1:-root}"
local function_name="${2:?'function_name is null string.'}"
shift 2
local func_augs=( "$@" )

local script

# *** script : header ***
script="#!/bin/bash\n"
script="${script}\n"

# *** script : making augments for function ***
script="${script}augs=("

for aug in "${func_augs[@]}"
do
if [[ "${aug}" =~ [[:blank:]] ]]; then
script=" ${script} \"${aug}\""
else
script=" ${script} ${aug}"
fi
done
script="${script})\n"

local tmp_script_file="${RANDOM}"

echo -e "${script}" >> "${tmp_script_file}"

# *** script : calling function with augments ***

echo -e "${function_name} \"\${augs[@]}\"\n" >> "${tmp_script_file}"

echo "export -f "${function_name}"" >&2
export -f "${function_name}"

sudo -u"${sudo_user}" bash "${tmp_script_file}"
rm "${tmp_script_file}"

}


temporally generated file (in this case, file name is 30481)

#!/bin/bash

augs=( 3 "4 5")

display_func "${augs[@]}"

Answer

As I said in a comment, the basic problem is that sudo cleans its environment (including both variables and functions) before running the command (/script) as another user. This can be overridden with sudo -E, but only if it's explicitly allowed in /etc/sudoers.

But the problem is not insoluble; you just have to include the definition of the function in the script, so it gets recreated in that environment. bash even has a convenient command, declare -f display_func, that prints the function definition in the appropriate form (and declare -p variable does the same for variables). So you can use those to add the appropriate definitions to the script.

Here's a script I wrote to do this. I made a few other changes vs. your script: I take -u username to specify a different user to run as (so you don't have to pass '' as the first argument if you don't want to specify a different user). I also added -f functionname and -v variablename to "export" additional function and variable definitions into the script (in case the main function depends on them). I also create the temp script file in /tmp, and change ownership if necessary so it'll be readable by the other user.

#!/bin/bash

me="$(basename "$0")"
usage() {
    echo "Usage: $me [-u user] [-f otherfunction] [-v variablename] function [args...]" >&2
}

tmp_script_file=$(mktemp "/tmp/${me}.XXXXXXXXXXXX") || {
    echo "Error creating temporary script file" >&2
    exit 1
}
echo "#!/bin/bash" > "$tmp_script_file" # Not actually needed, since we'll run it with "bash"

# Parse the command options; "-u" gets stored for later, but "-f" and "-v" write
# the relevant declarations to the script file as we go.
sudo_user=""
while getopts u:f:v: OPT; do
    case "$OPT" in
      u)
        sudo_user="$OPTARG" ;;

      f)
        declare -f "$OPTARG" >>"$tmp_script_file" || {
            echo "Error saving definition of function $OPTARG" >&2
            exit 1
        } ;;

      v)
        declare -p "$OPTARG" >>"$tmp_script_file" || {
            echo "Error saving definition of variable $OPTARG" >&2
            exit 1
        } ;;

      ?) usage; exit 1 ;;
    esac
done
shift $(($OPTIND-1))

if (( $# == 0 )); then # No actual command specified
    usage
    exit 1
fi

# Write the main function itself into the script
declare -f "$1" >>"$tmp_script_file" || {
    echo "Error saving definition of function $1" >&2
    exit 1
}

# Then the command to run it, with arguments quoted/escaped as
# necessary.
printf "%q " "$@" >>"$tmp_script_file"
# the printf above won't write a newline, so add it by hand
echo >>"$tmp_script_file"

# If the script will run as someone other than root, change ownership of the
# script so the target user can read it
if [[ -n "$sudo_user" ]]; then
    sudo chown "$sudo_user" "$tmp_script_file"
fi

# Now launch the script, suitably sudo'ed
sudo ${sudo_user+ -u "$sudo_user"} bash "$tmp_script_file"

# Clean up
sudo rm "$tmp_script_file"

Here's an example of using it:

$ foo() { echo "foo_variable is '$foo_variable'"; }
$ bar() { echo "Running the function bar as $(whoami)"; echo "Arguments: $*"; foo; }
$ export -f foo bar # need to export these so the script can see them
$ export foo_variable='Whee!!!' # ditto
$ # Run the function directly first, so see what it does
$ bar 1 2 3
Running the function bar as gordon
Arguments: 1 2 3
foo_variable is 'Whee!!!'
$ # Now run it as another user with the wrapper script
$ ./sudo_wrap.sh -f foo -v foo_variable -u deenovo bar 1 2 3
Running the function bar as deenovo
Arguments: 1 2 3
foo_variable is 'Whee!!!'

Note that you could remove the need to export the functions and variables by either running the script with source or making it a function, but doing that would require changes to how $me is defined, the usage function, replacing all those exits with returns, and maybe some other things I haven't thought of.

Comments