1

I am trying to write code to break up a large array into many different small arrays. Eventually the array I would be passed is one of unknown size, this is just my test subject. I have gotten this far:

#!/bin/bash
num=(10 3 12 3 4 4)
inArray=${#num[@]}
numArrays=$(($inArray/2))
remain=$(($inArray%2))
echo $numArrays
echo $remain
nun=0
if test $remain -gt  $nun; then
        numArrays=$(($numArrays+1))
fi
array=(1 2)
j=0
for ((i=0;i<$numArrays;i++, j=j+2)); do
        array=("${num[@]:$j:2}")
        echo "The array says: ${array[@]}"
        echo "The size? ${#array[@]}"
done    

What I am really having a problem with is : I would like to make the variable 'array' be able to change names slightly every time, so each array is kept and has a unique name after the loop. I have tried making the name array_$i but that returns:

[Stephanie@~]$ ./tmp.sh 
3
0
./tmp.sh: line 16: syntax error near unexpected token `"${num[@]:$j:2}"'
./tmp.sh: line 16: `    array_$i=("${num[@]:$j:2}")'
[Stephanie@RDT00069 ~]$ ./tmp.sh 
3
0
./tmp.sh: line 16: syntax error near unexpected token `$i'
./tmp.sh: line 16: `    array($i)=("${num[@]:$j:2}")'

Does anyone have any advice? Thanks

Stephopolis
  • 1,765
  • 9
  • 36
  • 65
  • I think `eval` may be what you want: as in `eval "array_$i=(\"${num[@]:$j:2}\")"`. Not sure if the quoting is all correct there, though - you may need to fiddle with that a bit. – twalberg Jul 18 '12 at 19:00
  • 1
    By the way, that `if test $remain…` block can be written `(( numArrays+=(remain>0?1:0) ))` or `(( numArrays+=(remain>0) ))`. – kojiro Jul 18 '12 at 19:27
  • @kojiro Thanks! I am fairly new to bash so it is really good to see how the code could be used more eloquently – Stephopolis Jul 18 '12 at 19:43

3 Answers3

1

With simple variables, you can use the declare keyword to make indirect assignments:

v=foo
declare $v=5
echo $foo    # Prints 5

This doesn't extend to arrays in the obvious (to me, anyway) sense:

i=2
# This produces a syntax error
declare -a array_$i=("${num[@]:$j:2}")

Instead, you can declare an empty array

declare -a array_$i

or assign items one at a time:

declare -a array_$i[0]=item1 array_$i[1]=item2

Here's an example of using a for-loop to copy, say, the 3rd and 4th letters of a big array into a smaller one. We use i as the dynamic part of the name of the smaller array, and j as the index into that array.

letters=(a b c d e f)
i=1
j=0
for letter in "${letters[@]:2:2}"; do
    # E.g., i=0 and j=1 would result in
    #   declare -a array_0[1]=c
    declare -a array_$i[$j]=$letter
    let j+=1
  done
done

echo ${array_1[@]};  # c d

${foo[@]:x:y} gives us elements x, x+1, ..., x+y-1 from foo, and

You can wrap the whole thing inside another for-loop to accomplish the goal of splitting letters into 3 smaller arrays:

 # We'll create array_0, array_1, and array_2
for i in 0 1 2; do 
  # Just like our subset above, but start at position i*2 instead of
  # a constant.
  for letter in "${letters[@]:$((i*2)):2}"; do
      declare -a array_$i[$j]=$letter
  done
done

Once you manage to populate your three arrays, how do you access them without eval? Bash has syntax for indirect access:

v=foo
foo=5
echo ${!v}   # echoes 5!

The exclamation point says to use the word that follows as a variable whose value should be used as the name of the parameter to expand. Knowing that, you might think you could do the following, but you'd be wrong.

i=1
v=array_$i   # array_1
echo ${!v[0]}  # array_1[0] is c, so prints c, right? Wrong.

In the above, bash tries to find a variable called v[0] and expand it to get the name of a parameter to expand. We actually have to treat our array plus its index as a single name:

i=1
v=array_$i[0]
echo ${!v}    # This does print c
chepner
  • 497,756
  • 71
  • 530
  • 681
  • It still doesn't seem to like this. It is giving a syntax error like before. – Stephopolis Jul 18 '12 at 19:24
  • I am afraid I am confused by your solution. (I am not the best versed in bash, I have only been using it about a month.) Could you explain how the for loop is working? Is it supposed to entirely replace the one I am using or is it to be nested in mine? – Stephopolis Jul 18 '12 at 19:39
  • Bash behaves inconsistently across versions for statements like `declare foo=bar`. It's probably best to do the `declare` in one line and the definition in another. – kojiro Jul 18 '12 at 19:51
  • @kojiro, good to know. I've been coming to understand 'declare' through trial and error, as the documentation for it is spotty. – chepner Jul 18 '12 at 20:38
1

I don't think you can really avoid eval here, but you might be able to do it safely if you're careful. Here's my approach:

for name in "${!array_*}"; do # Get all names starting with array_
    i="${name#array_*}" # Get the part after array_
    if [[ $i != *[^0-9]* ]]; then # Check that it's a number.
        printf '%s is not a valid subarray name\n' "$name"
    else
        # Create a variable named "statement" that contains code you want to eval.
        printf -v statement 'cur_array=( "${%s[@]}" )' "$name"
        eval "$statement"
        # Do interesting things with $cur_array
    fi
done

Before this, when you're just creating the array, you know what $name should be, so just use the printf -v part.

To make it even safer, you could save all the allowed array names in another array and check that $name is a member.

kojiro
  • 74,557
  • 19
  • 143
  • 201
  • Out of curiosity, why is using eval here dangerous? – Stephopolis Jul 18 '12 at 19:43
  • @Stephopolis `eval` executes code directly, so it can cause difficult-to-debug errors and security holes. It's especially dangerous with user-provided data. You can mitigate some of these risks by validating the input, but it's almost always useful to try to avoid eval. http://stackoverflow.com/questions/86513 – kojiro Jul 18 '12 at 20:01
0

This should work, but this is not a good solution, another language may be better bash does not support multi dimensional arrays

eval array_$i='('"${num[@]:$j:2}"')'

And then, for example

eval 'echo "${array_'$i'[0]}"'
chepner
  • 497,756
  • 71
  • 530
  • 681
Nahuel Fouilleul
  • 18,726
  • 2
  • 31
  • 36