Problem
To say that Bash has many idiosyncrasies is an understatement. One of the most troubling is the lack of an obvious way to return something besides an integer from a function without invoking the function in a subshell.
Subshells
The term subshell means that Bash fork()'s a child process, and executes the function within that separate but initially identical process. If you want to write a multi-process application, this is a wonderful feature. If you simply want to get a complex return value from a function, this can be an exercise in masochism.
Complex Return Values
Again: you can already return an integer from any Bash function without invoking a subshell; the problem is how to go about returning anything else.
One possible way to relay the result of a function is to store it in a global variable, and then retrieve it after the function returns. There isn't a lot of immediate appeal to this strategy since a single global variable dedicated for this purpose precludes the straightforward implementation of recursion, or having any function return more than one value.
Return Stack
Stacks have been used in computing forever. Implementing one in Bash only takes 77 well-spaced lines. First off, there must be global array which can be used for this stack:
# Global array to use as a return stack
declare -g -a __RTN_STK
Notice the leading underscores to avoid namespace collisions. Next we'll need a way to push things onto this stack. Since Bash is casual about function prototypes, we'll leave it up to the caller to decide how many items to push on our stack:
function RTN_push
############################################################
# Use this to push things onto the global return stack
# Arguments:
# Whatever strings you wish placed on the stack
#
{
# Necessary to avoid "unbound variable" exception for empty stack
local OPTS=$(shopt -o -p nounset errexit)
set +ue
# For readability, store array index value here before using
local -i ndx
# Push arguments onto the stack
while [[ -n "$1" ]]; do
# Array index is current size of array
ndx=${#__RTN_STK[@]}
# Place argument onto stack
__RTN_STK[$ndx]="$1"
# Discard argument from argv
shift
done
# Restore options
eval $OPTS
}
Excellent. Now we'll need a way to pop the values from the stack:
function RTN_pop
############################################################
# Use this to pop things off of the return stack
# Arguments:
# Names of global varibles to be loaded by popping
# strings from the stack.
#
{
# Necessary to avoid "unbound variable" exception
local OPTS=$(shopt -o -p nounset)
set +u
local -i arg ndx
for (( arg= $#; arg ; --arg )); do
ndx=${#__RTN_STK[@]}
(( --ndx ))
eval ${!arg}="\${__RTN_STK[\$ndx]}"
# pop from stack, free memory
unset __RTN_STK[$ndx]
done
# Restore options
eval $OPTS
}
Finally, an example of how this may be used:
#!/bin/bash
############################################################
# Example script to demonstrate a function which returns
# multiple objects without the use of subshells
#
# John Robertson <john@rrci.com>
# Initial release: Thu Sep 10 11:58:23 EDT 2020
#
# Halt on error, no globbing, no unbound variables
set -efu
# import return stack tools
source ../bash++
function returns_3_strings ()
#######################################################
# Example function with 3 returns objects
# Arguments:
# none
# Returns:
# 3 strings
#
{
RTN_push 'string #1' 'string #2' 'string #3'
}
###################################
### Execution starts here #########
###################################
# NOTE: We'll reserve R1 R2 R3 ... global
# variables to fetch return values from
# return stack.
# Call our function
returns_3_strings
# Pop the results into global return "registers"
RTN_pop R1 R2 R3
# print the results
echo "R1= '$R1', R2= '$R2', R3= '$R3'"
... and the result:
R1= 'string #1', R2= 'string #2', R3= 'string #3'
Both files referenced in this article are available on Github!
Top comments (1)
Dear god what have we done.