「何分の一で」とかの情報は出てくるんだけど、知りたかったことそのものズバリの「何パーセントの確率でアレをやる」という物がなかなかパッとは出てこなかったのでまとめてみました。
シェルスクリプトで乱数
まず根底にある「ランダムに」っていう所だけど、これはBashかそうでないかでやり方が変わる。
Bashでは$RANDOM
を参照すると0から32767の範囲でランダムな結果が得られる。
$ echo $RANDOM
15999
Bash以外では、/dev/urandom
とod
コマンドを組み合わせて似たような事ができるようだ。
$ od -vAn --width=4 -tu4 -N4 </dev/urandom
1939740834
0~N-1の範囲で乱数を得る
以下、説明を簡単にするために$RANDOM
の方でコードを書くけど、違うシェルでは適宜読み替えて下さいという事で。
あと、ここからは数値計算が出てくるので、中に書いた式を計算した結果を得る$((計算式))
の書き方(算術展開)を使っていく。
で、0~N-1の範囲でランダムに1つを選ぶ方法。
これは割り算の余りを使う。
乱数をNで割った余りを求めれば、0~N-1のいずれかの数字が得られる。
例えば$(($RANDOM % 10))
とすれば、0~9のいずれかの数字が得られる(つまり、10パターンに分岐できる)。
$ echo $(($RANDOM % 10))
0
$ echo $(($RANDOM % 10))
5
$ echo $(($RANDOM % 10))
3
1/Nの確率で何かやる
先の結果がどれか1つの選択肢に等しくなった時だけ処理を実行すれば、「約1/Nの確率で実行」ということになる。
[ $(($RANDOM % 3)) -eq 0 ]
なら、約1/3の確率で真になり&&
以下が実行される。
$ [ $(($RANDOM % 3)) -eq 0 ] && echo 'Run!'
$ [ $(($RANDOM % 3)) -eq 0 ] && echo 'Run!'
Run!
$ [ $(($RANDOM % 3)) -eq 0 ] && echo 'Run!'
ここまではすぐ例文が出てくるんだけど、ここから先が出てこなかったので自分で考える必要があった。
N%の確率で何かやる
実際に「ランダムに何かをやりたい」時というのは、多分、だいたいは「パーセンテージとか割合で頻度を指定したい」って場面だと思う。
「60%の確率で分岐したい」みたいな。
これは、「1/Nの確率で」の例を発展させるとできる。
1/100までの精度だったら、まず0~99のいずれか1つをランダムに得る。
次に、これを-lt
演算子(less thanだから、左辺が右辺より小さい<
の意味)で「何パーセントでやりたい」という数字と比較する。
結果が真の時だけ処理を実行すれば、つまり「何パーセントの確率で実行」ということになる。
絵を描くのが面倒なのでアスキーアートでやると、
0--------------------99
こういう数直線があって
0-----+-------------99
↑30
この位置に線を引いて、0から99までのどれか1つをランダムに選んだ結果が線より左にある時だけ実行するということです。
↓この時だけ実行 ↓こっちだったら実行しない
○ ○ × × ×
0-----+-------------99
↑30
これを踏まえて、30%の確率でRun!
という文字列を出すコマンド列なら、以下のようになる。
$ if [ $(($RANDOM % 100)) -lt 30 ]; then echo 'Run!'; fi
30
の所を変えれば任意のパーセンテージにできる。
関数にするならこんな感じか。
run_with_probability() {
local probability=$1
if [ $(($RANDOM % 100)) -lt $probability ]
then
echo 'Run!'
fi
}
ほんとに狙ったとおりの結果を得られているか、同じ物を1000回くらい繰り返し実行して確かめてみる。
与えた数の連番を出力するseq
コマンドとfor
ループを組み合わせて、先の関数を1000回実行し、Run!
が出力される頻度を見てみる。
(for
ループの出力結果をパイプラインでwc -l
に渡して行数を数えれば、実際に出力された回数が分かる。)
$ for i in $(seq 1000); do run_with_probability 30; done | wc -l
303
$ for i in $(seq 1000); do run_with_probability 30; done | wc -l
292
$ for i in $(seq 1000); do run_with_probability 30; done | wc -l
316
1000回中の300回前後なので、まあだいたい30%になっている。
ばらつきがあるけど、試行回数を増やせば指定のパーセンテージに収束していくはず。
実際は「一定の確率で文字列を出力する」というのを汎用的にやりたかったので、こういう風にした。
probability() {
[ $(($RANDOM % 100)) -lt $1 ] && cat
}
# 95%の確率で出力→だいたいは出力される
output_message | probability 95
# 10%の確率で出力→滅多に出ない
output_message | probability 10
入力された複数行の中からランダムに1行抜き出す
ちょっと毛色が違うけど、これもついでに。
入力に対してその中からランダムに1つをピックアップするという場面だけど、これはコメントで指摘を頂いたとおり、そのものずばりのshuf
というコマンドがある。これは標準入力で受け取った内容を行ごとにシャッフルして出力するコマンドで、-n
で取り出す行数を指定できるので、以下のようにすれば「ランダムに1行取り出す」という結果になる。
# 他のコマンドから渡された結果の中からランダムに1行を出力してみる
read_messages | shuf -n 1
shuf
コマンドの存在を知らなかった時にそれを使わずに解いてみた時には、先の「0~N-1のいずれかを得る」の応用で以下のようにしてた。
choose_random_one() {
// 標準入力を一旦変数に保持
local input="$(cat)"
// 入力の行数を得る
local n_lines="$(echo "$input" | wc -l)"
// 「1~最終行の行番号」の範囲でどれか1つを得る
local index=$(( ($RANDOM % $n_lines) + 1 ))
// 得た行番号を使って、sedで「指定された番号の行だけを取り出す」操作を行う
echo "$input" | sed -n "${index}p"
}
# 他のコマンドから渡された結果の中からランダムに1行を出力してみる
read_messages | choose_random_one
入力を「行数を数える時」と「実際に抽出する時」の2回使わないといけないので、一旦全部cat
で読み取って変数に保持してるというのがポイントでしょうか。
まとめ
ということで、「シェルスクリプトでランダムにアレをやる」色々でした。
なんでこんな事やってるかというと、シス管系女子の宣伝を自動化したくて、宣伝用アカウントの運用をボットにやらせたかったのですが、「コマンド&シェルスクリプト」の連載なんだからボットもシェルスクリプトの方がネタになるよね&自分で作れば「お、作者はちゃんと技術分かってる人なんだな」と技術的な信頼に繋がるかな?と思って、TwitterクライアントとボットをBashでゴリゴリ書いているからなのでした。BtoBの仕事だったり実用のアドオンだったりでしかコード書いてないと、一定の確率で何かやるという事が必要になる場面が全く無くて(確実に何かやる、という事ばっかりだから……)、ぱっとやり方を思いつけなくて参りました。
(この記事の内容は自サイトのエントリの転載です)