1. Overview

In shell programming, when we create functions, usually, we put them in a shell script. A function in a shell script contains a group of commands that we can reuse.

In this tutorial, we’re going to learn how to call such functions from outside the shell script file.

2. Example Shell Script File

Before we dive into the discussion of how to call a function from outside the script, let’s try to understand the problem through an example.

Let’s create a shell script file myScript.sh:

$ cat myScript.sh
#!/bin/bash
#A variable
VAR="VAR inside the script"
# print INFO message to stdout
# Argument: 
#    $1: INFO message to print
log_info() {
    local MSG="$1"
    printf "%s - [INFO] %s\n" "$(date)" "$MSG"
}

# print ERROR message to stderr
# Argument: 
#    $1: ERROR message to print
log_error() {
    local MSG="$1"
    printf "%s - [ERROR] %s\n" "$(date)" "$MSG" >&2
}

In the myScript.sh file, we’ve defined a variable VAR and two functions: log_info() and log_error(). We can call the functions within the script file.

Now, using the heredoc feature, let’s append two lines to myScript.sh to call the two functions, and then, let’s execute the script:

$ cat <<EOF>> myScript.sh 
heredoc> log_info "This is an INFO message."
heredoc> log_error "This is an ERROR message."
heredoc> EOF

$ ./myScript.sh 
Mon 24 Aug 2020 10:27:09 PM CEST - [INFO] This is an INFO message.
Mon 24 Aug 2020 10:27:09 PM CEST - [ERROR] This is an ERROR message.

And, as we can see, the two functions get called — so far, so good.

Now, let’s see how to call the functions from outside the myScript.sh.

3. Source the Script

Sourcing the script file is the most straightforward way to call functions defined in the script file. 

We can combine the source command and the function calls with the “*&&*” operator to build a one-liner:

$ . myScript.sh && log_info "I am an INFO message outside the script file"
Mon 24 Aug 2020 10:47:34 PM CEST - [INFO] I am an INFO message outside the script file

As the example shows, after sourcing the script file, we can call the functions from the command line.

4. The Problems of Sourcing a Script

We’ve seen that sourcing a script file to call functions in it is pretty straightforward. However, under some situations, we cannot directly source the script file. Next, let’s see a couple of cases where sourcing is problematic.

4.1. Variables Get Overwritten

After we source a script, it overwrites the values of variables that are set in the current shell if the names of variables are the same:

$ VAR="A very important message to print"; . myScript.sh && log_info "$VAR"
Mon 24 Aug 2020 11:05:16 PM CEST - [INFO] VAR inside the script

As the above example shows, after we sourced the script, the value of the variable VAR has been overwritten by the value in the script. This is because when we source a script, the script will be executed in the current shell. Therefore, we don’t get the expected info log.

4.2. All Commands in the Script Get Executed

Another side-effect of sourcing a script is that it will execute all commands in the script. Let’s add some commands to myScript.sh:

$ cat myScript.sh
#!/bin/bash
VAR="VAR inside the script"
log_info() ...
log_error() ...
# simulate some command to send emails to all users
echo "Sending emails to all email addresses in users.txt..."
echo "Done"
# log
log_info "Emails have been sent to all users."

The newly added commands simulate sending emails to all users.

Let’s see what happens if we source the file and call the log_info() function:

$ . myScript.sh && log_info "An INFO message outside the script"
Sending emails to all email addresses in users.txt...
Done
Mon 24 Aug 2020 11:26:48 PM CEST - [INFO] Emails have been sent to all users.
Mon 24 Aug 2020 11:26:48 PM CEST - [INFO] An INFO message outside the script

As the output shows, the expected log output has been printed. However, the commands for sending emails have been executed as well. When we want to call the log_info() function in the script only to write a log message, we clearly don’t want this action to send emails to all the users.

It will be better if we can find a way to call the required functions in a script without sourcing the script.

5. Call Functions Without Sourcing the Script

Usually, if a shell script contains functions, the function declarations are ahead of those commands that do concrete work, such as the commands for sending emails in our myScript.sh.

We can modify the script by adding a bit of function call handling code between the functions we want to expose and the other commands contained in the script.

5.1. Extend the Script to Handle External Function Calls

An example will help us to understand this approach more easily. Let’s first see the modified script file and how it handles external function calls:

$ cat myScript.sh 
#!/bin/bash
VAR="VAR inside the script"
log_info() ...
log_error() ...

case "$1" in
    "") ;;
    log_info) "$@"; exit;;
    log_error) "$@"; exit;;
    *) log_error "Unkown function: $1()"; exit 2;;
esac

# simulate to send emails to all users
....

We’ve added a case statement between the function declarations and the email sending commands. The case statement is responsible for managing function calls from outside.

It allows us to execute the command ./myScript.sh function_name arguments to call the functions defined in myScript.sh without sourcing the whole script:

$ ./myScript.sh log_info "An INFO message outside the script"
Tue 25 Aug 2020 02:01:08 PM CEST - [INFO] An INFO message outside the script

As the example shows, we see the expected logs. Also, the commands for sending emails are not executed.

Further, since we don’t source the script, the variable in the current shell will not be overwritten by the script:

$ VAR="An ERROR message outside the script"; ./myScript.sh log_error "$VAR"
Tue 25 Aug 2020 02:04:25 PM CEST - [ERROR] An ERROR message outside the script

Moreover, if we attempt to call a non-existent function, we’ll see the error message:

$ ./myScript.sh log_warn "A WARN message outside the script"
Tue 25 Aug 2020 02:07:42 PM CEST - [ERROR] Unkown function: log_warn()

Finally, the case statement will not influence the script’s normal execution:

$ ./myScript.sh 
Sending emails to all email addresses in users.txt...
Done
Tue 25 Aug 2020 02:13:54 PM CEST - [INFO] Emails have been sent to all users.

5.2. How It Works

Now, let’s understand how the case statement handles external function calls.

When we call a function using the command ./myScript.sh function_name argument, the function_name becomes the first argument of the script. Therefore, we can check the “$1” variable in the case statement:

  • “”) ;; – If the $1 argument is empty, it’s a normal execution of the script instead of an external function call. Therefore, we continue the execution beyond the case statement
  • log_info) “$@”; exit;; – If a function name matches, we call the matched function with all arguments and exit the execution after the function execution
  • *) log_error “Unkown function: $1()”; exit 2;; – If we cannot find a matched function name, we think the caller attempted to call an invalid function. Therefore, we print the error log and exit

6. Organize Common Functions in a Separate Script

We’ve learned that checking the $1 variable in the script is one way to route external function calls. However, when we write shell scripts, if other scripts could reuse some functions, we should consider moving the functions into a separate script and sourcing it from our current script.

This makes the sharing of the functions a lot easier and cleaner.

For example, we can separate our myScript.sh into two scripts, logger.sh and sendEmail.sh:

$ cat logger.sh 
#!/bin/bash
log_info() {
    local MSG="$1"
    printf "%s - [INFO] %s\n" "$(date)" "$MSG"
}

log_error() {
    local MSG="$1"
    printf "%s - [ERROR] %s\n" "$(date)" "$MSG" >&2
}
$ cat sendEmail.sh 
#!/bin/bash

source logger.sh

VAR="Inside Script"
# simulate to send emails to all users
echo "Sending emails to all email addresses in users.txt..."
echo "Done"
# log
log_info "Emails have been sent to all users."

In this way, if we want to call the log functions, we source logger.sh:

$ . logger.sh && log_info "I am an INFO message outside the script file" 
Tue 25 Aug 2020 10:47:34 PM CEST - [INFO] I am an INFO message outside the script file

7. Conclusion

In this article, we’ve learned how to call a function defined in a shell script from the command line.

At first, we may think that sourcing the script is the most straightforward approach. However, we should keep in mind that sourcing a script will execute all commands in the script and may have the unwanted side-effect of overwriting variables already defined in the shell.

Later, we’ve also seen how to call a function without sourcing the whole script. Finally, we saw that by moving functions into separate script files, we can safely source them from the command line or from other scripts.