bash

how to return a multi-line string from a bash function call ... properly?


I have been working with multi-line string in bash (no need to bring up bash array, it is a POSIX thing). A full working demo is posted in an online BASH emulator.

Problem I have is that everytime I call a function and return a string back, the proper way to handling multi-line string has inadvertly resulted in the tacking on an extra chr(10) to the end of the string.

Suggested duplicate(s) did not apply:

This working example of a multi-line bash string correctly has a blank line at the end and is:

# bash variable declared as multi-line string
ini_buffer="1.1.1.1"
"

That translate the original multi-line string into a hex dump of length 8:

00000000  31 2e 31 2e 31 2e 31 0a  00                       |1.1.1.1..|
00000009

Bash script starts off with:

# there is exactly one chr(10) at the end of ini_buffer
ini_buffer="1.2.3.4
"
echo "initial ini_buffer \"\"\"$ini_buffer\"\"\""
echo "initial ini_buffer len: ${#ini_buffer}"
echo


IFS= read -rd '' result < <(echo "$ini_buffer")
# got a SECOND chr(10) prepended to the final output 

echo "result len: ${#result}"
echo "\"\"\"$result\"\"\""
echo

Results are:

initial ini_buffer """1.2.3.4
"""
initial ini_buffer len: 8

result len: 9
"""1.2.3.4

"""

Notice that it grew a character?!

00000000  31 2e 31 2e 31 2e 31 0a  0a 00                    |1.1.1.1..|
00000009

Added the first function:

first_function()
{
  local first_buffer result1_buffer
  # takes a string with a single chr(10)
  first_buffer="$1"

  # calls a function, which does nothing.
  IFS= read -rd '' result1_buffer < <(second_function "$first_buffer")
  # yet it got prepended by another chr(10) for a total of two chr(10)

echoerr "result1_buffer len: ${#result1_buffer}"
echoerr "\"\"\"$result1_buffer\"\"\""
  # Suuposedly only one way to return a multi-line string neatly, 
  # and that is via STDOUT (fd=1)
  # echo "first_buffer len: ${#first_buffer}"
  echo "$result1_buffer"
}


# there is exactly one chr(10) at the end of ini_buffer
ini_buffer="1.2.3.4
"
echo "initial ini_buffer \"\"\"$ini_buffer\"\"\""
echo "initial ini_buffer len: ${#ini_buffer}"
echo

#  THIS LINE CHANGED from `echo` to `first_function`
IFS= read -rd '' result < <(first_function "$ini_buffer")
# got a SECOND chr(10) prepended to the final output 
# for a total of 3 prepended chr(10)s

echo "result len: ${#result}"
echo "\"\"\"$result\"\"\""
echo

Result of the first function is:

initial ini_buffer """1.2.3.4
"""
initial ini_buffer len: 8

result1_buffer len: 9
"""1.2.3.4

"""
result len: 10
"""1.2.3.4


"""

Every time that a function returning from a nested-called, another chr(10) gets tacked on to it upon return.

This also got increase when a second function was introduced of which I shall not include here for brevity.

This is getting maddening here to me. Has to do with the last-line being blank (or jus a chr(10) character). Not many online authoritative content on proper handling of multi-line string.

What did I do wrong?

Process substitution (<( ... )) gets used here instead of the usual command substitution ($( ... )) which had shown difficulty in working with multi-line string. As a result, I must output any debug stattement to the STDERR using:

echoerr() { printf "%s\n" "$*" >&2; }

I would like to do the proper thing of bash progrmaming with regard to multi-line handling, notably with blank line(s) at the end of its string.

A complete test is reiterated here (working demo link is in cited in the first paragraph):

echoerr() { printf "%s\n" "$*" >&2; }

dump_string_char()
{
  local string len_str idx this_char this_int
  string="$1"
  echoerr "string: \"\"\"$string\"\"\"" 
  len_str="${#string}"
  idx=0
  while [ $idx -lt ${len_str} ]; do
    this_char="${string:$idx:1}"
    this_int="$(LC_CTYPE=C printf "%d" "'$this_char")"
    echoerr "idx: $idx"
    if [ $this_int -lt 32 ]; then
      echoerr "$idx: ${this_int}" 
    else
      echoerr "$idx: \"${this_char}\"" 
    fi
    ((idx++))
  done
}

second_function()
{
  local second_ini_buffer result2_buffer
  second_ini_buffer="$1"

  # Some magical awk/sed that did not match any pattern
  # So let us use 'echo' to re-save the same string

  IFS= read -rd '' result2_buffer < <(echo "$second_ini_buffer")
  echoerr "result2_buffer len: ${#result2_buffer}"
  echoerr "\"\"\"$result2_buffer\"\"\""
  # so pass back the full ini_buffer as-is.
  # hopefully with ONE chr(1) at end-of-line.
  # but it doesn't.

  echo "$result2_buffer"
}


first_function()
{
  local first_buffer result1_buffer
  # takes a string with a single chr(10)
  first_buffer="$1"

  # calls a function, which does nothing.
  IFS= read -rd '' result1_buffer < <(second_function "$first_buffer")
  # IFS= read -rd '' result1_buffer < <(echo "$first_buffer")
  # yet it got prepended by another chr(10) for a total of two chr(10)

echoerr "result1_buffer len: ${#result1_buffer}"
echoerr "\"\"\"$result1_buffer\"\"\""
  # Suuposedly only one way to return a multi-line string neatly, 
  # and that is via STDOUT (fd=1)
  # echo "first_buffer len: ${#first_buffer}"
  echo "$result1_buffer"
}


# there is exactly one chr(10) at the end of ini_buffer
ini_buffer="1.2.3.4
"
echoerr "initial ini_buffer \"\"\"$ini_buffer\"\"\""
echoerr "initial ini_buffer len: ${#ini_buffer}"
echoerr
dump_string_char "$ini_buffer"


IFS= read -rd '' result < <(first_function "$ini_buffer")
# got a SECOND chr(10) prepended to the final output 
# for a total of 3 prepended chr(10)s

echoerr "result len: ${#result}"
echoerr "\"\"\"$result\"\"\""
echoerr

Once again to the moderator who thinks these are the same question:

It is not related to a carriage return but many ASCII characters, so this does not apply:

And does not address empty multi-lines like this question does:

Edit: to moderators, i've stated four other questions as UNRELATED.


Solution

  • Your problem here is that echo always adds a newline to the end of your string - if it already contains one, it will have two. Since you're always "returning" using echo "$result", every time it will add a new one.

    You should try using printf "%s" "$result":

    ini_buffer="1.2.3.4
    "
    echo "initial ini_buffer len: ${#ini_buffer}"
    
    IFS= read -rd '' result < <(printf "%s" "$ini_buffer")
    
    echo "result len: ${#result}"
    

    Result:

    initial ini_buffer len: 8
    result len: 8