explode() and implode() in Bash: Split and Join Strings
PHP's explode(",", $s) splits a string into an array and implode(",", $arr) joins it back together. Bash has arrays and a field splitter, so both jobs can be done in pure Bash. The built-in tools have some problems, though. IFS only knows single characters, and empty fields disappear in some cases but not others. Also, the shortest-looking version expands filenames.
Below are the built-in ways first, with their problems, then two functions that behave like PHP, including multi-character delimiters.
Splitting with IFS and read -ra
The standard idiom sets IFS just for one read command and reads the fields into an array with -a. -r stops backslashes being removed:
IFS=, read -ra parts <<< "red,green,blue"
declare -p partsdeclare -a parts=([0]="red" [1]="green" [2]="blue")Because IFS=, is a prefix assignment, it applies only to read, and the rest of the script keeps the normal IFS. This works in Bash 3.2 too. Now the problems, all run on Bash 4.4:
IFS=, read -ra parts <<< "a,,b," # empty and trailing fields
IFS=' ' read -ra parts <<< " a b " # whitespace delimiter
IFS='::' read -ra parts <<< "a::b:c" # "multi-character" delimiter
IFS=, read -ra parts <<< $'a,b\nc,d' # embedded newlinedeclare -a parts=([0]="a" [1]="" [2]="b")
declare -a parts=([0]="a" [1]="b")
declare -a parts=([0]="a" [1]="" [2]="b" [3]="c")
declare -a parts=([0]="a" [1]="b")- Trailing delimiter. An empty field in the middle is kept, but a single trailing empty field is dropped. PHP's
explode(",", "a,,b,")returns four elements.readgives three. - Whitespace is special. When the delimiter is a space, tab or newline, several in a row count as one separator and leading and trailing ones are ignored. Good for
psoutput, wrong if you wanted empty fields. - No multi-character delimiters.
IFS='::'does not mean "split on::". It means "split on:or:", soa::b:cgives an empty field and splitsb:cas well. LikewiseIFS=', 'splits on comma or space. - Only one line.
readstops at the first newline, so everything after it is silently lost. - Empty input gives an empty array, not one empty element.
Do not split with an unquoted expansion
The other common idiom is IFS=,; parts=($s). It splits, but the result of word splitting is then subject to filename expansion:
$ ls
file1 file2
$ s="x,*,y"
$ IFS=,; parts=($s); unset IFS
$ declare -p parts
declare -a parts=([0]="x" [1]="file1" [2]="file2" [3]="y")The * field turned into the names of the files in the current directory. You can wrap it in set -f and set +f, but you also have to restore IFS afterwards (unset IFS is not the same as restoring a custom value). read -ra never globs, so there is no reason to use this form.
Splitting with mapfile -d (Bash 4.4+)
mapfile (also called readarray) arrived in Bash 4.0, and the -d option for a custom delimiter in Bash 4.4. It reads records rather than fields. So it handles newlines inside fields and keeps trailing empty fields correctly. But you must give it the string without the extra newline that <<< adds:
mapfile -t -d , parts < <(printf '%s' "a,,b,")
declare -p parts
mapfile -t -d , parts < <(printf '%s' $'one\ntwo,three')
declare -p parts
mapfile -t -d , parts <<< "a,b"
declare -p partsdeclare -a parts=([0]="a" [1]="" [2]="b")
declare -a parts=([0]=$'one\ntwo' [1]="three")
declare -a parts=([0]="a" [1]=$'b\n')-t removes the delimiter from each element. The first result has no fourth element because the final comma ends the last record rather than starting a new one. The third shows the here-string problem: <<< appends a newline, which ends up inside the last element. Use printf '%s' with process substitution instead. mapfile -d is single-character too, and it is not available on the default Bash 3.2 on macOS.
The functions
When I want PHP behaviour, including multi-character delimiters and a trailing empty field, I use these:
# explode() -- split a string on a delimiter into an array
# Usage: explode arrayname delimiter string
# Needs Bash 4.3+ (local -n)
explode() {
[ $# -eq 3 ] && [ -n "$2" ] || return 1
local -n _explode_out="$1"
local delim="$2" s="$3"
_explode_out=()
while [[ $s == *"$delim"* ]]; do
_explode_out+=("${s%%"$delim"*}")
s="${s#*"$delim"}"
done
_explode_out+=("$s")
}
# implode() -- join arguments with a glue string
# Usage: implode glue item...
implode() {
[ $# -ge 1 ] || return 1
local glue="$1" out item
shift
out="${1-}"
[ $# -gt 0 ] && shift
for item in "$@"; do
out+="$glue$item"
done
printf '%s\n' "$out"
}How it works
explode uses a nameref (local -n, Bash 4.3+) so it can fill an array in the caller's scope. A function cannot return an array any other way without printing and re-parsing it. The loop runs while the delimiter is still in the string. Each pass takes everything before the first delimiter with ${s%%"$delim"*}, appends it, and drops that part plus the delimiter with ${s#*"$delim"}. Whatever is left at the end is the last field, even if it is empty. The delimiter is quoted inside the patterns so * or ? are matched literally.
The odd array name _explode_out is deliberate. If the caller passes a name that is the same as the nameref, Bash reports a circular reference, so the internal name should be one nobody would choose. An empty delimiter returns 1, as PHP refuses it too.
implode takes the glue and then the items as separate arguments, so it works with "${arr[@]}" and needs no namerefs. It runs on Bash 3.2. It starts with the first item and appends glue plus item for the rest. ${1-} keeps it safe under set -u when there are no items.
Usage
$ explode parts "::" "a::b:c::"; declare -p parts
declare -a parts=([0]="a" [1]="b:c" [2]="")
$ explode parts ", " "red, green, blue"; declare -p parts
declare -a parts=([0]="red" [1]="green" [2]="blue")
$ explode parts "*" "x*y"; declare -p parts
declare -a parts=([0]="x" [1]="y")
$ explode parts "," ""; declare -p parts
declare -a parts=([0]="")
$ arr=(red green blue)
$ implode ", " "${arr[@]}"
red, green, blue
$ implode " -> " a b c
a -> b -> c
$ explode p " | " "a | b | c"; implode " | " "${p[@]}"
a | b | cSplitting an empty string gives one empty element, which is what PHP does. All of this was run with set -u on. The loop copies the remaining string on each pass, so for very long strings with thousands of fields mapfile -d or awk is quicker.
Joining with "${arr[*]}"
For a single-character glue you do not need a function. Inside double quotes, "${arr[*]}" joins the elements with the first character of IFS:
arr=(red green blue)
( IFS=,; printf '%s\n' "${arr[*]}" )
( IFS=', '; printf '%s\n' "${arr[*]}" )red,green,blue
red,green,blueNote the second line: only the first character of IFS is used, so ', ' still gives plain commas. The parentheses run each line in a subshell so the changed IFS does not affect the rest of the script. IFS= (empty) joins with nothing at all. A prefix assignment like IFS=, printf ... "${arr[*]}" does not work, because the expansion happens before the assignment takes effect.
Another trick you will see is printf -v out "%s$glue" "${arr[@]}" followed by stripping the last glue. It works until the glue contains a %: with glue='%d' I got printf: green: invalid number and the result red0blue0. implode above does not put data in the format string, so it has no such problem.
The Bash builtins section of the manual covers read and mapfile, and explode() on php.net has the PHP original.
Related functions
- in_array(): check whether an array contains a value
- str_replace(): replace text in a string
- Reading a file line by line: splitting lines into fields
- trim(): clean up fields after splitting
- PHP string functions in Bash: the full index