Simple import system for a large shell program

I am writing a large shell program comprised of several source files. I was looking for a basic import system that accomplishes three goals.

1. Avoid sourcing entire files for access to just a few functions
2. Prevent files from being sourced multiple times.
3. Imported functions must be executed in the context of their script so that internal dependancies do not have to be imported individually.

Please see the comments on the last function called import for the usage.

Any feedback on whether or not this is a relatively efficient implementation would be appreciated.

core.sh:

#!/bin/sh

# Core utilities
# Author: arctelix

# Make sure repo and library are available
if ! [ "$IMPORT_DIR" ]; then read -p "Enter directory to import from: " IMPORT_DIR < /dev/tty fi debug_text="\e[0;90m" rc="\e[0m" #reset color # Prints debug messages when DEBUG=true debug () { if [ "$DEBUG" = true ]; then
printf "%b%b%b\n" $debug_text "$1" $rc 1>&2 fi } # a print function that does not interfere with function output print () { printf "%b%b%b\n"$debug_text "$1"$rc 1>&2
}

# Test for the existence of a command
cmd_exists() {
if ! [ "$1" ];then return 1;fi command -v$1 >/dev/null 2>&1
}

# Test if script contains function
script_func_exists() {
script_exists "$1"$1 command -v $2 >/dev/null 2>&1 } # Test if script exists script_exists() { if [ -f "$1" ]; then
chmod u+x "$1" cmd_exists "$1"
return $? fi return 1 } # Creates a local function to call an external script's functions without # polluting the local scope with all of the external function names. # Also allows for sourcing external scripts without redundancy. # external script (only script name available locally): # import platforms # PLATFORM="$(platforms get_platform)"

# external script function (only imported function name available locally):
# import platforms get_platform
# PLATFORM="$(get_platform)" # source external script (makes all functions available locally): # import source platforms # PLATFORM="$(get_platform)"

import () {

usage="import [source] <file> [<func>]"
usage_full="
source  Source entire file
file    File to import
func    Function to import
"

if [ ! "$1" ];then echo "$usage"
echo "$usage_full" return fi local source if [ "$1" = "source" ]; then
source="true"
shift
fi

local file_name="$1" local script="$IMPORT_DIR/${file_name}.sh" local func_name="$2"

# if local func exists nothing to do
if [ "$func_name" ] && cmd_exists "$func_name" ; then
debug "already imported $func_name" return elif ! [ "$func_name" ] && cmd_exists "$file_name" ; then debug "already imported$file_name"
return
fi

# test for script
if ! script_exists "$script" ; then echo "IMPORT ERROR:$file_name does not exist"

# test for script func
elif [ "$func_name" ] && ! script_func_exists "$script" "$func_name"; then echo "IMPORT ERROR:$file_name $func_name does not exist" fi # source: sources script and creates a placeholder function # causing a second call to import source to be aborted. if [ "$source" ]; then
debug "importing source $file_name$func_name"
source "$script" eval "${file_name}() {
debug \"call sourced $file_name : $@ \" \"$@\" }" return fi # import: executes script functions in a sub-shell preventing # inadvertant local name collisions. if [ "$func_name" ]; then
debug "importing $func_name" eval "${func_name}() {
debug \"call $func_name : $@\" # Source file into subshell and execute function ( source \"script\"; func_name \"$@\" ) }" else debug "importing$file_name"
eval "${file_name}() { debug \"call$file_name : $@ \" # Source file into subshell and execute function ( source \"script\"; \"$@\" )
}"
fi

}


Example usage of import function:

1) Create a directory import_test and add core.sh allong with the following two files.

main.sh (this is an example of your program entry point)

export IMPORT_DIR="./"
source ./core.sh

import user get_name

echo "hello $(get_name)"  user.sh (this is an example of a module for use in main.sh) get_name () { echo "$(whoami)"
}


2) Make sure the files have execute permissions:

cd test_import
chmod 755 ./*


3) Run the main script

bash ./main.sh


You will see the output: hello yourname

4) You can use the import command in you shell as follows:

source ./core.sh


5) When prompted for an import directory enter ./.

6) Now you can do the following:

import user get_user
echo $(get_user).  Notes: In the above example there must be a file called ./user.sh and it must have a function named get_user. If you run the import command without any parameters you will now get the usage message rather then an error. • if I was doing this in bash I set more error and debugging flags rather than tons of ifdefs or debug statements – μολὼν.λαβέ Oct 29 '16 at 17:54 • @Alexander Why would I set debugging flags? I'm not trying to debug an error. The if statements are part of the programs functionality, not for debugging purposes. Please clarify? – arctelix Oct 29 '16 at 18:33 1 Answer I don't have enough reputation to comment, but I am rather intrigued by your script, because I also enjoy writing large programs in the shell, and recently have been trying write modular shell scripts. Perhaps the reason nobody has tried to answer your question is because the script is rather intimidating and somewhat confusing. At the risk of appearing ignorant, I will attempt to offer my insight from the outside perspective of someone that is trying to figure out how to use your script. I think your script suffers from a lack of ... • User friendliness - It's not clear to me how exactly this script is supposed to be used. For instance, when I run it with no arguments, instead of getting a usage message, I get this error: ghost@devbox:~/source:$ bash core.sh
bash core.sh

Inspecting the code, I see that if the variable "$DOTSYS_REPOSITORY" is not set, than the next action taken is to export it. But since I don't have a program named dotsys that names an argument named "repository" in my $PATH, I end up with a cryptic error message. So, that makes me wonder, is this script also supposed to be sourced itself, from another script?
Funny side note: I created testit.sh, which sourced core.sh, exported "$DOTSYS_REPOSITORY", created the lib directory, placed your example scripts in the proper places, and tried running it with bash core.sh main.sh set_name. Something went wrong, and bash got stuck in a loop, forking itself faster than I could kill the process, until my system crashed due to RAM exhaustion. I'm not sure why that happened. The reason I mention that is that I think your script could benefit from better documentation and error handling. Admittedly, I am not sure if I am running the script correctly, or whether some of these functions are supposed to run recursively, or what. However, conventional wisdom says... • Avoid using variable names that are also programs/shell builtins - I also don't understand why you use function and variable names that are also common program names, like import, or script. ghost@devbox:~/source/lib:$  which import script

• Thanks for your comments! This was not intended to be a standalone script, but rather as a component of a larger program in need of an import solution. Since the larger program is executed as a subprocess there should be no conflicts. The local variables inside the import function will not conflict with anything on your system. If you have a program called import on your system then it will be superseded by the local import function when you run source core.sh . I have slightly modified the code and provided more information on how to set it up for a test. – arctelix Oct 29 '16 at 1:36
• let me know if you have more success with the new setup instructions and the code modifications i made. Before the function required for set_name did not exist (my bad) and if you ran bash core.sh main.sh set_name` then you surely did get a recursion error. Thanks for your interest! – arctelix Oct 29 '16 at 1:58