mattintosh note

どこかのエンジニアモドキの備忘録

2024-06-05: 現在ホビー関連の記事を hobby.mattintosh-note.jp に移行しています。
現在掲載されている一部の画像と今後掲載される画像は特定の環境から閲覧できなくなります。

bash でランダムな三択とか乱数作ったりとか

シェルスクリプト書いてるときに外部コマンドを使わずにシェルだけでハッシュっぽいもの作れないかなと思ったので頭の体操。

bash には ${パラメータ:オフセット:長さ} という変数の展開方法があり、例えば ${パラメータ:オフセット:1} とすれば「"パラメータ"の n 番目を1個取り出す」ということができる。オフセットや長さの部分には算術式を用いることもできる。なお、オフセットはゼロから始まる。

bash には RANDOM という特殊変数があり、これは展開する度に 0 〜 32767 の数値がランダムで得られる。オフセットの値に関しては RANDOM 変数を元の文字数で割った余りを使えば文字数以上の長さになることはない。

p=0123456789abcdef
echo ${p:$((RANDOM%16)):1}

上の ${p:$((RANDOM%16)):1} の部分はいくつか書式が考えられるが上のがそこそこ短い書式。一番短いのは算術式を $[算術式] で書く方法だけど ksh とかで動かなくなるのである程度移植性の高い $((算術式)) がいいと思う。

# 普通
${p:$((RANDOM%16)):1}

# $(()) を $[] で置き換えたもの(移植性については謎)
${p:$[RANDOM%16]:1}

# 丁寧に書いた場合
${p:$((${RANDOM} % 16)):1}

# 文字数を可変に
${p:$((${RANDOM} % ${#p})):1}

ある程度の長さを得たい場合はループさせればよいだけ。1文字ずつ printf してもいいと思うけど最後にまとめてポンっと出力する方法。

p=0123456789abcdef
set -- # 位置パラメータをリセット
for ((i=0; i<64; i++))
do
    set -- ${*}${p:$((RANDOM%16)):1}
done
echo ${*}

関数化すれば引数で長さ指定ができる。関数化するときは関数の定義を function(){( コード; )} のようにサブシェルで実行するようにしておいた方がいいかもしれない。というのもこの関数を for ((i=0; i<100; i++)); do rand_hex; done みたいに呼び出すと関数の中で変数 i が常にリセットされて無限ループになる。length とか pdeclare -ireadonly で保護するようにしておいた方がいいと思う。

rand_hex(){(
    length=${1:-8} # 引数がなければ長さ8に設定
    p=0123456789abcdef
    set --
    for ((i=0; i<length; i++))
    do
        set -- ${*}${p:$((RANDOM%16)):1}
    done
    echo ${*}
)}

まぁ uuidgen とかの方が早いですわな。

$ time for ((i=0; i<100; i++)); do rand_hex 8; done >/dev/null
real    0m0.187s
user    0m0.137s
sys     0m0.060s

$ time for ((i=0; i<100; i++)); do rand_hex 32; done >/dev/null
real    0m0.289s
user    0m0.188s
sys     0m0.113s

$ time for ((i=0; i<100; i++)); do uuidgen; done >/dev/null
real    0m0.166s
user    0m0.100s
sys     0m0.072s

固定長なら若干速い。

fake_uuidgen(){(
    : ${p:=0123456789abcdef}
    echo \
${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}\
${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}-\
${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}-\
${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}-\
${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}-\
${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}\
${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}\
${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}${p:$((RANDOM%16)):1}
)}
$ time for ((i=0; i<100; i++)); do fake_uuidgen; done >/dev/null
ddfb8a1a-5cf0-1541-5da0-9fe735f83e3a
93013969-61fe-c430-18bb-0092cb0c69ac
7c29e654-b11d-5418-62dd-c906e26160b8
:
中略
:
real    0m0.193s
user    0m0.132s
sys     0m0.071s

適当に64桁を10件ほど。精度の程は知らんけどシェルだけで適当な乱数欲しいときはこれで十分かな。

     1  91f26b3f3b340a6fb4ae252d334c63a4494801d9968a085069924444f8b4ac13
     2  c8e2771989fb9374476efd2b4951e47c0fcbe604cb3033e787c8e7f1c5a61037
     3  405eb6b8fae038b64651ab10b3a6cc1ee211ee676097a0274a235a87929f22eb
     4  2643f3cbdf1bf75487b5dc2ae78fb9225100437d69a73ae4b52e2bce7a5e786f
     5  b1600fd13dc36a19c15bdc3f25a5bffd5580f1eefdcd8d5b1044743373576441
     6  46457b497e72d6f2650dd78c7991eb13638c4c1bfc8562ee61ec0eb3bb1f9b26
     7  61729017c41b2e72d797341697168631cebf8396e9e6505dbe0e7b199298ceb1
     8  88d3a91c8d266c02e8ae75a6c3d6da6a06bdaa46f0b73cb9b3eed6c1d3c4d06c
     9  4c8b7d7cefa16e9877292e9927de7d20d946c7522726f0d349f53fe4ff859a60
    10  6c42f5c60b1c0c9daa9db899f892a41c098fae3cb41eeb670ba4859505217282

まぁそれなりにバラけてるのではなかろうか。











先頭2桁でグレースケール。










同じく 6 桁の 16 進数を取り出して RGB 値のように変換して総計を追加して CSV で書き出す。

for ((i=0; i<100; i++))
do
    echo rand_hex 6
done |
while read
do
set -- $((16#${REPLY:0:2})) $((16#${REPLY:2:2})) $((16#${REPLY:4:2}))
echo $1 $2 $3 $(($1+$2+$3))
done |
awk 'BEGIN { OFS="," } { print NR,$1,$2,$3,$4 }'
1,61,44,0,105
2,58,242,76,376
3,205,120,177,502
:
中略
:
98,43,3,9,55
99,223,91,110,424
100,66,251,228,545

んで gnuplot で見てみる。

gnuplot \
-e 'set datafile separator ","' \
-e 'plot "1.csv" using 1:2 with lines, "" using 1:3 with lines, "" using 1:4 with lines, "" using 1:5 with lines' \
-e 'pause -1'

f:id:mattintosh4:20191026085534p:plain
rand_hex

う〜ん…うまくバラけてるようなバラけてないような。一応 uuidgen の結果も見てみる。

f:id:mattintosh4:20191026091219p:plain
uuidgen

細かく見りゃ違うのかもしれんがパット見はあんまり差は無さそう。


2進、8進、10進、16進で作っておくと汎用性あるかな。

rand(){( length=${1:-8} type=${2:-hex}
    case ${type} in 
    bin|binary)
        p=01
    ;;
    oct|octal)
        p=01234567
    ;;
    dec|decimal)
        p=0123456789
    ;;
    hex|hexdecimal|*)
        p=0123456789abcdef
    ;;
    esac
    for ((i=0; i<length; i++))
    do
        printf "%s" "${p:$((RANDOM%${#p})):1}"
    done
)}
$ rand
0f8951b5
$ rand 8
1efecfbf
$ rand 16 bin
0011000111110000
$ rand 32 octal
74103727514173627606361250035140
$ rand 64 dec
4802980765383038512696398596756822988485684845342237617436529278
$ rand 128
55aa92646fb91f75a3b3aa57c9edbe34f44965f64497a5108351302459cc711deaec83d5656e7c1f85cddc1be2402107cff984ba4c75ead9236106a4fde13478

本当は単に A B C のうちどれかをランダムで返してくれるだけでいいものを作っていたんだけどな…。

p=abc; echo ${p:$((RANDOM%3)):1}

GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)