49

Let's say I have:

function x {
    echo "x"
}
call_func="x"

Now, I can simply use eval as follows:

eval $call_func

but I was wondering if there was some other way of invoking the function (if it exists) whose name is stored in the variable: call_func.

hjpotter92
  • 78,589
  • 36
  • 144
  • 183

3 Answers3

58

You should be able to just call the function directly using

$call_func

For everything else check out that answer: https://stackoverflow.com/a/17529221/3236102 It's not directly what you need, but it shows a lot of different ways of how to call commands / functions.

Letting the user execute any arbitrary code is bad practice though, since it can be quite dangerous. What would be better is to do it like this:

if [ $userinput == "some_command" ];then
    some_command
fi

This way, the user can only execute the commands that you want them to and can even output an error message if the input was incorrect.

Dakkaron
  • 5,930
  • 2
  • 36
  • 51
  • The variable inside `call_func` will be a string (taken from user-input). just putting `$call_func` echoes out its content, without executing the function. – hjpotter92 Oct 28 '15 at 09:39
  • 1
    This should not happen. You said, you are using bash? What happens if you put this: `echo $($call_func)`? Also, maybe try to solve that problem differently. Letting the user execute arbitrary code is dangerous! – Dakkaron Oct 28 '15 at 09:40
  • Ah, nvm. It was because the function name and `echo` inside it were the same. :| I feel silly now. – hjpotter92 Oct 28 '15 at 09:42
  • No worries! I added some more info on how to handle that situation better. – Dakkaron Oct 28 '15 at 09:43
  • There are about 20 acceptable function names. Currently I'm searching if the user input is found in a string (which contains all the allowed names) and then `eval` it (or just calling it directly, now). I'd love a cleaner workaround though. – hjpotter92 Oct 28 '15 at 09:46
  • That works as well. The best way with that amount of functions would probably be a case statement along the lines of the if-statement I posted above. – Dakkaron Oct 28 '15 at 10:18
  • 2
    I suggest to use ```case $userinput in; command1|command2|command) $userinput ;; esac``` this way you have your security in checking the input, but less noise as using many explicit if-elses – Phil Sep 05 '18 at 08:35
  • Using `command` as your stand-in is a bad choice, as it's the name of a shell builtin. We've had questions from someone trying to follow this answer as an example caused by that particular confusion. – Charles Duffy Sep 11 '22 at 01:28
  • @CharlesDuffy Fair point – Dakkaron Sep 15 '22 at 13:52
4

Please, Take note of:

Variables hold data, functions hold code.

It is bad practice to mix them, do not try to do it.


Yes, just use a var. If the var a was set by a=ls, then:

$ $a

will execute ls. The first $ is the line prompt of the shell.

  • Yes. The method works. I got confused as the `echo` inside `x` was same as the value of `call_func`. – hjpotter92 Oct 28 '15 at 09:49
  • 1
    What if I also want to call the function with an argument (which is also a variable)? – MrCalvin Jan 31 '18 at 22:32
  • @MrCalvin `eval "$cmd_with_args"` – Carl G Apr 04 '18 at 21:53
  • 1
    My take on what [the link](http://mywiki.wooledge.org/BashFAQ/050) provided says about 'mixing functions and variables' is that it is possible to dynamically construct function/argument calls, but that one should separate them like so: `$the_command "${args[@]}"` (where `args` is an array) to avoid weird word-splitting issues. – Carl G Apr 04 '18 at 22:01
3

File Processing Use Case: Emulate Passing Anonymous Functions

If you have 100 files of five different types, and you want a different function to process each type of file, you can create a switching function that contains a for loop with an embedded case statement.

Your goal would be to supply the switching function with:

  1. The name of the file processing function. ($1)

  2. A list of filenames by calling the appropriate file gathering function.

Hence, instead of writing separate functions to loop through each kind of file, you just use one function to do that.

#!/bin/sh

##################################################################
#        Functions that gather specific kinds of filenames       #
##################################################################

function getDogFiles
{
    local -r TARGET_DIR="$1"
    local fileGlobPattern="*.dog"

    ls ${TARGET_DIR}${fileGlobPattern}
}

function getCatFiles
{
    local -r TARGET_DIR="$1"
    local fileGlobPattern="*.cat"

    ls ${TARGET_DIR}${fileGlobPattern}
}

function getBirdFiles
{
    local -r TARGET_DIR="$1"
    local fileGlobPattern="*.bird"

    ls ${TARGET_DIR}${fileGlobPattern}
}

function getFishFiles
{
    local -r TARGET_DIR="$1"
    local fileGlobPattern="*.fish"

    ls ${TARGET_DIR}${fileGlobPattern}
}

function getFrogFiles
{
    local -r TARGET_DIR="$1"
    local fileGlobPattern="*.frog"

    ls ${TARGET_DIR}${fileGlobPattern}
}

##################################################################
#            Functions that process each type of file.           #
##################################################################

function processDogFiles
{
    local -r FILE_NAME="$1"
    cat $FILE_NAME
}

function processCatFiles
{
    local -r FILE_NAME="$1"
    cat $FILE_NAME
}

function processBirdFiles
{
    local -r FILE_NAME="$1"
    cat $FILE_NAME
}

function processFishFiles
{
    local -r FILE_NAME="$1"
    cat $FILE_NAME
}

function processFrogFiles
{
    local -r FILE_NAME="$1"
    cat $FILE_NAME
}

##################################################################
#            Functions to process all of the files               #
##################################################################

function processItems
{
    local -r PROCESSING_FUNCTION=$1
    shift 1
    
    for item in "$@"
    do
        $PROCESSING_FUNCTION "$item"
    done
}

function processAnimalFiles
{
    local -r TARGET_DIR="$1"

    shift 1  # Remove the target directory from the argument list.

    local -ar FILE_TYPES=( "$@" )

    processingPrefix="process"
    processingSuffix="Files"

    gatheringPrefix="get"
    gatheringSuffix="Files"

    for fileType in "${FILE_TYPES[@]}"
    do
        case "$fileType" in
            Dog | Cat | Bird | Fish | Frog)
                fileProcessingFunction="${processingPrefix}${fileType}${processingSuffix}"
                fileGatheringFunction="${gatheringPrefix}${fileType}${gatheringSuffix}"
                processItems "$fileProcessingFunction" $($fileGatheringFunction "$TARGET_DIR")   #    The second argument expands to a list of file names. 
                ;;
            *)
                echo "Unknown file type: ${fileType} file." >> /var/log/animalFiles.err.log
                ;;
        esac
    done
}

#############################################################################

local -a animalFiles=(Dog Cat Bird Fish Frog Truck)
processAnimalFiles "/opt/someapp/data/" "${animalFiles[@]}"
Tom Hale
  • 40,825
  • 36
  • 187
  • 242
Anthony Rutledge
  • 6,980
  • 2
  • 39
  • 44