Bash concatenate strings: join, append (`+=`), separators, and newlines

Tech reviewed: Deepak Prasad
Bash concatenate strings: join, append (`+=`), separators, and newlines

You often need one string built from several pieces: paths, log lines, CSV chunks, or a message assembled in steps. Bash string concatenation is usually either sticking expansions together (${left}${right}), growing text with +=, or handing joins to printf when you want separators without a trailing mess. The sections below cover those patterns, plus the ${VAR1}_ brace trap that bites the first time you add an underscore without braces.

Tested all the commands and code from this article on Ubuntu 25.04, kernel 6.14.0-37-generic, Bash 5.2.37.


Bash concatenate strings (side by side)

Two expansions next to each other become one string—no operator required:

bash
VAR1='Hello '   # trailing space on purpose
VAR2='World'

echo "${VAR1}${VAR2}"

Output:

text
Hello World

Store the same thing in another variable:

bash
VAR3="${VAR1}${VAR2}"
echo "$VAR3"

Same line:

text
Hello World

Bash append to string with +=

+= appends to a string (or grows an array when var+=(item)—different shape). For plain concat string bash workflows:

bash
s="a"
s+="b"
s+="c"
echo "$s"

Output:

text
abc

That is the usual bash append string pattern inside loops when you accumulate text.


Separators and the ${name}_ gotcha

If you glue a literal _ (or -, ., etc.) straight after a variable without braces, the shell greedily reads the longest valid name first. VAR1_ may be a different variable than VAR1:

bash
VAR1=Hello
VAR2=World

echo "unbraced: [$VAR1_$VAR2]"
echo "braced:   [${VAR1}_${VAR2}]"

Output:

text
unbraced: [World]
braced:   [Hello_World]

Use ${VAR1}_${VAR2} (or ${VAR1}-${VAR2}) whenever the next character could be _, a letter, or a digit. Same rule applies for bash join strings with separator patterns in larger pipelines.


Building text in a loop (bash append to string)

A common pattern is to walk a list and append each hit to SUCCESS or FAILED. In real life you might ping hosts (see test SSH / connectivity ideas); here the logic is stubbed so the transcript stays stable:

bash
SUCCESS=""
FAILED=""

for server in host_a host_b host_c; do
  case $server in
    host_a|host_c) SUCCESS+="$server ";;
    *)             FAILED+="$server ";;
  esac
done

echo "Reachable: $SUCCESS"
echo "Not Reachable: $FAILED"

Output:

text
Reachable: host_a host_c 
Not Reachable: host_b

Trailing spaces are deliberate gaps; trim with "${SUCCESS% }" or ${var%% } if the last space hurts later parsing. For word lists you often prefer a Bash array and join via printf—strings still work when the consumer is happy with spaces.


Newlines inside bash string concatenation

You can append a newline with $'\n' (ANSI-C quoting) instead of fighting echo -e:

bash
OUT=""
for word in ab cd ef; do
  OUT+="${word}"$'\n'
done
printf '%s' "$OUT"

Output:

text
ab
cd
ef

If you only need to print joined lines, printf '%s\n' ab cd ef is shorter; += is for when you genuinely build OUT across steps or a while loop.


printf for joining (often cleaner than manual +=)

When you already have fields, let printf insert separators:

bash
printf -v joined '%s|' a b c
echo "$joined"

Output:

text
a|b|c|

Drop the trailing delimiter with parameter expansion or build the format string once per field in a loop—either way it beats repeated sed to shave a trailing |.


Habits that save time

  • Quote expansions when building paths: path="${dir}/${name}.txt".
  • Prefer ${var} when punctuation touches the name.
  • For large lists, arrays plus printf avoid O(n²) string reshaping.

Summary

Most of the time you either place ${a}${b} next to each other, grow a buffer with +=, or format a list with printf. Watch ${name}_ when punctuation touches the variable name, trim deliberate trailing spaces if downstream code cares, and switch to arrays when the list gets long. That covers the usual bash concat string work in real scripts without reinventing a joiner every time.

Further reading:

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with over a decade of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, …

  • Red Hat Certified System Administrator in Red Hat OpenStack
  • Certified Kubernetes Application Developer (CKAD)
  • Red Hat Certified Specialist in Ansible Automation
  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security