bash-hackers-wiki/docs/snipplets/rndstr.md
2024-03-30 14:09:26 -05:00

4.7 KiB

Print a random string or select random elements

---- dataentry snipplet ---- snipplet_tags: terminal, line LastUpdate_dt: 2013-04-30 Contributors: Dan Douglas (ormaaj) type: snipplet


First off, here is a fast / reliable random string function for scripts or libraries which can optionally assign directly to a variable.

# Print or assign a random alphanumeric string of a given length.
# rndstr len [ var ]
function rndstr {
    if [[ $FUNCNAME == "${FUNCNAME[1]}" ]]; then
        unset -v a
        printf "$@"
    elif [[ $1 != +([[:digit:]]) ]]; then
        return 1
    elif (( $1 )); then
        typeset -a a=({a..z} {A..Z} {0..9})
        eval '${2:+"$FUNCNAME" -v} "${2:-printf}" -- %s "${a[RANDOM%'"${#a[@]}"']"{1..'"$1"'}"}"'
    fi
}

This example prints 10 random positional parameters and operates on basically the same principle as the rndstr function above.

 ~ $ ( set -- foo bar baz bork; printf '%s ' "${!_[_=RANDOM%$#+1,0]"{0..10}"}"; echo )
bork bar baz baz foo baz baz baz baz baz bork 

<div hide> This has some interesting option parsing concepts, but is overly complex. This is a good example of working too hard to avoid an eval for no benefit and some performance penalty. :/

# Print or assign a random alphanumeric string of a given length.
# rndstr [ -v var ] len
# Bash-only
rndstr()
    if [[ $FUNCNAME == "${FUNCNAME[1]}" ]]; then
        # On recursion, this branch unsets the outer scope's locals and assigns the result.
        unset -v a b
        printf -v "$1" %s "${@:2}"
    elif ! { [[ $1 == -v ]] && shift; }; [[ $?+1 -ne $# || ${!#} != +([[:digit:]]) || ( $? -gt 0 && -z $1 ) ]]; then
        # This branch does input validation, strips -v, and guarantees we're left with either 1 or 2 args.
        return 1
    elif (( ! ${!#} )); then
        # If a zero-length string is requested, return success.
        return
    else
        # This line generates the string and assigns it to "b".
        local -a a=({a..z} {A..Z} {0..9}) 'b=("${a[RANDOM%'"${#a[@]}"']"{1..'"${!#}"'}"}")'
        if (( $# == 2 )); then
            # If -v, then pass a variable name and value to assign and recurse once.
            "$FUNCNAME" "$1" "${b[@]}"
        else
            # If no -v, write to stdout.
            printf %s "${b[@]}"
        fi
    fi

</div>

The remaining examples don't use quite the same tricks, which will hopefully be explained elsewhere eventually. See unset for why doing assignments in this way works well.

This next example is a variation on print_horizontal_line. We're using the printf field width specifier to truncate the values of a sequence expansion to one character.

a=({a..z} {A..Z} {0..9})
printf '%.1s' "${a[RANDOM%${#a[@]}]}"{0..9} $'\n'

The extra detail that makes this work is to notice that in Bash, brace expansion is usually the very first type of expansion to be processed, always before parameter expansion. Bash is unique in this respect -- all other shells with a brace expansion feature perform it almost last, just before pathname expansion. First the sequence expansion generates ten parameters, then the parameters are expanded left-to-right causing the arithmetic for each to be evaluated individually, resulting in independent selection of random element of a. To get ten of the same element, put the array selection inside the format string where it will only be evaluated once, just like the dashed-line trick:

printf "%.s${a[RANDOM%${#a[@]}]}" {0..9} 

Selecting random elements whose lengths are not fixed is harder.

a=(one two three four five six seven eight nine ten)
printf '%.*s ' $(printf '%s ' "${#a[x=RANDOM%${#a[@]}]} ${a[x]}"{1..10})

This generates each parameter and it's length in pairs. The '*' modifier instructs printf to use the value preceding each parameter as the field width. Note the space between the parameters. This example unfortunately relies upon the unquoted command substitution to perform unsafe wordsplitting so that the outer printf gets each argument. Values in the array can't contain characters in IFS, or anything that might be interpreted as a pattern without using set -f.

Lastly, empty brace expansions can be used which don't generate any output that would need to be filtered. The disadvantage of course is that you must construct the brace expansion syntax to add up to the number of arguments to be generated, where the most optimal solution is its set of prime factors.

a=(one two three)
echo "${a[RANDOM%${#a[@]}]}"{,}{,,,,}