Random number from a range in a Bash Script

BashShellScripting

Bash Problem Overview


I need to generate a random port number between 2000-65000 from a shell script. The problem is $RANDOM is a 15-bit number, so I'm stuck!

PORT=$(($RANDOM%63000+2001)) would work nicely if it wasn't for the size limitation.

Does anyone have an example of how I can do this, maybe by extracting something from /dev/urandom and getting it within a range?

Bash Solutions


Solution 1 - Bash

shuf -i 2000-65000 -n 1

Enjoy!

Edit: The range is inclusive.

Solution 2 - Bash

On Mac OS X and FreeBSD you may also use jot:

jot -r 1  2000 65000

Solution 3 - Bash

According to the bash man page, $RANDOM is distributed between 0 and 32767; that is, it is an unsigned 15-bit value. Assuming $RANDOM is uniformly distributed, you can create a uniformly-distributed unsigned 30-bit integer as follows:

$(((RANDOM<<15)|RANDOM))

Since your range is not a power of 2, a simple modulo operation will only almost give you a uniform distribution, but with a 30-bit input range and a less-than-16-bit output range, as you have in your case, this should really be close enough:

PORT=$(( ((RANDOM<<15)|RANDOM) % 63001 + 2000 ))

Solution 4 - Bash

and here's one with Python

randport=$(python -S -c "import random; print random.randrange(2000,63000)")

and one with awk

awk 'BEGIN{srand();print int(rand()*(63000-2000))+2000 }'

Solution 5 - Bash

The simplest general way that comes to mind is a perl one-liner:

perl -e 'print int(rand(65000-2000)) + 2000'

You could always just use two numbers:

PORT=$(($RANDOM + ($RANDOM % 2) * 32768))

You still have to clip to your range. It's not a general n-bit random number method, but it'll work for your case, and it's all inside bash.

If you want to be really cute and read from /dev/urandom, you could do this:

od -A n -N 2 -t u2 /dev/urandom

That'll read two bytes and print them as an unsigned int; you still have to do your clipping.

Solution 6 - Bash

If you're not a bash expert and were looking to get this into a variable in a Linux-based bash script, try this:

VAR=$(shuf -i 200-700 -n 1)

That gets you the range of 200 to 700 into $VAR, inclusive.

Solution 7 - Bash

Here's another one. I thought it would work on just about anything, but sort's random option isn't available on my centos box at work.

 seq 2000 65000 | sort -R | head -n 1

Solution 8 - Bash

You can do this

cat /dev/urandom|od -N2 -An -i|awk -v f=2000 -v r=65000 '{printf "%i\n", f + r * $1 / 65536}'

If you need more details see Shell Script Random Number Generator.

Solution 9 - Bash

$RANDOM is a number between 0 and 32767. You want a port between 2000 and 65000. These are 63001 possible ports. If we stick to values of $RANDOM + 2000 between 2000 and 33500, we cover a range of 31501 ports. If we flip a coin and then conditionally add 31501 to the result, we can get more ports, from 33501 to 65001. Then if we just drop 65001, we get the exact coverage needed, with a uniform probability distribution for all ports, it seems.

random-port() {
    while [[ not != found ]]; do
        # 2000..33500
        port=$((RANDOM + 2000))
        while [[ $port -gt 33500 ]]; do
            port=$((RANDOM + 2000))
        done

        # 2000..65001
        [[ $((RANDOM % 2)) = 0 ]] && port=$((port + 31501)) 

        # 2000..65000
        [[ $port = 65001 ]] && continue
        echo $port
        break
    done
}

Testing

i=0
while true; do
    i=$((i + 1))
    printf "\rIteration $i..."
    printf "%05d\n" $(random-port) >> ports.txt
done

# Then later we check the distribution
sort ports.txt | uniq -c | sort -r

Solution 10 - Bash

Same with ruby:

echo $(ruby -e 'puts rand(20..65)') #=> 65 (inclusive ending)
echo $(ruby -e 'puts rand(20...65)') #=> 37 (exclusive ending)

Solution 11 - Bash

Bash documentation says that every time $RANDOM is referenced, a random number between 0 and 32767 is returned. If we sum two consecutive references, we get values from 0 to 65534, which covers the desired range of 63001 possibilities for a random number between 2000 and 65000.

To adjust it to the exact range, we use the sum modulo 63001, which will give us a value from 0 to 63000. This in turn just needs an increment by 2000 to provide the desired random number, between 2000 and 65000. This can be summarized as follows:

port=$((((RANDOM + RANDOM) % 63001) + 2000))

Testing

# Generate random numbers and print the lowest and greatest found
test-random-max-min() {
    max=2000
    min=65000
    for i in {1..10000}; do
        port=$((((RANDOM + RANDOM) % 63001) + 2000))
        echo -en "\r$port"
        [[ "$port" -gt "$max" ]] && max="$port"
        [[ "$port" -lt "$min" ]] && min="$port"
    done
    echo -e "\rMax: $max, min: $min"
}

# Sample output
# Max: 64990, min: 2002
# Max: 65000, min: 2004
# Max: 64970, min: 2000

Correctness of the calculation

Here is a full, brute-force test for the correctness of the calculation. This program just tries to generate all 63001 different possibilities randomly, using the calculation under test. The --jobs parameter should make it run faster, but it's not deterministic (total of possibilities generated may be lower than 63001).

test-all() {
    start=$(date +%s)
    find_start=$(date +%s)
    total=0; ports=(); i=0
    rm -f ports/ports.* ports.*
    mkdir -p ports
    while [[ "$total" -lt "$2" && "$all_found" != "yes" ]]; do
        port=$((((RANDOM + RANDOM) % 63001) + 2000)); i=$((i+1))
        if [[ -z "${ports[port]}" ]]; then
            ports["$port"]="$port"
            total=$((total + 1))
            if [[ $((total % 1000)) == 0 ]]; then
                echo -en "Elapsed time: $(($(date +%s) - find_start))s \t"
                echo -e "Found: $port \t\t Total: $total\tIteration: $i"
                find_start=$(date +%s)
            fi
        fi
    done
    all_found="yes"
    echo "Job $1 finished after $i iterations in $(($(date +%s) - start))s."
    out="ports.$1.txt"
    [[ "$1" != "0" ]] && out="ports/$out"
    echo "${ports[@]}" > "$out"
}

say-total() {
    generated_ports=$(cat "$@" | tr ' ' '\n' | \sed -E s/'^([0-9]{4})$'/'0\1'/)
    echo "Total generated: $(echo "$generated_ports" | sort | uniq | wc -l)."
}
total-single() { say-total "ports.0.txt"; }
total-jobs() { say-total "ports/"*; }
all_found="no"
[[ "$1" != "--jobs" ]] && test-all 0 63001 && total-single && exit
for i in {1..1000}; do test-all "$i" 40000 & sleep 1; done && wait && total-jobs

For determining how many iterations are needed to get a given probability p/q of all 63001 possibilities having been generated, I believe we can use the expression below. For example, here is the calculation for a probability greater than 1/2, and here for greater than 9/10.

Expression

Solution 12 - Bash

PORT=$(($RANDOM%63000+2001)) is close to what you want I think.

PORT=$(($RANDOM$RANDOM$RANDOM%63000+2001)) gets around the size limitation that troubles you. Since bash makes no distinctions between a number variable and a string variable, this works perfectly well. The "number" $RANDOM can be concatenated like a string, and then used as a number in a calculation. Amazing!

Solution 13 - Bash

Or on OS-X the following works for me:

$ gsort --random-sort

Solution 14 - Bash

You can get the random number through urandom

head -200 /dev/urandom | cksum

Output:

3310670062 52870

To retrieve the one part of the above number.

head -200 /dev/urandom | cksum | cut -f1 -d " "

Then the output is

3310670062

To meet your requirement,

head -200 /dev/urandom |cksum | cut -f1 -d " " | awk '{print $1%63000+2001}'

Solution 15 - Bash

This is how I usually generate random numbers. Then I use "NUM_1" as the variable for the port number I use. Here is a short example script.

#!/bin/bash
    
clear
echo 'Choose how many digits you want for port# (1-5)'
read PORT

NUM_1="$(tr -dc '0-9' </dev/urandom | head -c $PORT)"

echo "$NUM_1"

if [ "$PORT" -gt "5" ]
then
clear
echo -e "\x1b[31m Choose a number between 1 and 5! \x1b[0m"
sleep 3
clear
exit 0
fi
    

Solution 16 - Bash

This works for me:

export CUDA_VISIBLE_DEVICES=$((( RANDOM % 8 )))

you can add 1 if you want it to start from 1 instead of 0.

Solution 17 - Bash

Generating 50 numbers in Bash from a range 100000000000-999999999999 and saving them into a file filename.csv

shuf -i 100000000000-999999999999 -n 50 -o filename.csv

Solution 18 - Bash

$(((RANDOM % $((upperBound - lowerBound))) + lowerBound)) is everything you want, simple and compatible. The range is [lowerBound,upperBound).

Quick test:

for i in $(seq 100); do echo $(((RANDOM % $((65000 - 2000))) + 2000));done

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJason GoonerView Question on Stackoverflow
Solution 1 - Bashleedm777View Answer on Stackoverflow
Solution 2 - BasherrnoView Answer on Stackoverflow
Solution 3 - BashJesinView Answer on Stackoverflow
Solution 4 - Bashghostdog74View Answer on Stackoverflow
Solution 5 - BashCascabelView Answer on Stackoverflow
Solution 6 - BashBertoView Answer on Stackoverflow
Solution 7 - BashvaladilView Answer on Stackoverflow
Solution 8 - BashNew ExitView Answer on Stackoverflow
Solution 9 - Bashuser1726010View Answer on Stackoverflow
Solution 10 - BashLev LukomskyView Answer on Stackoverflow
Solution 11 - BashanonView Answer on Stackoverflow
Solution 12 - BashWastrelView Answer on Stackoverflow
Solution 13 - BashChadwick BoggsView Answer on Stackoverflow
Solution 14 - BashzangwView Answer on Stackoverflow
Solution 15 - BashYokaiView Answer on Stackoverflow
Solution 16 - BashCharlie ParkerView Answer on Stackoverflow
Solution 17 - BashSergey NemchinovView Answer on Stackoverflow
Solution 18 - BashShimingView Answer on Stackoverflow