3
#!/bin/sh

execute_cmd()
{
   $($@)
}

execute_cmd export MY_VAR=my_val
echo ${MY_VAR}

Since $() executes in a sub-shell, $MY_VAR isn't set properly in the shell the script is running.

My question, how can I pass the export command to a function and execute it in the current shell the script is running in?

2 Answers2

5

It isn't clear what you want to do. $($@) doesn't make sense, but what do you want to do instead?

What $($@) does:

  • Take the arguments of the function. This is a list of strings.
  • Split each piece further where they contain whitespace¹.
  • Take each split piece and interpret it as a wildcard pattern. If the pattern matches any file, replace the piece by the list of matching file names.
  • Use the first piece as the name of a command to execute (function, shell builtin or executable file) and pass the other pieces as arguments.
  • Take the output of the command and split it where it contains whitespace.
  • Take each split piece and interpret it as a wildcard pattern. If the pattern matches any file, replace the piece by the list of matching file names.
  • Use the first piece as the name of a command to execute (function, shell builtin or executable file) and pass the other pieces as arguments.

If that sounds complicated, that's because it is.

If you want execute_cmd export MY_VAR=my_val to execute export MY_VAR=my_val, why are you even bothering with execute_cmd?

There are two ways this could be interpreted sensibly.

  1. You want to pass a command with parameters to a function. A command with parameters is a list of strings, the first being a function name, shell builtin or executable file. Then the syntax to invoke this command on the supplied parameters is "$@".

    execute_cmd () {
      "$@"
    }
    execute_cmd export MY_VAR=my_val
    

    The double quotes avoid the splitting and wildcard expansion steps I mentioned above. Always use double quotes around variable substitutions.

    Also note the dual nature of the export keyword/builtin. While export MY_VAR=$(seq 10) works at assigning the output of seq 10 to $MY_VAR as the shell parses MY_VAR=$(seq 10) because of the presence of the export keyword, the shell wouldn't parse MY_VAR=$(seq 10) as an assignment in execute_cmd export MY_VAR=$(seq 10), because the command is not export here, but execute_cmd, so that MY_VAR=$(seq 10) is parsed as in any argument to any normal command, and split+glob is performed upon $(seq 10), so you need: execute_cmd export MY_VAR="$(seq 10)".

  2. You want to run a shell snippet. A shell snippet is a single string, to be passed as a single argument. To run a string containing shell code, use the eval builtin.

    execute_cmd () {
      eval "$1"
    }
    execute_cmd 'export MY_VAR=my_val'
    

¹ Assuming the default IFS. If you know about that you don't need to read this paragraph.

1

You should check Gilles' answer for all the details. In short, you can use eval instead of $():

execute_cmd()
{
    eval "$@"
}

From bash manual:

eval [arguments]

The arguments are concatenated together into a single command, which is then read and executed, and its exit status returned as the exit status of eval. If there are no arguments or only empty arguments, the return status is zero.

Other shells usually have eval built-in with similar semantics.

sebasth
  • 14,872
  • Follow up question. What if I'm needing to redirect the stdout and stderr of the argument passed to the function, to a logfile.

    e.g.

    eval $@ 2>&1 | tee -a ${logfile}

    – DuckCowMooQuack Aug 28 '17 at 17:52
  • Usual output redirection: eval echo "Hello World!" > log.txt, or execute_cmd echo "Hello World" > log.txt. Note the latter redirects the output of execute_cmd instead of passing those characters to the function. To redirect in execute_cmd pass the redirection as string: execute_cmd 'echo "Hello World" > log.txt'. – sebasth Aug 28 '17 at 17:59
  • 1
    eval $@ doesn't make sense. It should probably be either "$@" or eval "$1", or possibly eval "$@" (which makes execute_cmd redundant since it's equivalent to eval). – Gilles 'SO- stop being evil' Aug 28 '17 at 22:40