I am writing a large shell program comprised of several source files. I was looking for a basic import system that accomplishes three goals.
- Avoid sourcing entire files for access to just a few functions
- Prevent files from being sourced multiple times.
- 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.
-
\$\begingroup\$ if I was doing this in bash I set more error and debugging flags rather than tons of ifdefs or debug statements \$\endgroup\$μολὼν.λαβέ– μολὼν.λαβέ2016年10月29日 17:54:17 +00:00Commented Oct 29, 2016 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– arctelix2016年10月29日 18:33:59 +00:00Commented Oct 29, 2016 at 18:33
1 Answer 1
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?
-
\$\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 theimport
function will not conflict with anything on your system. If you have a program calledimport
on your system then it will be superseded by the local import function when you runsource core.sh
. I have slightly modified the code and provided more information on how to set it up for a test. \$\endgroup\$arctelix– arctelix2016年10月29日 01:36:14 +00:00Commented Oct 29, 2016 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 ranbash core.sh main.sh set_name
then you surely did get a recursion error. Thanks for your interest! \$\endgroup\$arctelix– arctelix2016年10月29日 01:58:57 +00:00Commented Oct 29, 2016 at 1:58