💾 Archived View for thatit.be › 2023-02-16-09-00-17.gmi captured on 2024-05-26 at 14:49:15. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2024-05-10)
➡️ Next capture (2024-06-16)
-=-=-=-=-=-=-
This was actually last week, but I’m making a new category for “Today I Learned…” (TIL) so it’s easier for me to find things like this in the future when I want to refer back to a new thing.
I’m documenting this everywhere because it’s been a little bit of a point of contention in my scripts for a while. And the solution, as it always has been, already exists in the damn shell. I don’t know how I missed it, or why I never saw it on stack overflow, but you can structure a bash script such that it behaves like a python module with regard to being source-able or executable directly and not require any extra work on the part of the caller.
In your modules:
In your scripts that use it:
Imagine you have a function, foo, you use in your script and you want to put it in a library, library.sh. But if someone calls your library directly you want to print a message about how to use it.
Here is what library.sh might look like:
SOURCED=( $(caller) ) sourced(){ test "${SOURCED[0]}" -ne 0 } foo(){ echo "Hello from foo!" } main(){ echo "Hi, I'm just a library, source me instead of calling me directly." } if ! sourced ; then main "${@}" fi
And a sample script that sources the library and makes use of its function.
. library.sh foo
And that’s it. Now if library.sh is executed, it will print that message, but if it’s sourced, as in the above script, it just makes the function foo available.
Stop reading if you didn’t grasp the above. This next bit of foolishness is for my amusement, but if you’re still reading and you don’t understand the above, it could make it more confusing.
Let’s make it more Pythonic and import just one function from a library instead of all of the functions, first, a library that provides such a magical import method:
import() { local FUNCTION=${1} local LIBRARY=${3} eval << EOF . ${LIBRARY} ; declare -f ${FUNCTION} EOF }
Now, a script that we’ll call lol.sh that uses that and the aforementioned library.sh:
. import.sh import foo from library.sh foo
Now if the above lol.sh is called as a script, it will be able to call only the function foo from library.sh. It would not, for example, be able to call main or any other functions because import.sh used a subshell to source the library and then executed the resulting declaration of that function.
This will fall apart if the function being imported needs to call any other functions in library.sh, but I still thought it was amusing.
While using this technique for real I found that sourcing multiple libraries that also used this was problematic. I had to adapt it slightly, but I am leaving the above in tact because it’s clearer to read and understand.
An easy way to make this still work is to prefix the function and variables with a unique substring derived from the file name or purpose. For example:
FOO_SOURCED=( $(caller) ) foo_sourced(){ test "${FOO_SOURCED[0]}" -ne 0 } foo(){ echo "Hello from foo!" } main(){ echo "Hi, I'm just a library, source me instead of calling me directly." } if ! foo_sourced ; then main "${@}" fi
I’m still experimenting to make something that is automatic and portable that mimics namespaces in other languages. I’ve tried dynamically creating some new variables using random but it was hard to read and due to array use wasn’t the same across different versions of Bash. I am very close though with subscripting an array with the file name that is setting the variable.
From the man page:
caller [expr] Returns the context of any active subroutine call (a shell function or a script executed with the . or source builtins). Without expr, caller displays the line number and source filename of the current subroutine call. If a non-negative integer is supplied as expr, caller displays the line number, subroutine name, and source file corresponding to that position in the current execution call stack. This extra information may be used, for example, to print a stack trace. The current frame is frame 0. The return value is 0 unless the shell is not executing a subroutine call or expr does not correspond to a valid position in the call stack.
updated: 2023-02-16 09:29:54
generated: 2024-05-25