Jason Gooner
Jason Gooner

Reputation: 3281

Random number from a range in a Bash Script

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?

Upvotes: 295

Views: 268406

Answers (19)

Lev Lukomskyi
Lev Lukomskyi

Reputation: 6667

Same with ruby:

ruby -e 'puts rand(2000...65000)'

Upvotes: 7

Alfred.37
Alfred.37

Reputation: 149

If you need a range bigger than 15 bit, dont use the slow unsecure and outdated 15 bit RANDOM, use the fast and secure 32 bit SRANDOM.

SRANDOM are available since about 2021 bash 5.1 roll out.

"one interesting addition to note with Bash 5.1 is the new SRANDOM variable. The SRANDOM variable provides random data from the system's entropy engine and cannot be reseeded. In particular, the SRANDOM variable provides a 32-bit random number that relies upon getrandom/getentropy -- with fall-backs to /dev/urandom or arc4random or even another fallback after that if necessary."

Source: https://www.phoronix.com/news/GNU-Bash-5.1

See what are the different of RANDOM and SRANDOM in bash:

Difference between RANDOM and SRANDOM in Bash

Feel free to improve this answer.

Upvotes: 1

Shiming
Shiming

Reputation: 171

Generate random numbers in the range [$floor,$ceil), no dependence:

$(((RANDOM % $(($ceil- $floor))) + $floor))

Generate 100 numbers between 2000 to 65000:

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

Upvotes: 4

Sergey Nemchinov
Sergey Nemchinov

Reputation: 1616

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

Upvotes: 0

Charlie Parker
Charlie Parker

Reputation: 5201

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.

Upvotes: 0

Yokai
Yokai

Reputation: 1220

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

Upvotes: 1

New Exit
New Exit

Reputation: 61

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.

Upvotes: 6

zangw
zangw

Reputation: 48346

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}'

Upvotes: 2

Berto
Berto

Reputation: 712

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.

Upvotes: 15

user1726010
user1726010

Reputation:

$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

Upvotes: 5

Jesin
Jesin

Reputation: 1019

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 ))

Upvotes: 61

anon
anon

Reputation:

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

Upvotes: 5

Wastrel
Wastrel

Reputation: 39

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!

Upvotes: 3

Chadwick Boggs
Chadwick Boggs

Reputation: 31

Or on OS-X the following works for me:

$ gsort --random-sort

Upvotes: 2

valadil
valadil

Reputation: 1693

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

Upvotes: 7

errno
errno

Reputation: 981

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

jot -r 1  2000 65000

Upvotes: 98

ghostdog74
ghostdog74

Reputation: 342273

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 }'

Upvotes: 51

Cascabel
Cascabel

Reputation: 496722

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.

Upvotes: 19

leedm777
leedm777

Reputation: 24032

shuf -i 2000-65000 -n 1

Enjoy!

Edit: The range is inclusive.

Upvotes: 555

Related Questions