Shell – Assigning a new value directly into a character index of a value in an array with zsh

arrayshell-scriptvariablezsh

If I have an array of strings and I want to change a single character in one value I could do this:

$ array=(hello world)
$ array[2]=${array[2]:0:2}X${array[2]:3}
$ echo $array[2]
woXld

Even though this solution works it's slow for very long strings as it actually reassigns the value at the index and has to expand both sides of the original value, before and after the changed character.

While it's possible to address the character indices of individual values in an array:

$ echo ${array[2][3]}
X

and to assign a new value into a character index for a scalar variable:

$ string='hello world'
$ string[9]=X
$ echo $string
hello woXld

a similar syntax doesn't work for arrays:

$ array[2][3]=X
zsh: no matches found: array[2][3]=X

Is there a way to do what I want faster than with the first method?

Best Answer

There does not really seem to be a way to do this elegantly. This is probably because zsh does not actually support nested arrays and the syntax is therefore not fully developed.

One thing you could try is to use a temporary variable instead of slicing around the character you want to change:

array=(hello world)
tmp=$array[2]
tmp[3]=X
array[2]=$tmp

Whether it is actually faster seems to depend on the length of $tmp or possibly the array as a whole.

I did a bit of performance testing and got some interesting results: If you are handling only scalars, then replacing a single charcter by index (method A)

foo[500]=X

seems to be always much faster than slicing the left and right partitions and building a new string (method B)

foo=${foo:0:499}X${foo:500}

I put both inside a loop with 100,000 iterations for strings of length 100, 1,000, 10,000 and 100,000 with the following results:

./scalar_A100.zsh 100000:         0.16s
./scalar_A1000.zsh 100000:        0.29s
./scalar_A10000.zsh 100000:       1.66s
./scalar_A100000.zsh 100000:     14.63s

 ./scalar_B100.zsh 100000:        0.42s
 ./scalar_B1000.zsh 100000:       1.17s
 ./scalar_B10000.zsh 100000:      5.39s
 ./scalar_B100000.zsh 100000:    46.23s

Unfortunately, when the string is inside of an array, it depends on the length of the string (or maybe the array itself).

For the array test, I used an array with two elements, the first being "hello" and the second being again a string with lengths between 100 and 100,000 characters.

Except for relativesly short strings, method A (via temporary variable)

foo=$bar[2]
foo[500]=O
bar[2]=$foo

is actually slower than replacing the array element inplace with slices:

bar[2]=${bar[2]:0:499}O${bar[2]:500}

This is due to the values actually being copied into the temporary variable and back into the array. Here are the results for that:

./array_A100.zsh 100000:      0.46s
./array_A1000.zsh 100000:     1.84s
./array_A10000.zsh 100000:   10.50s
./array_A100000.zsh 100000: 101.03s

./array_B100.zsh 100000:      0.60s
./array_B1000.zsh 100000:     1.35s
./array_B10000.zsh 100000:    3.17s
./array_B100000.zsh 100000:  22.13s

Also note that handling arrays is slower than scalars in every case.

Related Question