1

I'm surprised by the difference in manipulating the array between echo and printf in Bash:

  1. printf cannot handle += operator
  2. Both printf and echo cannot get results of += out of while loop

So, why? I'm using Bash 5.2.37 in Ubuntu 25.10.

declare -a array=("a b")
printf "c\nd" | xargs -n1 | while IFS= read -r line; do
    array+=($line)
    echo   "in while(echo):    array=${array[@]}"
    printf "in while(printf):  array=${array[@]}\n"
    echo; echo
done
echo   "out of while(echo):   array=${array[@]}"
printf "out of while(printf): array=${array[@]}\n"

Output:

in while(echo):    array=a b c
in while(printf):  array=a b

in while(echo):    array=a b c d
in while(printf):  array=a b

out of while(echo):   array=a b
out of while(printf): array=a b

1 Answer 1

8
  1. it's not that "printf can't handle the += operator", it's that you're using printf wrong, because bash doesn't expand arrays like you assume it does.

    printf "in while(printf): array=${array[@]}\n" is NOT expanded by bash as:

    printf "in while(printf):  a _b c d\n"`
    

    It is expanded as:

    printf "in while(printf):  a _b\n" "c" "d"
    

    The first element of the array is expanded inside the double-quoted string. The remainder are expanded as additional arguments outside of the string. Since you don't have any %s format strings ("conversion specifiers") for the args "c" and "d", they're not printed.

    To do what you want, try something like this:

    printf "in while(printf):  array="
    printf "%s " "${array[@]}"
    printf "\n"
    

    But note that there will be a trailing space in the output after the last element of the array. To avoid, that, and a better solution all-round is to use a proper join function to join the elements of the array into a single string. Unfortunately, bash doesn't have one built-in, but it's not too difficult to write one. e.g.

    function join_by {
      [ -z "$1" ] && return
      local d="$1"; shift;     # delimiter AKA separator
    
      [ -z "$1" ] && return
      printf "$1"; shift;      # first arg after delimiter
    
      printf "%s" "${@/#/$d}"; # remaining args, if any
    }
    
    j=$(join_by " " "${array[@]}")
    printf "in while(printf):  $j\n"
    
  2. "cannot get results of += out of while loop".

    That's because the while loop is being run in a pipeline, which means that it's being run in a separate child shell. Child processes CAN NOT affect the environment of their parent process. This means that any changes you make to the array only happen within the child process, i.e. they're ephemeral and disappear when the child exits.

    You can avoid this by using Process Substition instead of a pipe to feed data into the while read loop. That way, the while read is being run in the main shell, not in a child shell. For example:

#!/bin/bash

function join_by {
  [ -z "$1" ] && return
  local d="$1"; shift;     # delimiter AKA separator

  [ -z "$1" ] && return
  printf "$1"; shift;      # first arg after delimiter

  printf "%s" "${@/#/$d}"; # remaining args, if any
}


declare -a array=("a b")

while IFS= read -r line; do
    array+=($line)
    echo   "in while(echo):    array=${array[@]}"

    j=$(join_by " " "${array[@]}")
    printf "in while(printf):  array=$j\n"

    echo; echo
done < <(printf "%s\n" c d)


echo   "out of while(echo):   array=${array[@]}"

j=$(join_by " " "${array[@]}")
printf "out of while(printf):  array=$j\n"

Example output:

in while(echo):    array=a b c
in while(printf):  array=a b c


in while(echo):    array=a b c d
in while(printf):  array=a b c d


out of while(echo):   array=a b c d
out of while(printf):  array=a b c d

Rather than a while read loop, you're probably better off just using readarray. For example:

array=("a b")
typeset -p array

readarray -t tmp_array < <(printf "%s\n" c d)
typeset -p tmp_array

array+=("${tmp_array[@]}")
typeset -p array

Output:

declare -a array=([0]="a b")
declare -a tmp_array=([0]="c" [1]="d")
declare -a array=([0]="a b" [1]="c" [2]="d")
9
  • Thanks for your explanation in detail! It's very clear and I'm understanding clearly! Commented Sep 4 at 3:47
  • FYI, some essential reading about while read loops: Why is using a shell loop to process text considered bad practice? and Understanding "IFS= read -r line" Commented Sep 4 at 4:18
  • Why is my variable local in one 'while read' loop, but not in another seemingly similar loop? has information on making that whatever | while ... work. Commented Sep 4 at 6:50
  • The idea of "${array[@]}" is to have the array elements as distinct args, so that e.g. ls "${filenames[@]}" works for an arbitrary list of filenames, even with whitespace. But the shell can also join them into a string, just use "${array[*]}" instead. Commented Sep 4 at 6:53
  • @ilkkachu [*] instead of [@] - yeah, that's true, that would work. I guess I think too much in perl-ish terms when it comes to working with arrays and lists. And bash really does need a proper join function. So does awk, for that matter. It's so essential a feature it should be built-in. Commented Sep 4 at 7:00

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.