Importing functions from a shell script

Shell

Shell Problem Overview


I have a shell script that I would like to test with shUnit. The script (and all the functions) are in a single file since it makes installation much easier.

Example for script.sh

#!/bin/sh

foo () { ... }
bar () { ... }

code

I wanted to write a second file (that does not need to be distributed and installed) to test the functions defined in script.sh

Something like run_tests.sh

#!/bin/sh

. script.sh

# Unit tests

Now the problem lies in the . (or source in Bash). It does not only parse function definitions but also executes the code in the script.

Since the script with no arguments does nothing bad I could

. script.sh > /dev/null 2>&1

but I was wandering if there is a better way to achieve my goal.

Edit

My proposed workaround does not work in the case the sourced script calls exit so I have to trap the exit

#!/bin/sh

trap run_tests ERR EXIT

run_tests() {
   ...
}

. script.sh

The run_tests function is called but as soon as I redirect the output of the source command the functions in the script are not parsed and are not available in the trap handler

This works but I get the output of script.sh:

#!/bin/sh
trap run_tests ERR EXIT
run_tests() {
   function_defined_in_script_sh
}
. script.sh

This does not print the output but I get an error that the function is not defined:

#!/bin/sh
trap run_tests ERR EXIT
run_tests() {
   function_defined_in_script_sh
}
. script.sh | grep OUTPUT_THAT_DOES_NOT_EXISTS

This does not print the output and the run_tests trap handler is not called at all:

#!/bin/sh
trap run_tests ERR EXIT
run_tests() {
   function_defined_in_script_sh
}
. script.sh > /dev/null

Shell Solutions


Solution 1 - Shell

According to the “Shell Builtin Commands” section of the bash manpage, . aka source takes an optional list of arguments which are passed to the script being sourced. You could use that to introduce a do-nothing option. For example, script.sh could be:

#!/bin/sh

foo() {
    echo foo $1
}

main() {
    foo 1
    foo 2
}

if [ "${1}" != "--source-only" ]; then
    main "${@}"
fi

and unit.sh could be:

#!/bin/bash

. ./script.sh --source-only

foo 3

Then script.sh will behave normally, and unit.sh will have access to all the functions from script.sh but will not invoke the main() code.

Note that the extra arguments to source are not in POSIX, so /bin/sh might not handle it—hence the #!/bin/bash at the start of unit.sh.

Solution 2 - Shell

Picked up this technique from Python, but the concept works just fine in bash or any other shell...

The idea is that we turn the main code section of our script into a function. Then at the very end of the script, we put an 'if' statement that will only call that function if we executed the script but not if we sourced it. Then we explicitly call the script() function from our 'runtests' script which has sourced the 'script' script and thus contains all its functions.

This relies on the fact that if we source the script, the bash-maintained environment variable $0, which is the name of the script being executed, will be the name of the calling (parent) script (runtests in this case), not the sourced script.

(I've renamed script.sh to just script cause the .sh is redundant and confuses me. :-)

Below are the two scripts. Some notes...

  • $@ evaluates to all of the arguments passed to the function or script as individual strings. If instead, we used $*, all the arguments would be concatenated together into one string.
  • The RUNNING="$(basename $0)" is required since $0 always includes at least the current directory prefix as in ./script.
  • The test if [[ "$RUNNING" == "script" ]].... is the magic that causes script to call the script() function only if script was run directly from the commandline.

script

#!/bin/bash

foo ()    { echo "foo()"; }

bar ()    { echo "bar()"; }

script () {
  ARG1=$1
  ARG2=$2
  #
  echo "Running '$RUNNING'..."
  echo "script() - all args:  $@"
  echo "script() -     ARG1:  $ARG1"
  echo "script() -     ARG2:  $ARG2"
  #
  foo
  bar
}

RUNNING="$(basename $0)"

if [[ "$RUNNING" == "script" ]]
then
  script "$@"
fi

runtests

#!/bin/bash

source script 

# execute 'script' function in sourced file 'script'
script arg1 arg2 arg3

Solution 3 - Shell

If you are using Bash, a similar solution to @andrewdotn's approach (but without needing an extra flag or depending on the script name) can be accomplished by using BASH_SOURCE array.

script.sh:

#!/bin/bash

foo () { ... }
bar () { ... }

main() {
    code
}

if [[ "${#BASH_SOURCE[@]}" -eq 1 ]]; then
    main "$@"
fi

run_tests.sh:

#!/bin/bash

. script.sh

# Unit tests

Solution 4 - Shell

If you are using Bash, another solution may be:

#!/bin/bash

foo () { ... }
bar () { ... }

[[ "${FUNCNAME[0]}" == "source" ]] && return
code

Solution 5 - Shell

I devised this. Let's say our shell library file is the following file, named aLib.sh:

funcs=("a" "b" "c")                   # File's functions' names
for((i=0;i<${#funcs[@]};i++));        # Avoid function collision with existing
do
        declare -f "${funcs[$i]}" >/dev/null
        [ $? -eq 0 ] && echo "!!ATTENTION!! ${funcs[$i]} is already sourced"
done

function a(){
        echo function a
}
function b(){
        echo function b
}
function c(){
        echo function c
}


if [ "$1" == "--source-specific" ];      # Source only specific given as arg
then    
        for((i=0;i<${#funcs[@]};i++));
        do      
                for((j=2;j<=$#;j++));
                do      
                        anArg=$(eval 'echo ${'$j'}')
                        test "${funcs[$i]}" == "$anArg" && continue 2
                done    
                        unset ${funcs[$i]}
        done
fi
unset i j funcs

At the beginning it checks and warns for any function name collision detected. At the end, bash has already sourced all functions, so it frees memory from them and keeps only the ones selected.

Can be used like this:

 user@pc:~$ source aLib.sh --source-specific a c
 user@pc:~$ a; b; c
 function a
 bash: b: command not found
 function c

~

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMatteoView Question on Stackoverflow
Solution 1 - ShellandrewdotnView Answer on Stackoverflow
Solution 2 - ShellDocSalvagerView Answer on Stackoverflow
Solution 3 - ShellHelder PereiraView Answer on Stackoverflow
Solution 4 - ShellsmartechView Answer on Stackoverflow
Solution 5 - Shellmark_infiniteView Answer on Stackoverflow