シェルスクリプト書いてるときに外部コマンドを使わずにシェルだけでハッシュっぽいもの作れないかなと思ったので頭の体操。
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
とか p
は declare -i
や readonly
で保護するようにしておいた方がいいと思う。
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'
う〜ん…うまくバラけてるようなバラけてないような。一応 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}