3

Given an array of elements (servers), how do I shuffle the array to obtain a random new array ?

inarray=("serverA" "serverB" "serverC")

outarray=($(randomize_func ${inarray[@]})

echo ${outarray[@]}
serverB serverC serverA

There is a command shuf (man page) but it does not exist on every linux.

This is my first attempt to post a self-answered question stackoverflow, if you have a better solution, please post it.

JoKoT3
  • 66
  • 1
  • 6

4 Answers4

3

This is another pure Bash solution:

#! /bin/bash

# Randomly permute the arguments and put them in array 'outarray'
function perm
{
    outarray=( "$@" )

    # The algorithm used is the Fisher-Yates Shuffle
    # (https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle),
    # also known as the Knuth Shuffle.

    # Loop down through 'outarray', swapping the item at the current index
    # with a random item chosen from the array up to (and including) that
    # index
    local idx rand_idx tmp
    for ((idx=$#-1; idx>0 ; idx--)) ; do
        rand_idx=$(( RANDOM % (idx+1) ))
        # Swap if the randomly chosen item is not the current item
        if (( rand_idx != idx )) ; then
            tmp=${outarray[idx]}
            outarray[idx]=${outarray[rand_idx]}
            outarray[rand_idx]=$tmp
        fi
    done
}

inarray=( 'server A' 'server B' 'server C' )

# Declare 'outarray' for use by 'perm'
declare -a outarray

perm "${inarray[@]}"

# Display the contents of 'outarray'
declare -p outarray

It's Shellcheck-clean, and tested with Bash 3 and Bash 4.

The caller gets the results from outarray rather than putting them in outarray because outarray=( $(perm ...) ) doesn't work if any of the items to be shuffled contain whitespace characters, and it may also break if items contain glob metacharacters. There is no nice way to return non-trivial values from Bash functions.

If perm is called from another function then declaring outarray in the caller (e.g. with local -a outarray) will avoid creating (or clobbering) a global variable.

The code could safely be simplified by doing the swap unconditionally, at the cost of doing some pointless swaps of items with themselves.

pjh
  • 6,388
  • 2
  • 16
  • 17
1

This is the solution I found (it even works in bash < 4.0).

Shellchecked and edited thanks to comments below.

#!/bin/bash
# random permutation of input
perm() {
    # make the input an array
    local -a items=( "$@" )
    # all the indices of the array
    local -a items_arr=( "${!items[@]}" )
    # create out array
    local -a items_out=()
    # loop while there is at least one index
    while [ ${#items_arr[@]} -gt 0 ]; do
        # pick a random number between 1 and the length of the indices array
        local rand=$(( RANDOM % ${#items_arr[@]} ))
        # get the item index from the array of indices
        local items_idx=${items_arr[$rand]}
        # append that item to the out array
        items_out+=("${items[$items_idx]}")
        ### NOTE array is not reindexed when pop'ing, so we redo an array of 
        ### index at each iteration
        # pop the item
        unset "items[$items_idx]"
        # recreate the array
        items_arr=( "${!items[@]}" )
    done
    echo "${items_out[@]}"
}

perm "server1" "server2" "server3" "server4" "server4" "server5" "server6" "server7" "server8"

It is more than possible that it can be optimized.

JoKoT3
  • 66
  • 1
  • 6
  • 1
    You should make all variables `local`, or you leak them to global scope. – Benjamin W. Nov 09 '18 at 17:03
  • @chepner you quoted `$@` in items assignement, making so make you pass all arguments like this : `"arg1" "arg2"...` instead on `"arg1 arg2..."`. not sure what is best. – JoKoT3 Nov 09 '18 at 17:38
  • 1
    Quoting is necessary if you want to shuffle an array like `("foo bar" "1 2 3")`. It preserves the whitespace in the elements without confusing it with the whitespace that separates the elements. – chepner Nov 09 '18 at 17:41
  • @BenjaminW. thanks for you remark, variables localized – JoKoT3 Nov 09 '18 at 17:41
  • @chepner great thanks ! – JoKoT3 Nov 09 '18 at 17:42
  • Generally, you should quote virtually *every* parameter expansion, especially because the few cases where you might need word-splitting are now accommodated by arrays. – chepner Nov 09 '18 at 17:42
  • [Shellcheck](https://www.shellcheck.net/) issues several warnings for the code. Some of them are harmless, but the issue with `unset items[$items_idx]` ([Shellcheck SC2184](https://github.com/koalaman/shellcheck/wiki/SC2184)) is both subtle and potentially dangerous. In short: it could be broken by globbing. Also see [Bash Pitfalls #57 (unset unquoted array element)](https://mywiki.wooledge.org/BashPitfalls#unset_a.5B0.5D). – pjh Nov 09 '18 at 21:02
0

You should use shuf:

inarray=("serverA" "serverB" "serverC")
IFS=$'\n' outarray=($(printf "%s$IFS" "${inarray[@]}" | shuf))

Or when using array members with newlines and other strange characters, use null delimetered strings:

inarray=("serverA" "serverB" "serverC")
readarray -d '' outarray < <(printf "%s\0" "${inarray[@]}" | shuf -z)
KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • I recommend `set -f` with the first one. And note you will need `bash` 4.4 or later for the second one if I am not mistaken. You might also throw in a solution with a simple `while` `read` combo for other versions :) – PesaThe Nov 09 '18 at 17:59
-1

The sort utility has the ability to shuffle lists randomly.

Try this instead:

servers="serverA serverB serverC serverD"
for s in $servers ; do echo $s ; done | sort -R
r3mainer
  • 23,981
  • 3
  • 51
  • 88
  • Thanks for your answer, my version of sort (GNU CoreUtils 5.97) does not have the -R option which was added in 2005 :O. Also, I use you answer to document that sort -R is not a true shuffle : being based on hash, it group together identical value (as documented in [https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=641166] – JoKoT3 Nov 09 '18 at 17:01