Edited at

MとかGとかをbyteに変換するキモいスクリプト

More than 1 year has passed since last update.


はじめに

お仕事で200Mという文字列をバイトに変換する場面があったので、可読性の高いスクリプトを書いたのですが、

可読性を落としてキモく書こうと思ったら、どこまで出来るのかなぁと思い、やってみました。

で、可読性が低いと自分が数か月後に見た時にわからないので、Qiitaに解説を書いておきます。

需要など知らぬ存ぜぬ顧みぬ。


実装1

スクリプトは以下です。引数に「200M」とか「500P」とかを期待してます。

尚、Exaで算術式展開が限界をお迎えになられるので、Petaまでの対応です。


to_byte.sh

     1  #!/bin/bash

2
3 RC=1 # ReturnCode
4 PREFIX=("" "K" "M" "G" "T" "P")
5
6 for i in $(eval echo {0..$(( ${#PREFIX[*]} - 1 ))})
7 do
8 if [ "${1//[0-9]}" == "${PREFIX[$i]}" ] ; then
9 byte=$(( ${1%${PREFIX[$i]}} * 1024 ** ${i} ))
10 echo "${1} is ${byte}byte"
11 RC=0
12 break
13 fi
14 done
15
16 exit ${RC}


result

# ./to_byte.sh 5G

5G is 5368709120byte

では解説します。


for i in $(eval echo {0..$(( ${#PREFIX[*]} - 1 ))})


${#PREFIX[*]}でパラメーター展開(Parameter Expansion)をして、配列PREFIXの長さ(要素数)を取得します。

ループは0から開始したいので、算術式展開(Arithmetic Expansion)$(( ))で6-1を計算してます。

で、ブレース展開(Brace Expansion){0..5}をする…んですが、

ブレース展開はほかのどの展開よりも前に実行されるので、

算術式展開の結果の5が無い、{0..}という状態での展開になってしまいます。

ですので、eval echoを嚙ませることによって再展開します。

あれ、でもこれ${!PREFIX[*]}でいいんじゃ…。


if [ "${1//[0-9]}" == "${PREFIX[$i]}" ] ; then


パラメーター展開${1//[0-9]}で引数の数字部分を削除し、プレフィックスを取得してます。

${parameter/pattern/string}でparameter内のpatternをstringで置換となります。

patternが/で始まった場合は、patternにマッチした部分はすべてstringで置換されます。

また、patternの後の/は省略可能です。なので、省略しないで書くなら${1//[0-9]/}ですね。


byte=$(( ${1%${PREFIX[$i]}} * 1024 ** ${i} ))


パラメーター展開${1%${PREFIX[$i]}でMとかGとかを捨ててます。

${parameter%word}でparameter内のwordにマッチした部分を除去します。

で、PREFIXのインデックス乗でbyteを計算してます。

PREFIX配列の最初を空文字にしているのはこれがやりたいからだったりします。

解説は以上です。

んーでもどうせなら一覧で出したいなぁ。


実装2

ということで、与えられた文字列をbyte,KiB,MiB...にした場合どうなるのってのを表示するスクリプトにします。

ついでに、実装1でダメだったところを改善します。


to_byte2.sh

     1  #!/bin/bash

2
3 RC=1 # ReturnCode
4 PREFIX=("" "K" "M" "G" "T" "P" "E" "Z" "Y")
5
6 for i in ${!PREFIX[*]}
7 do
8 if [ "${1//[0-9]}" == "${PREFIX[$i]}" ] ; then
9 byte=$(bc -l <<< "${1%${PREFIX[$i]}} * 1024 ^ ${i}")
10 RC=0
11 break
12 fi
13 done
14
15 if [ ${RC} -eq 0 ] ; then
16 {
17 for j in ${!PREFIX[*]}
18 do
19 calc_result=$(bc -l <<< "${byte} / 1024 ^ ${j}")
20 echo ${PREFIX[$j]:=B} ${calc_result/#./0.}
21 done
22 } | ruby -pe '$_.gsub!(/(\..*[^0]+)0+$|\..*0$/,"\\1")'
23 fi
24
25 exit ${RC}


result

# ./to_byte2.sh 555Z

B 655228349498163273400320
K 639871435056800071680
M 624874448297656320
G 610228953415680
T 595926712320
P 581959680
E 568320
Z 555
Y 0.5419921875

では見ていきましょう。


for i in ${!PREFIX[*]}


実装1で気づいたので変えました。この書き方で配列のインデックスのリストが取得できます。


byte=$(bc -l <<< "${1%${PREFIX[$i]}} * 1024 ^ ${i}")


算術式展開の計算結果がしょぼいので、bcに噛ませるようにしました。

<<<の部分はヒアストリング(Here Strings)と言って、後ろの部分が展開されて、

コマンドの標準入力に与えられます。

echo hogehoge | bc -lの代わりに使えます。


echo ${PREFIX[$j]:=B} ${calc_result/#./0.}


パラメーター展開で${parameter:=word}は、

parameterが設定されていないか空文字であればwordを代入するというものです。

バイトのところもなんか文字列入れてあげないと表示が崩れるので、とりあえずBとしました。

その後ろの${parameter/#pattern/string}は、

parameterの先頭にpatternがある場合stringに置換する、です。#が先頭を表します。

bcの計算結果で1未満の小数(0.xxx..)となるとの最初の0が省略されるので、これで付与します。


} | ruby -pe '$_.gsub!(/(\..*[^0]+)0+$|\..*0$/,"\\1")'


まず、16行目と22行目の{}はグループコマンド(group command)と呼ばれるものです。

{}内の出力を束ねて、一括で文字列処理したりファイル出力したりするのに使います。

今回は、rubyにワンライナーで文字列処理を任せています。

rubyオプションの-eはワンライナー必須のオプションで、続けて書いたスクリプトを実行します。

-pは、ループして入力を1行ずつ処理します。現在行は$_という変数に格納され、ループ終了毎に出力されます。

bcの結果の小数点以下の0の羅列がかなり邪魔なので、正規表現で取っ払います。

尚、-p$_を出力するという特性上、gsubは破壊的置換でいきます。

(\..*[^0]+)0+$|\..*0$/,"\\1"は、ピリオド\.から0以外の数字の繰り返し[^0]+の部分をキャプチャして、

行末までの0の繰り返し0+$を取り除くか、それに引っかからなければ、ピリオドから行末の0までを削除する、

という感じの正規表現です。


多分一個一個のキモテクをちゃんと書いた方がためになりますね。

キモテク実用編(という名の備忘録)ということにしておきます…。