Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bash: Split string into character array

Tags:

string

bash

I have a string in a Bash shell script that I want to split into an array of characters, not based on a delimiter but just one character per array index. How can I do this? Ideally it would not use any external programs. Let me rephrase that. My goal is portability, so things like sed that are likely to be on any POSIX compatible system are fine.

like image 721
n s Avatar asked Sep 28 '11 05:09

n s


People also ask

How do I convert a string to an array in bash?

Using the tr Command to Split a String Into an Array in Bash It can be used to remove repeated characters, convert lowercase to uppercase, and replace characters. In the bash script below, the echo command pipes the string variable, $addrs , to the tr command, which splits the string variable on a delimiter, ; .

How do you split a line in a word in shell script?

The -a option of read will allow you to split a line read in by the characters contained in $IFS . #!/bin/bash filename=$1 while read LINE do echo $LINE | read -a done < $filename should it work?

How do I echo an array in bash?

How to Echo a Bash Array? To echo an array, use the format echo ${Array[0]}. Array is your array name, and 0 is the index or the key if you are echoing an associative array. You can also use @ or * symbols instead of an index to print the entire array.


12 Answers

Try

echo "abcdefg" | fold -w1

Edit: Added a more elegant solution suggested in comments.

echo "abcdefg" | grep -o .
like image 80
xdazz Avatar answered Sep 30 '22 19:09

xdazz


You can access each letter individually already without an array conversion:

$ foo="bar"
$ echo ${foo:0:1}
b
$ echo ${foo:1:1}
a
$ echo ${foo:2:1}
r

If that's not enough, you could use something like this:

$ bar=($(echo $foo|sed  's/\(.\)/\1 /g'))
$ echo ${bar[1]}
a

If you can't even use sed or something like that, you can use the first technique above combined with a while loop using the original string's length (${#foo}) to build the array.

Warning: the code below does not work if the string contains whitespace. I think Vaughn Cato's answer has a better chance at surviving with special chars.

thing=($(i=0; while [ $i -lt ${#foo} ] ; do echo ${foo:$i:1} ; i=$((i+1)) ; done))
like image 43
Mat Avatar answered Sep 30 '22 20:09

Mat


If your string is stored in variable x, this produces an array y with the individual characters:

i=0
while [ $i -lt ${#x} ]; do y[$i]=${x:$i:1};  i=$((i+1));done
like image 38
Vaughn Cato Avatar answered Sep 30 '22 18:09

Vaughn Cato


As an alternative to iterating over 0 .. ${#string}-1 with a for/while loop, there are two other ways I can think of to do this with only bash: using =~ and using printf. (There's a third possibility using eval and a {..} sequence expression, but this lacks clarity.)

With the correct environment and NLS enabled in bash these will work with non-ASCII as hoped, removing potential sources of failure with older system tools such as sed, if that's a concern. These will work from bash-3.0 (released 2005).

Using =~ and regular expressions, converting a string to an array in a single expression:

string="wonkabars"
[[ "$string" =~ ${string//?/(.)} ]]       # splits into array
printf "%s\n" "${BASH_REMATCH[@]:1}"      # loop free: reuse fmtstr
declare -a arr=( "${BASH_REMATCH[@]:1}" ) # copy array for later

The way this works is to perform an expansion of string which substitutes each single character for (.), then match this generated regular expression with grouping to capture each individual character into BASH_REMATCH[]. Index 0 is set to the entire string, since that special array is read-only you cannot remove it, note the :1 when the array is expanded to skip over index 0, if needed. Some quick testing for non-trivial strings (>64 chars) shows this method is substantially faster than one using bash string and array operations.

The above will work with strings containing newlines, =~ supports POSIX ERE where . matches anything except NUL by default, i.e. the regex is compiled without REG_NEWLINE. (The behaviour of POSIX text processing utilities is allowed to be different by default in this respect, and usually is.)

Second option, using printf:

string="wonkabars"
ii=0
while printf "%s%n" "${string:ii++:1}" xx; do 
  ((xx)) && printf "\n" || break
done 

This loop increments index ii to print one character at a time, and breaks out when there are no characters left. This would be even simpler if the bash printf returned the number of character printed (as in C) rather than an error status, instead the number of characters printed is captured in xx using %n. (This works at least back as far as bash-2.05b.)

With bash-3.1 and printf -v var you have slightly more flexibility, and can avoid falling off the end of the string should you be doing something other than printing the characters, e.g. to create an array:

declare -a arr
ii=0
while printf -v cc "%s%n" "${string:(ii++):1}" xx; do 
    ((xx)) && arr+=("$cc") || break
done
like image 34
mr.spuratic Avatar answered Sep 30 '22 20:09

mr.spuratic


Pure Bash solution with no loop:

#!/usr/bin/env bash

str='The quick brown fox jumps over a lazy dog.'

# Need extglob for the replacement pattern
shopt -s extglob

# Split string characters into array (skip first record)
# Character 037 is the octal representation of ASCII Record Separator
# so it can capture all other characters in the string, including spaces.
IFS= mapfile -s1 -t -d $'\37' array <<<"${str//?()/$'\37'}"

# Strip out captured trailing newline of here-string in last record
array[-1]="${array[-1]%?}"

# Debug print array
declare -p array 
like image 21
Léa Gris Avatar answered Sep 30 '22 19:09

Léa Gris


The most simple, complete and elegant solution:

$ read -a ARRAY <<< $(echo "abcdefg" | sed 's/./& /g')  

and test

$ echo ${ARRAY[0]}
  a

$ echo ${ARRAY[1]}
  b

Explanation: read -a reads the stdin as an array and assigns it to the variable ARRAY treating spaces as delimiter for each array item.

The evaluation of echoing the string to sed just add needed spaces between each character.

We are using Here String (<<<) to feed the stdin of the read command.

like image 42
Alexandro de Oliveira Avatar answered Sep 30 '22 18:09

Alexandro de Oliveira


I have found that the following works the best:

array=( `echo string | grep -o . ` )

(note the backticks)

then if you do: echo ${array[@]} , you get: s t r i n g

or: echo ${array[2]} , you get: r

like image 31
AZAhmed Avatar answered Sep 30 '22 19:09

AZAhmed


string=hello123

for i in $(seq 0 ${#string})
    do array[$i]=${string:$i:1}
done

echo "zero element of array is [${array[0]}]"
echo "entire array is [${array[@]}]"

The zero element of array is [h]. The entire array is [h e l l o 1 2 3 ].

like image 35
0x00 Avatar answered Sep 30 '22 20:09

0x00


If the text can contain spaces:

eval a=( $(echo "this is a test" | sed "s/\(.\)/'\1' /g") )
like image 34
Karoly Horvath Avatar answered Sep 30 '22 19:09

Karoly Horvath


$ echo hello | awk NF=NF FS=
h e l l o

Or

$ echo hello | awk '$0=RT' RS=[[:alnum:]]
h
e
l
l
o
like image 27
Zombo Avatar answered Sep 30 '22 19:09

Zombo


Yet another on :), the stated question simply says 'Split string into character array' and don't say much about the state of the receiving array, and don't say much about special chars like and control chars.

My assumption is that if I want to split a string into an array of chars I want the receiving array containing just that string and no left over from previous runs, yet preserve any special chars.

For instance the proposed solution family like

for (( i=0 ; i < ${#x} ; i++ )); do y[i]=${x:i:1}; done

Have left overs in the target array.

$ y=(1 2 3 4 5 6 7 8)
$ x=abc
$ for (( i=0 ; i < ${#x} ; i++ )); do y[i]=${x:i:1}; done
$ printf '%s ' "${y[@]}"
a b c 4 5 6 7 8 

Beside writing the long line each time we want to split a problem, so why not hide all this into a function we can keep is a package source file, with a API like

s2a "Long string" ArrayName

I got this one that seems to do the job.

$ s2a()
> { [ "$2" ] && typeset -n __=$2 && unset $2;
>   [ "$1" ] && __+=("${1:0:1}") && s2a "${1:1}"
> }

$ a=(1 2 3 4 5 6 7 8 9 0) ; printf '%s ' "${a[@]}"
1 2 3 4 5 6 7 8 9 0 

$ s2a "Split It" a        ; printf '%s ' "${a[@]}"
S p l i t   I t 
like image 25
Phi Avatar answered Sep 30 '22 19:09

Phi


I know this is a "bash" question, but please let me show you the perfect solution in zsh, a shell very popular these days:

string='this is a string'
string_array=(${(s::)string})  #Parameter expansion. And that's it!

print ${(t)string_array}  -> type array
print $#string_array -> 16 items
like image 37
Frat Quintero Avatar answered Sep 30 '22 20:09

Frat Quintero