Search code examples
bashshellshzsh

Bash string interpolation without subshell


I have a function like this

print_stuff_and_set_vars() {
  IMPORTANT_VAL_1=""
  IMPORTANT_VAL_2=""
  echo -n "some stuff"
  echo -n "some more stuff"
  echo -n "result"
}

and I call it like this:

my_main_func() {
  print_stuff_and_set_vars
  print_stuff_and_set_vars
  print_stuff_and_set_vars
  echo "IMPORTANT_VAL_1 was $IMPORTANT_VAL_1"
}

Instead, I want to save all the echoed results to a string

my_main_func() {
  # doesn't work -- result is empty
  result="${print_stuff_and_set_vars}${print_stuff_and_set_vars}${print_stuff_and_set_vars}"
  echo "the result length was ${#result}"
  echo "$result"
  echo "IMPORTANT_VAL_1 was $IMPORTANT_VAL_1"
}

This does work if I use $() instead of ${} to start a subshell, but then the global variables are not set.

Is there any way to save the result of a function to a string without starting a subshell? I know the obvious answer in this example would be to save "result" to a global variable instead of echoing it but in my actual script that would require a lot of changes and I want to avoid it if possible.

Actually, I only need to know the length so if there is a way to keep track of how much has been printed since the start of the function that would work fine too. I'm actually using zsh if that makes a difference, too.


Solution

  • Assuming the output from the function print_stuff_and_set_vars does not contain newline characters, how about:

    mkfifo p                        # create a named pipe "p"
    exec 3<>p                       # open fd 3 for both reading and writing
    rm p                            # now "p" can be closed
    
    print_stuff_and_set_vars() {
      IMPORTANT_VAL_1="foo"
      IMPORTANT_VAL_2="bar"
      echo -n "some stuff "
      echo -n "some more stuff "
      echo -n "result "
    }
    
    my_main_func() {
      print_stuff_and_set_vars 1>&3 # redirect to fd 3
      print_stuff_and_set_vars 1>&3 # same as above
      print_stuff_and_set_vars 1>&3 # same as above
      echo 1>&3                     # send newline as an end of input
    
      IFS= read -r -u 3 result      # read a line from fd 3
      echo "the result length was ${#result}"
      echo "$result"
      echo "IMPORTANT_VAL_1 was $IMPORTANT_VAL_1"
    }
    
    my_main_func
    
    exec 3>&-                       # close fd 3
    

    Output:

    the result length was 102
    some stuff some more stuff result some stuff some more stuff result some stuff some more stuff result
    IMPORTANT_VAL_1 was foo