DEV Community

loading...
Cover image for Bash++ Return Stack

Bash++ Return Stack

John Robertson
Full Stack DevOp for Linux, Windows.
・3 min read

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!

Discussion (1)

Collapse
sirseanofloxley profile image
Sean Allin Newell

Dear god what have we done.