Joining strings with separators is a common task that seems trivial until you need to handle spaces, special characters, or maintain portability across different Unix-like systems. The naive approaches often fail when dealing with real-world input.
Many quick solutions have limitations:
# Fails with spaces:
echo "foo bar baz" | sed 's/ /---/g'
# Output: foo---bar---baz (but breaks with input containing spaces)
# Problematic with special characters:
printf "%s---" foo "bar baz" quux
# Output: foo---bar baz---quux--- (extra trailing separator)
Using Parameter Expansion (Bash/Ksh)
For modern shells with array support:
join_by() {
local d=${1-} f=${2-}
if shift 2; then
printf %s "$f" "${@/#/$d}"
fi
}
join_by "---" foo "bar baz" quux
# Output: foo---bar baz---quux
POSIX-compliant IFS Approach
Works across most shells:
join_by() {
sep="$1"
shift
old_ifs="$IFS"
IFS="$sep"
result="$*"
IFS="$old_ifs"
printf '%s\n' "$result"
}
join_by "---" foo "bar baz" quux
# Output: foo---bar baz---quux
Using awk for Complex Cases
When dealing with special characters or large inputs:
join_by() {
awk -v sep="$1" 'BEGIN {
for (i=2; i2 ? sep : ""), ARGV[i]
}
print ""
}' "$@"
}
join_by "---" foo "bar baz" "qu'ux"
# Output: foo---bar baz---qu'ux
For large datasets (1000+ items), the awk solution is generally fastest, followed by the IFS approach. The parameter expansion method works well for moderate-sized arrays but may hit command-line length limits.
Method | Bash | Zsh | Ksh | Dash | POSIX |
---|---|---|---|---|---|
Parameter Expansion | Yes | Yes | Yes | No | No |
IFS Approach | Yes | Yes | Yes | Yes | Yes |
awk Solution | Yes | Yes | Yes | Yes | Yes |
All presented solutions properly handle:
- Strings containing spaces
- Empty strings
- Special characters (including quotes and backslashes)
- No trailing separators
When working with Unix shell scripts, joining strings with separators seems trivial until you need to handle:
- Strings containing spaces
- Special characters in input
- Portability across different shells
# The sed approach (breaks with spaces)
echo "foo bar baz" | sed 's/ /---/g'
# Output: foo---bar---baz
# IFS trick (limited to single character separators)
join_by() {
local IFS="$1"
shift
echo "$*"
}
join_by --- foo bar baz
# Output: foo---bar---baz
For handling all characters including spaces and special cases:
POSIX-compliant Function
strjoin() {
sep="$1"
shift
first="$1"
shift
printf "%s" "$first"
[ $# -gt 0 ] && printf "%s" "$sep"
printf "%s" "$@"
}
Using printf with Argument Shifting
join_strings() {
sep="$1"
shift
printf '%s' "$1"
shift
printf '%s' "${@/#/$sep}"
}
# Handling paths with spaces
join_by ":" "/path/with spaces" "/another/path" "/normal/path"
# Creating CSV from array
arr=("value 1" "value,2" "value\n3")
join_strings "," "${arr[@]}"
For large datasets (10,000+ items):
- Avoid subshells in loops
- Use built-in string operations when possible
- Consider awk for very large joins
# High-performance awk version
awk -v sep="---" '{for(i=1;i<=NF;i++){if(i>1)printf sep; printf $i} printf "\n"}'