7

I have an array in Bash, say it contains the numbers {1, 2, 3, 4, 5}. I want to extract some of those numbers randomly, such that the same number doesn't get extracted twice.

Basically, if I wanted to extract 3 numbers from the array, I want results like: {3, 4, 1} or {5, 2, 4} and not {1, 1, 3} or {2, 5, 2}.

I've tried deleting elements as I extract them, but it always seems to mess up. Can anyone help?

gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
Rsaesha
  • 1,104
  • 1
  • 13
  • 25
  • shuffle the array, then pick the n first elements? Google for "bash shuffle array" finds some hints, for example http://stackoverflow.com/questions/5533569/simple-method-to-shuffle-the-elements-of-an-array-in-bash-shell – Anders Lindahl Jul 28 '11 at 12:55

5 Answers5

14

Decided to write an answer, as I found the --input-range option to shuf that turned out handy:

N=3
ARRAY=( zero one two three four five )

for index in $(shuf --input-range=0-$(( ${#ARRAY[*]} - 1 )) -n ${N})
do
    echo ${ARRAY[$index]}
done
Community
  • 1
  • 1
Anders Lindahl
  • 41,582
  • 9
  • 89
  • 93
2

How about this:

for i in {1..10}; do
    echo $i
done | shuf 

That will return all the numbers. If you only want a specific amount, do this:

numbers=5    
for i in {1..10}; do
    echo $i
done | shuf | head -$numbers 

And if you want to change the numbers, just change the {1..10} variable to whatever you want.

Matt
  • 2,790
  • 6
  • 24
  • 34
  • Your code absolutely works, but if you do `shuf --input-range 1-10` you don't need the for loop to render a set of array indices. – Anders Lindahl Jul 29 '11 at 06:47
1

Yet another syntax, still using shuf, and preserving elements with spaces:

N=3
ARRAY=( one "two = 2" "3 is three" 4 five )


for el in "${ARRAY[@]}"; do echo $el; done | shuf | head -$N
mivk
  • 13,452
  • 5
  • 76
  • 69
1

very simple 'oneliner' i like:

shuf -e ${POOL[@]} -n3

will give you 3 random elements from your $POOL array

ex:

#~ POOL=(a b c d e)
#~ shuf -e ${POOL[@]} -n3

d
e
a
RASG
  • 5,988
  • 4
  • 26
  • 47
0

This might work for you, if you want a genuine pure bash solution.

takeNrandom() {
# This function takes n+1 parameters: k a1 a2 ... an
# Where k in 0..n
# This function sets the global variable _takeNrandom_out as an array that
# consists of k elements chosen at random among a1 a2 ... an with no repetition
    local k=$1 i
    _takeNrandom_out=()
    shift
    while((k-->0 && $#)); do
        ((i=RANDOM%$#+1))
        _takeNrandom_out+=( "${!i}" )
        set -- "${@:1:i-1}" "${@:i+1}"
    done
}

Try it:

$ array=( $'a field with\na newline' 'a second field' 'and a third one' 42 )
$ takeNrandom 2 "${array[@]}"
$ declare -p _takeNrandom_out
declare -a _takeNrandom_out='([0]="a second field" [1]="a field with
a newline" [2]="and a third one")'

(the newline is really preserved in the array field).

This uses positional parameters, and uses set -- "${@:1:i-1}" "${@:i+1}" to remove the i-th positional parameter. We also used indirect expansion in the line _takeNrandom_out+=( "${!i}" ) to have access to the i-th positional parameter.

Note. This uses the RANDOM variable with a modulo, so the distribution is not exactly uniform. It should be fine for arrays with a small number of fields. Anyway if you have a huge array, you probably shouldn't be using Bash in the first place!

gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104