3
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ if I was doing this in bash I set more error and debugging flags rather than tons of ifdefs or debug statements \$\endgroup\$ – μολὼν.λαβέ Oct 29 '16 at 17:54
  • \$\begingroup\$ @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? \$\endgroup\$ – arctelix Oct 29 '16 at 18:33
3
\$\begingroup\$

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
core.sh: 9: core.sh: dotsys: not found

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
/usr/bin/import
/usr/bin/script

Because those programs exist on my system, it's possible that the shell could become confused. But I am also confused... Perhaps you could more thoroughly explain the context of how this script is supposed to be used?

\$\endgroup\$
  • \$\begingroup\$ 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. \$\endgroup\$ – arctelix Oct 29 '16 at 1:36
  • \$\begingroup\$ 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! \$\endgroup\$ – arctelix Oct 29 '16 at 1:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.