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:
VAR1='Hello ' # trailing space on purpose
VAR2='World'
echo "${VAR1}${VAR2}"Output:
Hello WorldStore the same thing in another variable:
VAR3="${VAR1}${VAR2}"
echo "$VAR3"Same line:
Hello WorldBash append to string with +=
+= appends to a string (or grows an array when var+=(item)—different shape). For
plain concat string bash workflows:
s="a"
s+="b"
s+="c"
echo "$s"Output:
abcThat 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:
VAR1=Hello
VAR2=World
echo "unbraced: [$VAR1_$VAR2]"
echo "braced: [${VAR1}_${VAR2}]"Output:
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:
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:
Reachable: host_a host_c
Not Reachable: host_bTrailing 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:
OUT=""
for word in ab cd ef; do
OUT+="${word}"$'\n'
done
printf '%s' "$OUT"Output:
ab
cd
efIf 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:
printf -v joined '%s|' a b c
echo "$joined"Output:
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
printfavoid 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:

