8

I have an example where I am attempting to randomly generate a letter in a set of letters (i.e. var="abcdefghijklmnopqrstuvwxyz").

How would I have:

var="abcdefghijklmnopqrstuvwxyz"

echo "${var}"

yield: some_letter

The point is, I’m trying to automatically generate a random letter from a selection of my choosing. Whether it be in an array or a string does not matter.

9
  • Arrays and strings are two different datatypes Commented Feb 17, 2018 at 2:50
  • 1
    Which shell, by the way? You also tagged this for sh, but if you need sh compatibility, some bash-only techniques aren't available. Commented Feb 17, 2018 at 2:52
  • 1
    Stack Overflow is not a code writing service. Please show your code. Since Stack Overflow hides the Close reason from you: Questions seeking debugging help ("why isn't this code working?") must include the desired behavior, a specific problem or error and the shortest code necessary to reproduce it in the question itself. Questions without a clear problem statement are not useful to other readers. See: How to create a Minimal, Complete, and Verifiable example. Commented Feb 17, 2018 at 3:20
  • What do you mean, "variable arrays"? This isn't an array at all. And if you're working in bash, why did you roll back my edit removing the sh tag? Commented Feb 17, 2018 at 3:23
  • 1
    (Similarly, the terminal tag is only for questions about terminals -- which this isn't -- and the linux tag is for questions that are specific to Linux, which this likewise isn't). Commented Feb 17, 2018 at 3:23

3 Answers 3

14
var="abcdefghijklmnopqrstuvwxyz"
echo "${var:$(( RANDOM % ${#var} )):1}" # pick a 1 char substring starting at a random position

This works because:

  • ${var:START:LEN} is a parameter expansion that expands to a substing of $var
  • ${#var} is a parameter expansion that expands to the length of the contents of the string variable var
  • $(( )) creates an arithmetic context, in which context non-numeric strings are assumed to refer to variable names (so one can use RANDOM instead of $RANDOM).
  • $RANDOM, each time it is evaluated, expands to a random integer between 0 and 32767.
  • $RANDOM % ${#var} takes the remainder of dividing that random integer by the number of characters in the string named var; consequently, it will be between 0 and (length-of-var - 1), and will be almost randomly divided (if the length-of-var doesn't divide into 32768 evenly, then some of the characters will have a very slightly higher chance of being chosen than others).

Thus, ${var:$(( RANDOM % ${#var} )) : 1} will, each time it is evaluated, pick a location inside the string, and expand to a single-character span within it.

Sign up to request clarification or add additional context in comments.

Comments

10

For most practical cases, the solution of Charles Duffy is the way forward. However, if your random character picking has to be uniform, then the story becomes slightly more complicated when you want to use RANDOM (see explanation below). The best way forward would be the usage of shuf. shuf generates a random permutation of a given range and allows you to pick the first number like shuf -i 0-25 -n1, so you could use

var="abcdefghijklmnopqrstuvwxyz"
echo ${var:$(shuf -i 0-$((${#var}-1)) -n1):1}

The idea here is to pick a letter from the string var by using the pattern expansion ${var:m,n} where you pick a substring starting at m of length n. The length is set to 1 and the starting position is defined by the command shuf -i 0-$((${#var}-1) which shuffles a range between 0 and ${#var}-1 where ${#var} is the string length of the variable var.


Why not using RANDOM:

The random variable RANDOM generates a pseudo-random number between 0 and 32767. This implies that if you want to generate a random number between 0 and n, you cannot use the mod. The problem here is that the first 32768%n numbers will have a higher chance to be drawn. This is easily seen with the following script :

% for i in {0..32767}; do echo $((i%5)); done | sort -g | uniq -c
   6554 0
   6554 1
   6554 2
   6553 3  < smaller change to hit 3
   6553 4  < smaller chance to hit 4

Another classic approach is to map the range of the random number generator onto the requested range by scaling the random value as n*RANDOM/32768. Unfortunately, this only works for a random number generator that generate real numbers. RANDOM generates an integer. The integer scaling essentially shuffles the earlier problem:

% for i in {0..32767}; do echo $((5*i/32768)); done | sort -g | uniq -c
   6554 0
   6554 1
   6553 2  < smaller chance to hit 2
   6554 3
   6553 4  < smaller chance to hit 4

If you want to use RANDOM, the best way is to skip the values which are not needed, this you can do with a simple while loop

var="abcdefghijklmnopqrstuvwxyz"
n=${#var}
idx=32769; while (( idx >= (32768/n)*n )); do idx=$RANDOM; done
char=${var:$idx:1}

note: it is possible that you are stuck for an eternity in the while loop.

Comment: we do not comment on how good the random number generator behind RANDOM is. All we do is cite the comment in the source :

source bash 4.4.18 (variables.c)

/* A linear congruential random number generator based on the example
   one in the ANSI C standard. This one isn't very good, but a more
   complicated one is overkill.
*/

1 Comment

Good call. Definitely appropriate to be cautious if one were to be using this mechanism for cryptographic keys or such.
5

You can also use shuf in a maybe more concise way.

Basically you split the string into a char on each line and then use shuf the normal way:

Using fold for splitting

echo "abcdefghijklmnopqrstuvwxyz" | fold -w1 | shuf -n1

Using grep for splitting

echo "abcdefghijklmnopqrstuvwxyz" | grep -o . | shuf -n1

1 Comment

Nice 1 liner, if you find a way to make it shorter let me know. Thank You!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.