bash-hackers-wiki/original_source/snipplets/rndstr.txt

94 lines
4.6 KiB
Plaintext
Raw Permalink Normal View History

====== 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.
<code bash>
# 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
}
</code>
This example prints 10 random positional parameters and operates on basically the same principle as the ''rndstr'' function above.
<code>
~ $ ( set -- foo bar baz bork; printf '%s ' "${!_[_=RANDOM%$#+1,0]"{0..10}"}"; echo )
bork bar baz baz foo baz baz baz baz baz bork
</code>
<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. :/
<code bash>
# 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
</code>
</div>
The remaining examples don't use quite the same tricks, which will hopefully be explained elsewhere eventually. See [[commands/builtin/unset#scope | unset]] for why doing assignments in this way works well.
This next example is a variation on [[snipplets:print_horizontal_line]]. We're using the printf field width specifier to truncate the values of a ''sequence expansion'' to one character.
<code bash>
a=({a..z} {A..Z} {0..9})
printf '%.1s' "${a[RANDOM%${#a[@]}]}"{0..9} $'\n'
</code>
The extra detail that makes this work is to notice that in Bash, [[syntax/expansion/brace | 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 [[syntax:arith_expr|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:
<code bash>
printf "%.s${a[RANDOM%${#a[@]}]}" {0..9}
</code>
Selecting random elements whose lengths are not fixed is harder.
<code bash>
a=(one two three four five six seven eight nine ten)
printf '%.*s ' $(printf '%s ' "${#a[x=RANDOM%${#a[@]}]} ${a[x]}"{1..10})
</code>
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.
<code bash>
a=(one two three)
echo "${a[RANDOM%${#a[@]}]}"{,}{,,,,}
</code>