916

In a Bash script, I would like to split a line into pieces and store them in an array.

For example, given the line:

Paris, France, Europe

I would like to have the resulting array to look like so:

array[0] = Paris
array[1] = France
array[2] = Europe

A simple implementation is preferable; speed does not matter. How can I do it?

ib.
  • 27,830
  • 11
  • 80
  • 100
Lgn
  • 9,581
  • 5
  • 20
  • 26
  • 65
    This is #1 Google hit but there's controversy in the answer because the question unfortunately asks about delimiting on `, ` (comma-space) and not a *single character* such as comma. If you're only interested in the latter, answers here are easier to follow: https://stackoverflow.com/questions/918886/how-do-i-split-a-string-on-a-delimiter-in-bash#tab-top – antak Jun 18 '18 at 09:22
  • 2
    If you want to munge a string and don't care about having it as an array, `cut` is a useful bash command to bear in mind too. Separator is definable https://en.wikibooks.org/wiki/Cut You can also extract out data from a fixed width record structure too. https://en.wikipedia.org/wiki/Cut_(Unix) https://www.computerhope.com/unix/ucut.htm – JGFMK Sep 30 '19 at 13:44

25 Answers25

1479
IFS=', ' read -r -a array <<< "$string"

Note that the characters in $IFS are treated individually as separators so that in this case fields may be separated by either a comma or a space rather than the sequence of the two characters. Interestingly though, empty fields aren't created when comma-space appears in the input because the space is treated specially.

To access an individual element:

echo "${array[0]}"

To iterate over the elements:

for element in "${array[@]}"
do
    echo "$element"
done

To get both the index and the value:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

The last example is useful because Bash arrays are sparse. In other words, you can delete an element or add an element and then the indices are not contiguous.

unset "array[1]"
array[42]=Earth

To get the number of elements in an array:

echo "${#array[@]}"

As mentioned above, arrays can be sparse so you shouldn't use the length to get the last element. Here's how you can in Bash 4.2 and later:

echo "${array[-1]}"

in any version of Bash (from somewhere after 2.05b):

echo "${array[@]: -1:1}"

Larger negative offsets select farther from the end of the array. Note the space before the minus sign in the older form. It is required.

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • How can I refer to the elements? – Lgn May 14 '12 at 15:20
  • 17
    Just use `IFS=', '`, then you don't have to remove the spaces separately. Test: `IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"` – l0b0 May 14 '12 at 15:24
  • 6
    @l0b0: Thanks. I don't know what I was thinking. I like to use `declare -p array` for test output, by the way. – Dennis Williamson May 14 '12 at 16:33
  • @DennisWilliamson: Nice one, added to `.bash_history`. – l0b0 May 15 '12 at 07:12
  • whats the way if I want to use multiple delimiters like , as well as ;? – Ram Dec 13 '12 at 15:09
  • @ram: Add more characters to the value of `IFS`. – Dennis Williamson Dec 15 '12 at 03:33
  • How about having the array's length? – Augustin Riedinger Apr 04 '14 at 12:40
  • Augustin-Riedinger: I've added that and a couple of other bits of information. Thanks. – Dennis Williamson Apr 04 '14 at 13:45
  • 1
    This doesn't seem to respect quotes. For example `France, Europe, "Congo, The Democratic Republic of the"` this will split after congo. – Yisrael Dov Sep 08 '14 at 08:21
  • 3
    @YisraelDov: Bash has no way to deal with CSV by itself. It can't tell the difference between commas inside quotes and those outside them. You will need to use a tool that understands CSV such as a lib in a higher level language, for example the [csv](https://docs.python.org/2/library/csv.html) module in Python. – Dennis Williamson Sep 08 '14 at 12:07
  • Thanks for this cool code. Is there anyway to place this in a function? When I do, I echo ${arr[@]} but in so doing, I may miss the cases where arr[x] may be a nul string. – Bitdiot Oct 02 '14 at 13:14
  • @Bitdiot: You may need to ask this as a separate question so you can include more detail about what you're trying to do. Yes, you can place this in a function. I don't know what you mean about missing cases where an array element is an empty string, since there's nothing to output. – Dennis Williamson Oct 06 '14 at 15:52
  • what if the string for tokenizing has wild cards? e.g. IFS='base*' – Jay D Aug 07 '15 at 03:50
  • @JayD: Each character is used individually, not as a sequence. The asterisk is just another character. – Dennis Williamson Aug 07 '15 at 04:32
  • `"$index ${array[index]}"` should be `"$index ${array[$index]}"`, no? – tetris11 Oct 15 '15 at 10:13
  • 1
    @tetris11: It can be either way. The array index is an arithmetic context where the dollar sign is not usually required. – Dennis Williamson Oct 15 '15 at 12:00
  • 1
    @caesarsol: Compare yours to `IFS=', ' read -a array <<< "a, d r s, w"` with spaces after the commas in the input. The result is the same (no empty array elements are created) because spaces are treated specially. Also compare inputs such as `"a, d r s,,w"` which does create an empty element and `"a, d r s w"` (multiple spaces between "s" and "w" - which don't show well here) which does not create any empty elements. Also, I commented regarding the characteristic of `IFS` you mentioned, earlier, on August 7, 2015, above. – Dennis Williamson Oct 29 '15 at 17:06
  • @DennisWilliamson that IFS will split also by spaces: the `d r s` words in my example should be a single array element. – caesarsol Oct 29 '15 at 18:20
  • @caesarsol: The `IFS` in your first comment includes a space. `declare -p array` is a good way to show an array's elements, btw. – Dennis Williamson Oct 29 '15 at 19:40
  • what if we have string like "/opt/test/dir" and want to split using "/"? i tried IFS='/' which is not working :( – Ganesh Sep 07 '16 at 06:41
  • @Ganesh: It works for me. How exactly are you doing it and how are you determining that it's not working? This is what I did with your example: `IFS='/' read -r -a array <<< "/opt/test/dir"; for d in "${array[@]}"; do echo "$d"; done` – Dennis Williamson Sep 07 '16 at 17:06
  • @DennisWilliamson thanks for the reply it worked, was my mistake. i was having string "/opt/test/dir" and spliting with "/". was trying to echo 0th element which :P – Ganesh Sep 08 '16 at 05:32
  • Great answer, thanks. A little note, if someone is using Bash < 4.2, `echo "$array[@]: -1:1}` is missing initial `{` and ending `"`, should be `echo "${array[@]: -1:1}"` – 244an Oct 21 '16 at 01:25
  • 11
    `str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"` will split into `array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")` as a note. So this only works with fields with no spaces since `IFS=', '` is a set of individual characters -- not a string delimiter. – dawg Nov 27 '17 at 04:57
  • @dawg As noted by caesarsol on 10/29/2015 above. I'll add the information to my answer. – Dennis Williamson Nov 27 '17 at 14:52
  • Any idea what's going wrong here? `s="a:b:c"; IFS=':' read -r -a array <<< $s; echo $array; echo ${array[1]}`. Expected output `a b ca`, actually got `a b c` – scubbo Mar 24 '18 at 18:54
  • 1
    @scubbo: I don't get what you got, I got `ab` which is what I would expect from your code which says "print the first element of the array; print the second element of the array". If you want your expected output: `s="a:b:c"; IFS=':' read -r -a array <<< "$s"; echo "${array[@]}"; echo "${array[0]}"` The reason for this is that a scalar reference to an array is the same as the first element of the array and array indices are zero-based. Note also that you should always use proper quoting (but that wasn't the issue for you). I have no idea how you got the output you showed... – Dennis Williamson Mar 24 '18 at 19:06
  • ...unless you didn't set `IFS=':'` but then the output would be `a:b:c`. – Dennis Williamson Mar 24 '18 at 19:07
  • Hmm - not sure why we got different results (maybe it's due to different shells? I got the error above in `sh` and `bash`, but in `zsh` I got `zsh: bad option: -a`), but your proposed solution works for me. Thanks! – scubbo Mar 24 '18 at 19:14
  • @DennisWilliamson Ohhh I found the difference. I need to quote-wrap `"$s"` to get the behaviour I expect. – scubbo Mar 24 '18 at 19:22
  • @scubbo: Copy and paste what you posted in your first message. I suspect that what you posted doesn't match what you're actually testing. – Dennis Williamson Mar 24 '18 at 22:57
  • @scubbo: OK, this is a bug that was fixed in Bash 4.3. – Dennis Williamson Mar 25 '18 at 23:49
  • There are many ways to make it work.The simple one use tr command it can work either MacOS and Linux. arrayData=($(echo "1.0.0.1 | tr "." "\n")) – sopheamak Oct 29 '19 at 02:35
  • I receive an error "read: bad option: -a" when I tried this – Patlatus Mar 30 '21 at 13:33
  • @Patlatus: You are probably using a shell (non-Bash) that doesn't support arrays (or using `read` to populate them). Bash has supported this for a very long time. – Dennis Williamson Jun 27 '22 at 12:53
660

All of the answers to this question are wrong in one way or another.


Wrong answer #1

IFS=', ' read -r -a array <<< "$string"

1: This is a misuse of $IFS. The value of the $IFS variable is not taken as a single variable-length string separator, rather it is taken as a set of single-character string separators, where each field that read splits off from the input line can be terminated by any character in the set (comma or space, in this example).

Actually, for the real sticklers out there, the full meaning of $IFS is slightly more involved. From the bash manual:

The shell treats each character of IFS as a delimiter, and splits the results of the other expansions into words using these characters as field terminators. If IFS is unset, or its value is exactly <space><tab><newline>, the default, then sequences of <space>, <tab>, and <newline> at the beginning and end of the results of the previous expansions are ignored, and any sequence of IFS characters not at the beginning or end serves to delimit words. If IFS has a value other than the default, then sequences of the whitespace characters <space>, <tab>, and <newline> are ignored at the beginning and end of the word, as long as the whitespace character is in the value of IFS (an IFS whitespace character). Any character in IFS that is not IFS whitespace, along with any adjacent IFS whitespace characters, delimits a field. A sequence of IFS whitespace characters is also treated as a delimiter. If the value of IFS is null, no word splitting occurs.

Basically, for non-default non-null values of $IFS, fields can be separated with either (1) a sequence of one or more characters that are all from the set of "IFS whitespace characters" (that is, whichever of <space>, <tab>, and <newline> ("newline" meaning line feed (LF)) are present anywhere in $IFS), or (2) any non-"IFS whitespace character" that's present in $IFS along with whatever "IFS whitespace characters" surround it in the input line.

For the OP, it's possible that the second separation mode I described in the previous paragraph is exactly what he wants for his input string, but we can be pretty confident that the first separation mode I described is not correct at all. For example, what if his input string was 'Los Angeles, United States, North America'?

IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")

2: Even if you were to use this solution with a single-character separator (such as a comma by itself, that is, with no following space or other baggage), if the value of the $string variable happens to contain any LFs, then read will stop processing once it encounters the first LF. The read builtin only processes one line per invocation. This is true even if you are piping or redirecting input only to the read statement, as we are doing in this example with the here-string mechanism, and thus unprocessed input is guaranteed to be lost. The code that powers the read builtin has no knowledge of the data flow within its containing command structure.

You could argue that this is unlikely to cause a problem, but still, it's a subtle hazard that should be avoided if possible. It is caused by the fact that the read builtin actually does two levels of input splitting: first into lines, then into fields. Since the OP only wants one level of splitting, this usage of the read builtin is not appropriate, and we should avoid it.

3: A non-obvious potential issue with this solution is that read always drops the trailing field if it is empty, although it preserves empty fields otherwise. Here's a demo:

string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")

Maybe the OP wouldn't care about this, but it's still a limitation worth knowing about. It reduces the robustness and generality of the solution.

This problem can be solved by appending a dummy trailing delimiter to the input string just prior to feeding it to read, as I will demonstrate later.


Wrong answer #2

string="1:2:3:4:5"
set -f                     # avoid globbing (expansion of *).
array=(${string//:/ })

Similar idea:

t="one,two,three"
a=($(echo $t | tr ',' "\n"))

(Note: I added the missing parentheses around the command substitution which the answerer seems to have omitted.)

Similar idea:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)

These solutions leverage word splitting in an array assignment to split the string into fields. Funnily enough, just like read, general word splitting also uses the $IFS special variable, although in this case it is implied that it is set to its default value of <space><tab><newline>, and therefore any sequence of one or more IFS characters (which are all whitespace characters now) is considered to be a field delimiter.

This solves the problem of two levels of splitting committed by read, since word splitting by itself constitutes only one level of splitting. But just as before, the problem here is that the individual fields in the input string can already contain $IFS characters, and thus they would be improperly split during the word splitting operation. This happens to not be the case for any of the sample input strings provided by these answerers (how convenient...), but of course that doesn't change the fact that any code base that used this idiom would then run the risk of blowing up if this assumption were ever violated at some point down the line. Once again, consider my counterexample of 'Los Angeles, United States, North America' (or 'Los Angeles:United States:North America').

Also, word splitting is normally followed by filename expansion (aka pathname expansion aka globbing), which, if done, would potentially corrupt words containing the characters *, ?, or [ followed by ] (and, if extglob is set, parenthesized fragments preceded by ?, *, +, @, or !) by matching them against file system objects and expanding the words ("globs") accordingly. The first of these three answerers has cleverly undercut this problem by running set -f beforehand to disable globbing. Technically this works (although you should probably add set +f afterward to reenable globbing for subsequent code which may depend on it), but it's undesirable to have to mess with global shell settings in order to hack a basic string-to-array parsing operation in local code.

Another issue with this answer is that all empty fields will be lost. This may or may not be a problem, depending on the application.

Note: If you're going to use this solution, it's better to use the ${string//:/ } "pattern substitution" form of parameter expansion, rather than going to the trouble of invoking a command substitution (which forks the shell), starting up a pipeline, and running an external executable (tr or sed), since parameter expansion is purely a shell-internal operation. (Also, for the tr and sed solutions, the input variable should be double-quoted inside the command substitution; otherwise word splitting would take effect in the echo command and potentially mess with the field values. Also, the $(...) form of command substitution is preferable to the old `...` form since it simplifies nesting of command substitutions and allows for better syntax highlighting by text editors.)


Wrong answer #3

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

This answer is almost the same as #2. The difference is that the answerer has made the assumption that the fields are delimited by two characters, one of which being represented in the default $IFS, and the other not. He has solved this rather specific case by removing the non-IFS-represented character using a pattern substitution expansion and then using word splitting to split the fields on the surviving IFS-represented delimiter character.

This is not a very generic solution. Furthermore, it can be argued that the comma is really the "primary" delimiter character here, and that stripping it and then depending on the space character for field splitting is simply wrong. Once again, consider my counterexample: 'Los Angeles, United States, North America'.

Also, again, filename expansion could corrupt the expanded words, but this can be prevented by temporarily disabling globbing for the assignment with set -f and then set +f.

Also, again, all empty fields will be lost, which may or may not be a problem depending on the application.


Wrong answer #4

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

This is similar to #2 and #3 in that it uses word splitting to get the job done, only now the code explicitly sets $IFS to contain only the single-character field delimiter present in the input string. It should be repeated that this cannot work for multicharacter field delimiters such as the OP's comma-space delimiter. But for a single-character delimiter like the LF used in this example, it actually comes close to being perfect. The fields cannot be unintentionally split in the middle as we saw with previous wrong answers, and there is only one level of splitting, as required.

One problem is that filename expansion will corrupt affected words as described earlier, although once again this can be solved by wrapping the critical statement in set -f and set +f.

Another potential problem is that, since LF qualifies as an "IFS whitespace character" as defined earlier, all empty fields will be lost, just as in #2 and #3. This would of course not be a problem if the delimiter happens to be a non-"IFS whitespace character", and depending on the application it may not matter anyway, but it does vitiate the generality of the solution.

So, to sum up, assuming you have a one-character delimiter, and it is either a non-"IFS whitespace character" or you don't care about empty fields, and you wrap the critical statement in set -f and set +f, then this solution works, but otherwise not.

(Also, for information's sake, assigning a LF to a variable in bash can be done more easily with the $'...' syntax, e.g. IFS=$'\n';.)


Wrong answer #5

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

Similar idea:

IFS=', ' eval 'array=($string)'

This solution is effectively a cross between #1 (in that it sets $IFS to comma-space) and #2-4 (in that it uses word splitting to split the string into fields). Because of this, it suffers from most of the problems that afflict all of the above wrong answers, sort of like the worst of all worlds.

Also, regarding the second variant, it may seem like the eval call is completely unnecessary, since its argument is a single-quoted string literal, and therefore is statically known. But there's actually a very non-obvious benefit to using eval in this way. Normally, when you run a simple command which consists of a variable assignment only, meaning without an actual command word following it, the assignment takes effect in the shell environment:

IFS=', '; ## changes $IFS in the shell environment

This is true even if the simple command involves multiple variable assignments; again, as long as there's no command word, all variable assignments affect the shell environment:

IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment

But, if the variable assignment is attached to a command name (I like to call this a "prefix assignment") then it does not affect the shell environment, and instead only affects the environment of the executed command, regardless whether it is a builtin or external:

IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it

Relevant quote from the bash manual:

If no command name results, the variable assignments affect the current shell environment. Otherwise, the variables are added to the environment of the executed command and do not affect the current shell environment.

It is possible to exploit this feature of variable assignment to change $IFS only temporarily, which allows us to avoid the whole save-and-restore gambit like that which is being done with the $OIFS variable in the first variant. But the challenge we face here is that the command we need to run is itself a mere variable assignment, and hence it would not involve a command word to make the $IFS assignment temporary. You might think to yourself, well why not just add a no-op command word to the statement like the : builtin to make the $IFS assignment temporary? This does not work because it would then make the $array assignment temporary as well:

IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command

So, we're effectively at an impasse, a bit of a catch-22. But, when eval runs its code, it runs it in the shell environment, as if it was normal, static source code, and therefore we can run the $array assignment inside the eval argument to have it take effect in the shell environment, while the $IFS prefix assignment that is prefixed to the eval command will not outlive the eval command. This is exactly the trick that is being used in the second variant of this solution:

IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does

So, as you can see, it's actually quite a clever trick, and accomplishes exactly what is required (at least with respect to assignment effectation) in a rather non-obvious way. I'm actually not against this trick in general, despite the involvement of eval; just be careful to single-quote the argument string to guard against security threats.

But again, because of the "worst of all worlds" agglomeration of problems, this is still a wrong answer to the OP's requirement.


Wrong answer #6

IFS=', '; array=(Paris, France, Europe)

IFS=' ';declare -a array=(Paris France Europe)

Um... what? The OP has a string variable that needs to be parsed into an array. This "answer" starts with the verbatim contents of the input string pasted into an array literal. I guess that's one way to do it.

It looks like the answerer may have assumed that the $IFS variable affects all bash parsing in all contexts, which is not true. From the bash manual:

IFS    The Internal Field Separator that is used for word splitting after expansion and to split lines into words with the read builtin command. The default value is <space><tab><newline>.

So the $IFS special variable is actually only used in two contexts: (1) word splitting that is performed after expansion (meaning not when parsing bash source code) and (2) for splitting input lines into words by the read builtin.

Let me try to make this clearer. I think it might be good to draw a distinction between parsing and execution. Bash must first parse the source code, which obviously is a parsing event, and then later it executes the code, which is when expansion comes into the picture. Expansion is really an execution event. Furthermore, I take issue with the description of the $IFS variable that I just quoted above; rather than saying that word splitting is performed after expansion, I would say that word splitting is performed during expansion, or, perhaps even more precisely, word splitting is part of the expansion process. The phrase "word splitting" refers only to this step of expansion; it should never be used to refer to the parsing of bash source code, although unfortunately the docs do seem to throw around the words "split" and "words" a lot. Here's a relevant excerpt from the linux.die.net version of the bash manual:

Expansion is performed on the command line after it has been split into words. There are seven kinds of expansion performed: brace expansion, tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, word splitting, and pathname expansion.

The order of expansions is: brace expansion; tilde expansion, parameter and variable expansion, arithmetic expansion, and command substitution (done in a left-to-right fashion); word splitting; and pathname expansion.

You could argue the GNU version of the manual does slightly better, since it opts for the word "tokens" instead of "words" in the first sentence of the Expansion section:

Expansion is performed on the command line after it has been split into tokens.

The important point is, $IFS does not change the way bash parses source code. Parsing of bash source code is actually a very complex process that involves recognition of the various elements of shell grammar, such as command sequences, command lists, pipelines, parameter expansions, arithmetic substitutions, and command substitutions. For the most part, the bash parsing process cannot be altered by user-level actions like variable assignments (actually, there are some minor exceptions to this rule; for example, see the various compatxx shell settings, which can change certain aspects of parsing behavior on-the-fly). The upstream "words"/"tokens" that result from this complex parsing process are then expanded according to the general process of "expansion" as broken down in the above documentation excerpts, where word splitting of the expanded (expanding?) text into downstream words is simply one step of that process. Word splitting only touches text that has been spit out of a preceding expansion step; it does not affect literal text that was parsed right off the source bytestream.


Wrong answer #7

string='first line
        second line
        third line'

while read -r line; do lines+=("$line"); done <<<"$string"

This is one of the best solutions. Notice that we're back to using read. Didn't I say earlier that read is inappropriate because it performs two levels of splitting, when we only need one? The trick here is that you can call read in such a way that it effectively only does one level of splitting, specifically by splitting off only one field per invocation, which necessitates the cost of having to call it repeatedly in a loop. It's a bit of a sleight of hand, but it works.

But there are problems. First: When you provide at least one NAME argument to read, it automatically ignores leading and trailing whitespace in each field that is split off from the input string. This occurs whether $IFS is set to its default value or not, as described earlier in this post. Now, the OP may not care about this for his specific use-case, and in fact, it may be a desirable feature of the parsing behavior. But not everyone who wants to parse a string into fields will want this. There is a solution, however: A somewhat non-obvious usage of read is to pass zero NAME arguments. In this case, read will store the entire input line that it gets from the input stream in a variable named $REPLY, and, as a bonus, it does not strip leading and trailing whitespace from the value. This is a very robust usage of read which I've exploited frequently in my shell programming career. Here's a demonstration of the difference in behavior:

string=$'  a  b  \n  c  d  \n  e  f  '; ## input string

a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a  b" [1]="c  d" [2]="e  f") ## read trimmed surrounding whitespace

a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="  a  b  " [1]="  c  d  " [2]="  e  f  ") ## no trimming

The second issue with this solution is that it does not actually address the case of a custom field separator, such as the OP's comma-space. As before, multicharacter separators are not supported, which is an unfortunate limitation of this solution. We could try to at least split on comma by specifying the separator to the -d option, but look what happens:

string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")

Predictably, the unaccounted surrounding whitespace got pulled into the field values, and hence this would have to be corrected subsequently through trimming operations (this could also be done directly in the while-loop). But there's another obvious error: Europe is missing! What happened to it? The answer is that read returns a failing return code if it hits end-of-file (in this case we can call it end-of-string) without encountering a final field terminator on the final field. This causes the while-loop to break prematurely and we lose the final field.

Technically this same error afflicted the previous examples as well; the difference there is that the field separator was taken to be LF, which is the default when you don't specify the -d option, and the <<< ("here-string") mechanism automatically appends a LF to the string just before it feeds it as input to the command. Hence, in those cases, we sort of accidentally solved the problem of a dropped final field by unwittingly appending an additional dummy terminator to the input. Let's call this solution the "dummy-terminator" solution. We can apply the dummy-terminator solution manually for any custom delimiter by concatenating it against the input string ourselves when instantiating it in the here-string:

a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

There, problem solved. Another solution is to only break the while-loop if both (1) read returned failure and (2) $REPLY is empty, meaning read was not able to read any characters prior to hitting end-of-file. Demo:

a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

This approach also reveals the secretive LF that automatically gets appended to the here-string by the <<< redirection operator. It could of course be stripped off separately through an explicit trimming operation as described a moment ago, but obviously the manual dummy-terminator approach solves it directly, so we could just go with that. The manual dummy-terminator solution is actually quite convenient in that it solves both of these two problems (the dropped-final-field problem and the appended-LF problem) in one go.

So, overall, this is quite a powerful solution. It's only remaining weakness is a lack of support for multicharacter delimiters, which I will address later.


Wrong answer #8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(This is actually from the same post as #7; the answerer provided two solutions in the same post.)

The readarray builtin, which is a synonym for mapfile, is ideal. It's a builtin command which parses a bytestream into an array variable in one shot; no messing with loops, conditionals, substitutions, or anything else. And it doesn't surreptitiously strip any whitespace from the input string. And (if -O is not given) it conveniently clears the target array before assigning to it. But it's still not perfect, hence my criticism of it as a "wrong answer".

First, just to get this out of the way, note that, just like the behavior of read when doing field-parsing, readarray drops the trailing field if it is empty. Again, this is probably not a concern for the OP, but it could be for some use-cases. I'll come back to this in a moment.

Second, as before, it does not support multicharacter delimiters. I'll give a fix for this in a moment as well.

Third, the solution as written does not parse the OP's input string, and in fact, it cannot be used as-is to parse it. I'll expand on this momentarily as well.

For the above reasons, I still consider this to be a "wrong answer" to the OP's question. Below I'll give what I consider to be the right answer.


Right answer

Here's a naïve attempt to make #8 work by just specifying the -d option:

string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

We see the result is identical to the result we got from the double-conditional approach of the looping read solution discussed in #7. We can almost solve this with the manual dummy-terminator trick:

readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')

The problem here is that readarray preserved the trailing field, since the <<< redirection operator appended the LF to the input string, and therefore the trailing field was not empty (otherwise it would've been dropped). We can take care of this by explicitly unsetting the final array element after-the-fact:

readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

The only two problems that remain, which are actually related, are (1) the extraneous whitespace that needs to be trimmed, and (2) the lack of support for multicharacter delimiters.

The whitespace could of course be trimmed afterward (for example, see How to trim whitespace from a Bash variable?). But if we can hack a multicharacter delimiter, then that would solve both problems in one shot.

Unfortunately, there's no direct way to get a multicharacter delimiter to work. The best solution I've thought of is to preprocess the input string to replace the multicharacter delimiter with a single-character delimiter that will be guaranteed not to collide with the contents of the input string. The only character that has this guarantee is the NUL byte. This is because, in bash (though not in zsh, incidentally), variables cannot contain the NUL byte. This preprocessing step can be done inline in a process substitution. Here's how to do it using awk:

readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

There, finally! This solution will not erroneously split fields in the middle, will not cut out prematurely, will not drop empty fields, will not corrupt itself on filename expansions, will not automatically strip leading and trailing whitespace, will not leave a stowaway LF on the end, does not require loops, and does not settle for a single-character delimiter.


Trimming solution

Lastly, I wanted to demonstrate my own fairly intricate trimming solution using the obscure -C callback option of readarray. Unfortunately, I've run out of room against Stack Overflow's draconian 30,000 character post limit, so I won't be able to explain it. I'll leave that as an exercise for the reader.

function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
bgoldst
  • 34,190
  • 6
  • 38
  • 64
  • 18
    It may also be helpful to note (though understandably you had no room to do so) that the `-d` option to `readarray` first appears in Bash 4.4. – fbicknel Aug 18 '17 at 15:57
  • 5
    Great answer (+1). If you change your awk to `awk '{ gsub(/,[ ]+|$/,"\0"); print }'` and eliminate that concatenation of the final `", "` then you don't have to go through the gymnastics on eliminating the final record. So: `readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")` on Bash that supports `readarray`. Note your method is Bash 4.4+ I think because of the `-d` in `readarray` – dawg Nov 26 '17 at 22:28
  • You state: *Unfortunately, there's no direct way to get a multicharacter delimiter to work.* There is actually directly in Bash: You can use a regex similar to what you would use in `sed` or `awk`. An example [in my answer](https://stackoverflow.com/a/47500443/298607). – dawg Nov 26 '17 at 22:31
  • @dawg Very cool solution! See my comments and edit to your answer. – bgoldst Nov 27 '17 at 04:33
  • 3
    @datUser That's unfortunate. Your version of bash must be too old for `readarray`. In this case, you can use the second-best solution built on `read`. I'm referring to this: `a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";` (with the `awk` substitution if you need multicharacter delimiter support). Let me know if you run into any problems; I'm pretty sure this solution should work on fairly old versions of bash, back to version 2-something, released like two decades ago. – bgoldst Feb 23 '18 at 03:37
  • 16
    Wow, what a brilliant answer! Hee hee, my response: ditched the bash script and fired up python! – artfulrobot May 14 '18 at 11:32
  • 2
    @datUser bash on OSX is still stuck at 3.2 (released ca. 2007); I've used the bash found in Homebrew to get 4.X bash versions on OS X – JDS May 14 '18 at 16:15
  • 2
    readarray: -d: invalid option, are you sure your answer is right? – Terry Lin May 22 '18 at 16:04
  • What exactly do I have to change if I do not want ", " but "|" as a delimiter? – Code ninetyninepointnine Jun 17 '18 at 09:06
  • 14
    I'd move your right answers up to the top, I had to scroll through a lot of rubbish to find out how to do it properly :-) – paxdiablo Jan 09 '20 at 12:31
  • 66
    This is exactly the kind of thing that will convince you to never code in bash. An astoundingly simple task that has 8 incorrect solutions. Btw, this is without a design constraint of, "Make it as obscure and finicky as possible" – Connor Feb 18 '20 at 18:57
  • 3
    I've heard Editing is the bulk of writing. While I appreciate the thoroughness, if you could have cut this down to < 5K characters and focused on the answer it'd be a better answer than it is. – RLZaleski Aug 18 '21 at 15:56
  • 2
    @dawg The solution without unset (`readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")`) causes the last array element to have a trailing newline on my system (Bash 5.0) – Marcus Feb 20 '22 at 19:59
  • 2
    I will use this : `readarray -td, a < <(printf "%s" "$string"); declare -p a` no trailing `\n` hth – Boop Mar 22 '22 at 14:43
  • @Marcus this is true as awk its `print` statement adds an `ORS` at the end which by default is the -character – kvantour Jul 07 '22 at 12:33
  • @bgoldst Your "Right answer" implemented with gawk only works correctly for some delimiters, so arguably is wrong :-) For example string=`a-` and delimiter=`--` should result in `a-` but your code outputs `a` – jhnc Aug 03 '22 at 18:54
  • @jhnc It is logically incorrect in the first place to select a multicharacter delimiter to delimit values whose final character(s), when concatenated with the subsequent delimiter, produce a spurious delimiter, `--` in your example. My answer is fine. – bgoldst Aug 03 '22 at 19:39
  • @fbicknel, I though you were being facetious when you said "It may also be helpful to note (though understandably you had no room to do so)," then I read the end of bgoldst's post "Unfortunately, I've run out of room against Stack Overflow's draconian 30,000 character post limit..." Extremely informative, bgoldst. Thank you. – Jonathan Wheeler Sep 16 '22 at 13:31
  • One tiny thing. What if my separator contains the '/' character, or any other special character that would be misinterpreted by bash or awk? E.g. I want to split "I//am//your//nightmare" into an array of ["I", "am", "your", "nightmare"]? How to rewrite this so that instead it separates by what's in the variable $SEP? – aphid Nov 02 '22 at 10:39
258

Here is a way without setting IFS:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

The idea is using string replacement:

${string//substring/replacement}

to replace all matches of $substring with white space and then using the substituted string to initialize a array:

(element1 element2 ... elementN)

Note: this answer makes use of the split+glob operator. Thus, to prevent expansion of some characters (such as *) it is a good idea to pause globbing for this script.

Community
  • 1
  • 1
Jim Ho
  • 2,772
  • 1
  • 13
  • 5
  • 1
    Used this approach... until I came across a long string to split. 100% CPU for more than a minute (then I killed it). It's a pity because this method allows to split by a string, not some character in IFS. – Werner Lehmann May 04 '13 at 22:32
  • 100% CPU time for one than a minute sounds to me like there must be something wrong somewhere. Just how long was that string, is it MB or GB sized ? I think, normally, if you're just going to need a small string split, you want to stay within Bash, but if it's a huge file, I would execute something like Perl to do it. –  Apr 14 '14 at 11:32
  • 13
    WARNING: Just ran into a problem with this approach. If you have an element named * you will get all the elements of your cwd as well. thus string="1:2:3:4:*" will give some unexpected and possibly dangerous results depending on your implementation. Did not get the same error with (IFS=', ' read -a array <<< "$string") and this one seems safe to use. – Dieter Gribnitz Sep 02 '14 at 15:46
  • works in basic shell, while the other answer does not. Thanks! – MrE May 20 '16 at 06:17
  • 4
    quoting `${string//:/ }` prevents shell expansion – Andrew White Jun 01 '16 at 11:44
  • 1
    I had to use the following on OSX: `array=(${string//:/ })` – Mark Thomson Jun 05 '16 at 20:44
  • As with @Markouver, I did not get the expected result directly following the answer, and instead had to use an unquoted version. – Derrell Durrett Jun 06 '16 at 20:39
  • @AndrewWhite The edit to add quoting prevents shell splitting and breaks the answer. It is still vulnerable to expansion of * and related issues. –  Jun 18 '16 at 02:09
  • This answer is based on splitting by the shell, it would be wise to avoid globbing expansion to avoid expansion of an `*`. Answer edited accordingly. –  Jun 18 '16 at 02:11
  • @BinaryZebra fair enough. The answer as it stood certainly didn't work for me due to glob expansion and quoting it resolved that problem. Sorry if I was mistaken. – Andrew White Jun 18 '16 at 22:26
  • I like this a lot better than mucking with IFS. And tons faster than 'split'. – Keith Tyler Mar 06 '17 at 22:18
  • This worked for me when IFS barfed on me trying to process a string that had a "/" as a separator. (Yes, I even tried to escape it.) Thanks! – ingernet Dec 18 '17 at 23:35
  • Excellent, I love this non-IFS solution! – matthewcummings516 Jan 15 '18 at 23:19
  • unbeknownst to everyone here the splitting is still using `IFS`. It's just using the default value for `IFS` is the space, tab, new-line characters. – Trevor Boyd Smith Nov 13 '18 at 13:25
  • @Jim What if elements in the `string` contain space? – Putnik Apr 04 '19 at 19:46
149
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

Prints three

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
Jmoney38
  • 3,096
  • 2
  • 25
  • 26
  • 17
    I actually prefer this approach. Simple. – shrimpwagon Oct 16 '15 at 20:04
  • 4
    I copied and pasted this and it did did not work with echo, but did work when I used it in a for loop. – Ben Oct 31 '15 at 03:11
  • 2
    This does not work as stated. @Jmoney38 or shrimpwagon if you can paste this in a terminal and get the desired output, please paste the result here. – abalter Aug 30 '16 at 05:13
  • 2
    @abalter Works for me with `a=($(echo $t | tr ',' "\n"))`. Same result with `a=($(echo $t | tr ',' ' '))`. –  Jul 17 '17 at 16:28
  • @procrastinator I just tried it in`VERSION="16.04.2 LTS (Xenial Xerus)"` in a `bash` shell, and the last `echo` just prints a blank line. What version of Linux and which shell are you using? Unfortunately, can't display terminal session in a comment. – abalter Jul 25 '17 at 18:15
  • I rolled it back to the original, which worked. The edit on 10/16/2015 broke it. Note that backticks are deprecated and `$()` is preferred. The person who made the edit just put that correction in the wrong place. Also, the variables should be quoted. I've made an edit to reflect these changes. – Dennis Williamson Nov 06 '17 at 17:50
  • Works smoothly on Ubuntu Trusty! The simplest and neat answer. – Binita Bharati May 21 '18 at 07:01
  • Although it doesn't use the values from the original question, this is the best solution. It's clean and doesn't involve IFS. _And yes, it will work with original question if `','` is replaced with `', '`_ – Mike Nov 12 '19 at 10:57
  • Was this one of the ["wrong answers"](https://stackoverflow.com/a/45201229/605356)? (Said post may be thorough, but it's long enough without a summary to where I'm unable to currently read it; no offense intended.) Regardless, it seems to work nicely for me. – Johnny Utahh May 18 '20 at 19:34
  • This is a simple solution but unfortunately doesn't supports strings with spaces. – Diogo Cardoso Oct 01 '20 at 14:15
  • One liner for iterating: `for x in ${a[@]}; do echo $x; done` – Mandera Oct 21 '20 at 11:38
  • simple and works beautifully. I know there are many edge cases one could consider, but I feel it's much more productive to go with the method with minimum amount of code, filtering out the edge cases beforehand if necessary. – taiyodayo Jul 12 '21 at 02:59
32

Sometimes it happened to me that the method described in the accepted answer didn't work, especially if the separator is a carriage return.
In those cases I solved in this way:

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

for line in "${lines[@]}"
    do
        echo "--> $line"
done
Luca Borrione
  • 16,324
  • 8
  • 52
  • 66
  • 3
    +1 This completely worked for me. I needed to put multiple strings, divided by a newline, into an array, and `read -a arr <<< "$strings"` did not work with `IFS=$'\n'`. – Stefan van den Akker Feb 09 '15 at 16:52
  • 5
    [Here is the answer to make the accepted answer work when the delimiter is a newline](https://stackoverflow.com/questions/28417414/can-i-put-strings-divided-by-newlines-into-an-array-using-the-read-builtin). – Stefan van den Akker Feb 10 '15 at 13:49
  • This doesn't quite answer the original question. – Mike Nov 12 '19 at 10:58
32

The accepted answer works for values in one line.
If the variable has several lines:

string='first line
        second line
        third line'

We need a very different command to get all lines:

while read -r line; do lines+=("$line"); done <<<"$string"

Or the much simpler bash readarray:

readarray -t lines <<<"$string"

Printing all lines is very easy taking advantage of a printf feature:

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]
  • 2
    While not every solution works for every situation, your mention of readarray... replaced my last two hours with 5 minutes... you got my vote – Angry 84 Dec 31 '15 at 03:13
  • That `while read` loop will not produce the shown output as it'll strip leading/trailing white space. It'd need to be `while IFS= read -r line; do lines+=("$line"); done <<<"$string"` to produce the shown output. – Ed Morton Jul 04 '22 at 15:47
21

if you use macOS and can't use readarray, you can simply do this-

MY_STRING="string1 string2 string3"
array=($MY_STRING)

To iterate over the elements:

for element in "${array[@]}"
do
    echo $element
done
Romi Erez
  • 477
  • 4
  • 5
18

This works for me on OSX:

string="1 2 3 4 5"
declare -a array=($string)

If your string has different delimiter, just 1st replace those with space:

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

Simple :-)

To Kra
  • 3,344
  • 3
  • 38
  • 45
12

This is similar to the approach by Jmoney38, but using sed:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}

Prints 1

Stunner
  • 12,025
  • 12
  • 86
  • 145
ssanch
  • 389
  • 2
  • 6
  • 2
    it prints 1 2 3 4 in my case – minigeek Nov 07 '19 at 10:42
  • 3
    This basically just cribs the `tr` answer and makes it worse. Now a more complex tool is involved with a more complex syntax and regular expressions. Moreover, the modern `$()` syntax in the original has been replaced by the obsolescent backticks. – Kaz Sep 27 '21 at 16:15
12

The key to splitting your string into an array is the multi character delimiter of ", ". Any solution using IFS for multi character delimiters is inherently wrong since IFS is a set of those characters, not a string.

If you assign IFS=", " then the string will break on EITHER "," OR " " or any combination of them which is not an accurate representation of the two character delimiter of ", ".

You can use awk or sed to split the string, with process substitution:

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

It is more efficient to use a regex you directly in Bash:

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

With the second form, there is no sub shell and it will be inherently faster.


Edit by bgoldst: Here are some benchmarks comparing my readarray solution to dawg's regex solution, and I also included the read solution for the heck of it (note: I slightly modified the regex solution for greater harmony with my solution) (also see my comments below the post):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##
bgoldst
  • 34,190
  • 6
  • 38
  • 64
dawg
  • 98,345
  • 23
  • 131
  • 206
  • 1
    Very cool solution! I never thought of using a loop on a regex match, nifty use of `$BASH_REMATCH`. It works, and does indeed avoid spawning subshells. +1 from me. However, by way of criticism, the regex itself is a little non-ideal, in that it appears you were forced to duplicate part of the delimiter token (specifically the comma) so as to work around the lack of support for non-greedy multipliers (also lookarounds) in ERE ("extended" regex flavor built into bash). This makes it a little less generic and robust. – bgoldst Nov 27 '17 at 04:28
  • 1
    Secondly, I did some benchmarking, and although the performance is better than the other solutions for smallish strings, it worsens exponentially due to the repeated string-rebuilding, becoming catastrophic for very large strings. See my edit to your answer. – bgoldst Nov 27 '17 at 04:28
  • 1
    @bgoldst: What a cool benchmark! In defense of the regex, for 10's or 100's of thousands of fields (what the regex is splitting) there would probably be some form of record (like `\n` delimited text lines) comprising those fields so the catastrophic slow-down would likely not occur. If you have a string with 100,000 fields -- maybe Bash is not ideal ;-) Thanks for the benchmark. I learned a thing or two. – dawg Nov 27 '17 at 04:46
  • 1
    Echoing some of comments for the @bgoldst answer, `c_readarray` won't work for pre v4.4 Bash. `c_read` and `c_regex` work just fine. Where might you find such 'old' Bash version you ask?? In distros like RHEL7.9, I tells ya. – goldfishalpha Oct 14 '21 at 15:40
  • I couldn't see how to generalise your answer, as bash doesn't have ungreedy regex, but I've come up with a method using pattern substitution which seems to run at a comparable speed to the @bgoldst gawk up to ~256k characters, as long as delimiter is short (<10 or so). https://stackoverflow.com/a/73225463/10971581 – jhnc Aug 08 '22 at 21:51
11

I was curious about the relative performance of the "Right answer" in the popular answer by @bgoldst, with its apparent decrying of loops, so I have done a simple benchmark of it against three pure bash implementations.

In summary, I suggest:

  1. for string length < 4k or so, pure bash is faster than gawk
  2. for delimiter length < 10 and string length < 256k, pure bash is comparable to gawk
  3. for delimiter length >> 10 and string length < 64k or so, pure bash is "acceptable"; and gawk is less than 5x faster
  4. for string length < 512k or so, gawk is "acceptable"

I arbitrarily define "acceptable" as "takes < 0.5s to split the string".


I am taking the problem to be to take a bash string and split it into a bash array, using an arbitrary-length delimiter string (not regex).

# in: $1=delim, $2=string
# out: sets array a

My pure bash implementations are:

# naive approach - slow
split_byStr_bash_naive(){
    a=()
    local prev=""
    local cdr="$2"
    [[ -z "${cdr}" ]] && a+=("")
    while [[ "$cdr" != "$prev" ]]; do
        prev="$cdr"
        a+=( "${cdr%%"$1"*}" )
        cdr="${cdr#*"$1"}"
    done
    # echo $( declare -p a | md5sum; declare -p a )
}
# use lengths wherever possible - faster
split_byStr_bash_faster(){
    a=()
    local car=""
    local cdr="$2"
    while
        car="${cdr%%"$1"*}"
        a+=("$car")
        cdr="${cdr:${#car}}"
        (( ${#cdr} ))
    do
        cdr="${cdr:${#1}}"
    done
    # echo $( declare -p a | md5sum; declare -p a )
}
# use pattern substitution and readarray - fastest
split_byStr_bash_sub(){
        a=()
        local delim="$1" string="$2"

        delim="${delim//=/=-}"
        delim="${delim//$'\n'/=n}"

        string="${string//=/=-}"
        string="${string//$'\n'/=n}"

        readarray -td $'\n' a <<<"${string//"$delim"/$'\n'}"

        local len=${#a[@]} i s
        for (( i=0; i<len; i++ )); do
                s="${a[$i]//=n/$'\n'}"
                a[$i]="${s//=-/=}"
        done
        # echo $( declare -p a | md5sum; declare -p a )
}

The initial -z test in in the naive version handles the case of a zero-length string being passed. Without the test, the output array is empty; with it, the array has a single zero-length element.

Replacing readarray with while read gives < 10% slowdown.


This is the gawk implementation I used:

split_byRE_gawk(){
    readarray -td '' a < <(awk '{gsub(/'"$1"'/,"\0")}1' <<<"$2$1")
    unset 'a[-1]'
    # echo $( declare -p a | md5sum; declare -p a )
}

Obviously, in the general case, the delim argument will need to be sanitised, as gawk expects a regex, and gawk-special characters could cause problems. Also, as-is, the implementation won't correctly handle newlines in the delimiter.

Since gawk is being used, a generalised version that handles more arbitrary delimiters could be:

split_byREorStr_gawk(){
    local delim=$1
    local string=$2
    local useRegex=${3:+1}  # if set, delimiter is regex

    readarray -td '' a < <(
        export delim
        gawk -v re="$useRegex" '
            BEGIN {
                RS = FS = "\0"
                ORS = ""
                d = ENVIRON["delim"]

                # cf. https://stackoverflow.com/a/37039138
                if (!re) gsub(/[\\.^$(){}\[\]|*+?]/,"\\\\&",d)
            }
            gsub(d"|\n$","\0")
        ' <<<"$string"
    )
    # echo $( declare -p a | md5sum; declare -p a )
}

or the same idea in Perl:

split_byREorStr_perl(){
    local delim=$1
    local string=$2
    local regex=$3  # if set, delimiter is regex

    readarray -td '' a < <(
        export delim regex
        perl -0777pe '
            $d = $ENV{delim};
            $d = "\Q$d\E" if ! $ENV{regex};
            s/$d|\n$/\0/g;
        ' <<<"$string"
    )
    # echo $( declare -p a | md5sum; declare -p a )
}

The implementations produce identical output, tested by comparing md5sum separately.

Note that if input had been ambiguous ("logically incorrect" as @bgoldst puts it), behaviour would diverge slightly. For example, with delimiter -- and string a- or a---:

  • @goldst's code returns: declare -a a=([0]="a") or declare -a a=([0]="a" [1]="")
  • mine return: declare -a a=([0]="a-") or declare -a a=([0]="a" [1]="-")

Arguments were derived with simple Perl scripts from:

delim="-=-="
base="ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"

Here are the tables of timing results (in seconds) for 3 different types of string and delimiter argument.

  • #s - length of string argument
  • #d - length of delim argument
  • = - performance break-even point
  • ! - "acceptable" performance limit (bash) is somewhere around here
  • !! - "acceptable" performance limit (gawk) is somewhere around here
  • - - function took too long
  • <!> - gawk command failed to run

Type 1

d=$(perl -e "print( '$delim' x (7*2**$n) )")
s=$(perl -e "print( '$delim' x (7*2**$n) . '$base' x (7*2**$n) )")
n #s #d gawk b_sub b_faster b_naive
0 252 28 0.002 0.000 0.000 0.000
1 504 56 0.005 0.000 0.000 0.001
2 1008 112 0.005 0.001 0.000 0.003
3 2016 224 0.006 0.001 0.000 0.009
4 4032 448 0.007 0.002 0.001 0.048
= 5 8064 896 0.014 0.008 0.005 0.377
6 16128 1792 0.018 0.029 0.017 (2.214)
7 32256 3584 0.033 0.057 0.039 (15.16)
! 8 64512 7168 0.063 0.214 0.128 -
9 129024 14336 0.111 (0.826) (0.602) -
10 258048 28672 0.214 (3.383) (2.652) -
!! 11 516096 57344 0.430 (13.46) (11.00) -
12 1032192 114688 (0.834) (58.38) - -
13 2064384 229376 <!> (228.9) - -

Type 2

d=$(perl -e "print( '$delim' x ($n) )")
s=$(perl -e "print( ('$delim' x ($n) . '$base' x $n ) x (2**($n-1)) )")
n #s #d gawk b_sub b_faster b_naive
0 0 0 0.003 0.000 0.000 0.000
1 36 4 0.003 0.000 0.000 0.000
2 144 8 0.005 0.000 0.000 0.000
3 432 12 0.005 0.000 0.000 0.000
4 1152 16 0.005 0.001 0.001 0.002
5 2880 20 0.005 0.001 0.002 0.003
6 6912 24 0.006 0.003 0.009 0.014
= 7 16128 28 0.012 0.012 0.037 0.044
8 36864 32 0.023 0.044 0.167 0.187
! 9 82944 36 0.049 0.192 (0.753) (0.840)
10 184320 40 0.097 (0.925) (3.682) (4.016)
11 405504 44 0.204 (4.709) (18.00) (19.58)
!! 12 884736 48 0.444 (22.17) - -
13 1916928 52 (1.019) (102.4) - -

Type 3

d=$(perl -e "print( '$delim' x (2**($n-1)) )")
s=$(perl -e "print( ('$delim' x (2**($n-1)) . '$base' x (2**($n-1)) ) x ($n) )")
n #s #d gawk b_sub b_faster b_naive
0 0 0 0.000 0.000 0.000 0.000
1 36 4 0.004 0.000 0.000 0.000
2 144 8 0.003 0.000 0.000 0.000
3 432 16 0.003 0.000 0.000 0.000
4 1152 32 0.005 0.001 0.001 0.002
5 2880 64 0.005 0.002 0.001 0.003
6 6912 128 0.006 0.003 0.003 0.014
= 7 16128 256 0.012 0.011 0.010 0.077
8 36864 512 0.023 0.046 0.046 (0.513)
! 9 82944 1024 0.049 0.195 0.197 (3.850)
10 184320 2048 0.103 (0.951) (1.061) (31.84)
11 405504 4096 0.222 (4.796) - -
!! 12 884736 8192 0.473 (22.88) - -
13 1916928 16384 (1.126) (105.4) - -

Summary of delimiters length 1..10

As short delimiters are probably more likely than long, summarised below are the results of varying delimiter length between 1 and 10 (results for 2..9 mostly elided as very similar).

s1=$(perl -e "print( '$d' . '$base' x (7*2**$n) )")
s2=$(perl -e "print( ('$d' . '$base' x $n ) x (2**($n-1)) )")
s3=$(perl -e "print( ('$d' . '$base' x (2**($n-1)) ) x ($n) )")

bash_sub < gawk

string n #s #d gawk b_sub b_faster b_naive
s1 10 229377 1 0.131 0.089 1.709 -
s1 10 229386 10 0.142 0.095 1.907 -
s2 8 32896 1 0.022 0.007 0.148 0.168
s2 8 34048 10 0.021 0.021 0.163 0.179
s3 12 786444 1 0.436 0.468 - -
s3 12 786456 2 0.434 0.317 - -
s3 12 786552 10 0.438 0.333 - -

bash_sub < 0.5s

string n #s #d gawk b_sub b_faster b_naive
s1 11 458753 1 0.256 0.332 (7.089) -
s1 11 458762 10 0.269 0.387 (8.003) -
s2 11 361472 1 0.205 0.283 (14.54) -
s2 11 363520 3 0.207 0.462 (16.66) -
s3 12 786444 1 0.436 0.468 - -
s3 12 786456 2 0.434 0.317 - -
s3 12 786552 10 0.438 0.333 - -

gawk < 0.5s

string n #s $d gawk b_sub b_faster b_naive
s1 11 458753 1 0.256 0.332 (7.089) -
s1 11 458762 10 0.269 0.387 (8.003) -
s2 12 788480 1 0.440 (1.252) - -
s2 12 806912 10 0.449 (4.968) - -
s3 12 786444 1 0.436 0.468 - -
s3 12 786456 2 0.434 0.317 - -
s3 12 786552 10 0.438 0.333 - -

(I'm not entirely sure why bash_sub with s>160k and d=1 was consistently slower than d>1 for s3.)

All tests carried out with bash 5.0.17 on an Intel i7-7500U running xubuntu 20.04.

jhnc
  • 11,310
  • 1
  • 9
  • 26
  • Perfect answer! Total mastery of the subject! The "split_byStr_bash_faster" function was an excellent implementation. **The amount of upvotes for this post does not do justice to the quality of the answer presented**. – Eduardo Lucio Aug 07 '22 at 02:15
  • If it helps, an adaptation of the "split_byStr_bash_faster" function can be found here https://stackoverflow.com/a/73263419/3223785 . – Eduardo Lucio Aug 07 '22 at 02:17
  • This is BOSS-level analysis, very insightful answer. – raffian Jul 21 '23 at 21:19
7

enter code herePure bash multi-character delimiter solution.

As others have pointed out in this thread, the OP's question gave an example of a comma delimited string to be parsed into an array, but did not indicate if he/she was only interested in comma delimiters, single character delimiters, or multi-character delimiters.

Since Google tends to rank this answer at or near the top of search results, I wanted to provide readers with a strong answer to the question of multiple character delimiters, since that is also mentioned in at least one response.

If you're in search of a solution to a multi-character delimiter problem, I suggest reviewing Mallikarjun M's post, in particular the response from gniourf_gniourf who provides this elegant pure BASH solution using parameter expansion:

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

Link to cited comment/referenced post

Link to cited question: Howto split a string on a multi-character delimiter in bash?


Update 3 Aug 2022

xebeche raised a good point in comments below. After reviewing their suggested edits, I've revised the script provided by gniourf_gniourf, and added remarks for ease of understanding what the script is doing. I also changed the double brackets [[]] to single brackets, for greater compatibility since many SHell variants do not support double bracket notation. In this case, for BaSH, the logic works inside single or double brackets.

#!/bin/bash
  
str="LearnABCtoABCSplitABCABCaABCStringABC"
delimiter="ABC"
array=()

while [ "$str" ]; do

    # parse next sub-string, left of next delimiter
    substring="${str%%"$delimiter"*}" 

    # when substring = delimiter, truncate leading delimiter
    # (i.e. pattern is "$delimiter$delimiter")
    [ -z "$substring" ] && str="${str#"$delimiter"}" && continue

    # create next array element with parsed substring
    array+=( "$substring" )

    # remaining string to the right of delimiter becomes next string to be evaluated
    str="${str:${#substring}}"

    # prevent infinite loop when last substring = delimiter
    [ "$str" == "$delimiter" ] && break

done

declare -p array

Without the comments:

#!/bin/bash
str="LearnABCtoABCSplitABCABCaABCStringABC"
delimiter="ABC"
array=()
while [ "$str" ]; do
    substring="${str%%"$delimiter"*}" 
    [ -z "$substring" ] && str="${str#"$delimiter"}" && continue
    array+=( "$substring" )
    str="${str:${#substring}}"
    [ "$str" == "$delimiter" ] && break
done
declare -p array
MrPotatoHead
  • 1,035
  • 14
  • 11
  • 1
    See [my comment](https://stackoverflow.com/questions/40686922/howto-split-a-string-on-a-multi-character-delimiter-in-bash#comment99425158_47633817) for a similar but improved approach. – xebeche Jun 03 '19 at 21:45
1

Try this

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

It's simple. If you want, you can also add a declare (and also remove the commas):

IFS=' ';declare -a array=(Paris France Europe)

The IFS is added to undo the above but it works without it in a fresh bash instance

Geoff Lee
  • 144
  • 6
1
#!/bin/bash

string="a | b c"
pattern=' | '

# replaces pattern with newlines
splitted="$(sed "s/$pattern/\n/g" <<< "$string")"

# Reads lines and put them in array
readarray -t array2 <<< "$splitted"

# Prints number of elements
echo ${#array2[@]}
# Prints all elements
for a in "${array2[@]}"; do
        echo "> '$a'"
done

This solution works for larger delimiters (more than one char).
Doesn't work if you have a newline already in the original string

Xiaojiba
  • 116
  • 1
  • 8
1

This works for the given data:

$ aaa='Paris, France, Europe'
$ mapfile -td ',' aaaa < <(echo -n "${aaa//, /,}")
$ declare -p aaaa

Result:

declare -a aaaa=([0]="Paris" [1]="France" [2]="Europe")

And it will also work for extended data with spaces, such as "New York":

$ aaa="New York, Paris, New Jersey, Hampshire"
$ mapfile -td ',' aaaa < <(echo -n "${aaa//, /,}")
$ declare -p aaaa

Result:

declare -a aaaa=([0]="New York" [1]="Paris" [2]="New Jersey" [3]="Hampshire")
1

Here is a pure bash function which works for empty elements, multi-char-separators, globs etc.

# Usage: split_str_by "a,b,c" ,  # result is in "${__ret[@]}"
split_str_by(){
    local s="$1" sep="$2" el
    __ret=()
    while true; do
        el="${s%%"$sep"*}"
        __ret+=("$el")
        # If no sep was left, quit
        [[ "$el" == "$s" ]] && break
        s="${s#*"$sep"}"
    done
    return 0
}

# some tests:
split_str_by "a,b,c" ,
declare -p __ret  # __ret=([0]="a" [1]="b" [2]="c")

split_str_by ",a,,b,c," ,
declare -p __ret # __ret=([0]="" [1]="a" [2]="" [3]="b" [4]="c" [5]="")

split_str_by ",,a,b,,,c,," ,,
declare -p __ret # __ret=([0]="" [1]="a,b" [2]=",c" [3]="")

split_str_by " *a *b *c *" ' *'
declare -p __ret # __ret=([0]="" [1]="a" [2]="b" [3]="c" [4]="")

split_str_by "--aa--bb--cc" '--'
declare -p __ret # declare -a __ret=([0]="" [1]="aa" [2]="bb" [3]="cc")

spawn
  • 192
  • 3
  • 9
  • you can get rid of the ugly `true` and `break` by slightly modifying the loop to `while el=...; __ret+=...; [[ $el != $s ]]; do s=...; done` and I think `return 0` is the default behaviour – jhnc Aug 30 '23 at 18:30
  • this is equivalent to my `split_byStr_bash_naive` without needing an explicit test for empty string, and seems to run with comparable performance – jhnc Aug 30 '23 at 18:44
0

Another way to do it without modifying IFS:

read -r -a myarray <<< "${string//, /$IFS}"

Rather than changing IFS to match our desired delimiter, we can replace all occurrences of our desired delimiter ", " with contents of $IFS via "${string//, /$IFS}".

Maybe this will be slow for very large strings though?

This is based on Dennis Williamson's answer.

Lindsay-Needs-Sleep
  • 1,304
  • 1
  • 15
  • 26
  • the default IFS is a list of white space characters - you're just changing one string into another string but then still leaving IFS to separate based on characters in that string. – Ed Morton Jul 04 '22 at 15:52
0

I came across this post when looking to parse an input like: word1,word2,...

none of the above helped me. solved it by using awk. If it helps someone:

STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
        echo "This is the word $word"
done
balaganAtomi
  • 550
  • 8
  • 13
-1

UPDATE: Don't do this, due to problems with eval.

With slightly less ceremony:

IFS=', ' eval 'array=($string)'

e.g.

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar
user1009908
  • 1,390
  • 13
  • 22
  • 6
    eval is evil! don't do this. – caesarsol Oct 29 '15 at 14:42
  • 1
    Pfft. No. If you're writing scripts large enough for this to matter, you're doing it wrong. In application code, eval is evil. In shell scripting, it's common, necessary, and inconsequential. – user1009908 Oct 30 '15 at 04:05
  • 2
    put a `$` in your variable and you'll see... I write many scripts and I never ever had to use a single `eval` – caesarsol Nov 02 '15 at 18:19
  • 2
    You're right, this is only usable when the input is known to be clean. Not a robust solution. – user1009908 Dec 22 '15 at 23:04
  • The only time i ever had to use eval, was for a application that would self generate its own code/modules... AND this never had any form of user input... – Angry 84 Dec 31 '15 at 03:49
-1

Do not change IFS!

Here's a simple bash one-liner:

read -a my_array <<< $(echo ${INPUT_STRING} | tr -d ' ' | tr ',' ' ')
madhat1
  • 676
  • 9
  • 15
-2

Here's my hack!

Splitting strings by strings is a pretty boring thing to do using bash. What happens is that we have limited approaches that only work in a few cases (split by ";", "/", "." and so on) or we have a variety of side effects in the outputs.

The approach below has required a number of maneuvers, but I believe it will work for most of our needs!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: https://dba.stackexchange.com/questions/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split "$STRING_TO_SPLIT" "bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo " > -----------------------------------------"
    echo "${F_SPLIT_R[$i]}"
    echo " < -----------------------------------------"
done

if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo " > -----------------------------------------"
    echo "The strings are the same!"
    echo " < -----------------------------------------"
fi
Eduardo Lucio
  • 1,771
  • 2
  • 25
  • 43
-2

Since there are so many ways to solve this, let's start by defining what we want to see in our solution.

  1. Bash provides a builtin readarray for this purpose. Let's use it.
  2. Avoid ugly and unnecessary tricks such as changing IFS, looping, using eval, or adding an extra element then removing it.
  3. Find a simple, readable approach that can easily be adapted to similar problems.

The readarray command is easiest to use with newlines as the delimiter. With other delimiters it may add an extra element to the array. The cleanest approach is to first adapt our input into a form that works nicely with readarray before passing it in.

The input in this example does not have a multi-character delimiter. If we apply a little common sense, it's best understood as comma separated input for which each element may need to be trimmed. My solution is to split the input by comma into multiple lines, trim each element, and pass it all to readarray.

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')

# Result:
declare -p foo
# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'

EDIT: My solution allows inconsistent spacing around comma separators, while also allowing elements to contain spaces. Few other solutions can handle these special cases.

I also avoid approaches which seem like hacks, such as creating an extra array element and then removing it. If you don't agree it's the best answer here, please leave a comment to explain.

If you'd like to try the same approach purely in Bash and with fewer subshells, it's possible. But the result is harder to read, and this optimization is probably unnecessary.

string='     Paris,France  ,   All of Europe    '
foo="${string#"${string%%[![:space:]]*}"}"
foo="${foo%"${foo##*[![:space:]]}"}"
foo="${foo//+([[:space:]]),/,}"
foo="${foo//,+([[:space:]])/,}"
readarray -t foo < <(echo "$foo")
Bryan Roach
  • 743
  • 9
  • 12
-2

For multilined elements, why not something like

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT
Whimusical
  • 6,401
  • 11
  • 62
  • 105
-3

Another way would be:

string="Paris, France, Europe"
IFS=', ' arr=(${string})

Now your elements are stored in "arr" array. To iterate through the elements:

for i in ${arr[@]}; do echo $i; done
  • 1
    I cover this idea in [my answer](https://stackoverflow.com/questions/10586153/split-string-into-an-array-in-bash/45201229#45201229); see ***Wrong answer #5*** (you might be especially interested in my discussion of the `eval` trick). Your solution leaves `$IFS` set to the comma-space value after-the-fact. – bgoldst Aug 13 '17 at 22:38
-4

Another approach can be:

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

After this 'arr' is an array with four strings. This doesn't require dealing IFS or read or any other special stuff hence much simpler and direct.

rsjethani
  • 2,179
  • 6
  • 24
  • 30