Rangeから配列
Rangeから配列を作るのはよくあることで、プログラミング言語の標準ライブラリでサポートされていることがほとんど。
Rubyなら次のように書ける。
$ ruby -e 'p (1..9).to_a'
[1, 2, 3, 4, 5, 6, 7, 8, 9]
Bashならこう。
$ echo {1..9}
1 2 3 4 5 6 7 8 9
配列からRange
仕様
今回やりたいことはこの逆で、数値の並びからRangeの表記に書き換えること。
$ a=$(seq 3 && seq 7 9 && seq 15 15 && seq 30 36)
$ echo "$a"
1
2
3
7
8
9
15
30
31
32
33
34
35
36
上記をこのように表示したい。
1..3
7..9
15
30..36
Rubyで素直に書く
Rubyで素直に書くと以下のようになる。
#!/usr/local/bin/ruby
arr = readlines.map(&:to_i)
rng_arr = [arr.first]
prev = arr.first
arr[1..-1].each do |i|
rng_arr << prev << i unless prev + 1 == i
prev = i
end
rng_arr << arr.last
rng_arr.each_slice(2){|a| puts a[0] == a[1] ? a[0] : a.join("..")}
配列の一番目の要素は必ず必要なのでRange生成用の配列に格納し、そのあとは各要素で前の値に1足した数値でなければ配列に追加している。また最後の要素も必ず必要なのでRange生成用の配列に格納する。あとは配列からeach_slice
で二つずつ値を取り出し、..
で繋げて標準出力する。
$ echo "$a" | ruby arr_to_rng.rb
1..3
7..9
15
30..36
Rubyでワンライナーで書く
ワンライナーに挑戦してみる。
方法としては、配列をズラして組み合わせることで、前後の値の比較をしすくする。配列をズラして組み合わせる方法を二つ書く。
前後に要素を追加してzip
正数を対象にしていると仮定して、-1
を配列の前に足したものと配列の後に足したものをzip
する。
$ echo "$a" | ruby -e 'arr = readlines.map(&:to_i); p ([-1] + arr).zip(arr + [-1])'
[[-1, 1], [1, 2], [2, 3], [3, 7], [7, 8], [8, 9], [9, 15], [15, 30], [30, 31], [31, 32], [32, 33], [33, 34], [34, 35], [35, 36], [36, -1]]
素直にスクリプトを書いたときはprev
に値を代入していたが、これで代入なしで各要素の配列で前後が比べやすくなった。
生成した配列にselect
を使って前後の値が異なるものを取り出す。
$ echo "$a" | ruby -e 'arr = readlines.map(&:to_i); p ([-1] + arr).zip(arr + [-1]).select{|x| x[0] + 1 != x[1]}'
[[-1, 1], [3, 7], [9, 15], [15, 30], [36, -1]]
あとはflatten
してから-1
を消してあげるとRange生成用の配列ができる。
$ echo "$a" | ruby -e 'arr = readlines.map(&:to_i); ([-1] + arr).zip(arr + [-1]).select{|x| x[0] + 1 != x[1]}.flatten.reject{|i| i == -1}.each_slice(2){|a| puts a[0] == a[1] ? a[0] : a.join("..")}'
1..3
7..9
15
30..36
rotate
してzip
「配列をズラす」そのままのメソッドがArrayにある。
$ echo "$a" | ruby -e 'arr = readlines.map(&:to_i); p arr.zip(arr.rotate)'
[[1, 2], [2, 3], [3, 7], [7, 8], [8, 9], [9, 15], [15, 30], [30, 31], [31, 32], [32, 33], [33, 34], [34, 35], [35, 36], [36, 1]]
先ほどと同じようにselect
する。
$ echo "$a" | ruby -e 'arr = readlines.map(&:to_i);p arr.zip(arr.rotate).select{|x| x[0] + 1 != x[1]}'
[[3, 7], [9, 15], [15, 30], [36, 1]]
rotate
してズラしたから、上記の結果も一番はじめの要素が一番最後の要素になって移動してしまっている。flatten
してから今度は逆方向にrotate
することで、一番はじめの要素が一番はじめに戻ってくる。
$ echo "$a" | ruby -e 'arr = readlines.map(&:to_i); arr.zip(arr.rotate).select{|x| x[0] + 1 != x[1]}.flatten.rotate(-1).each_slice(2){|a| puts a[0] == a[1] ? a[0] : a.join("..")}'
1..3
7..9
15
30..36
実用例
連番を含む数値が大量にあれば、それを資料に記載したり人に伝えたりする際に、Range化すると可読性が上がるため便利。
他にも次のような実用例が考えられる。
- 開発サーバで複数のアプリがポートを分けて常駐している
- 一時的にiptablesでアプリのポートを解放し、外部からアクセスさせたい
今回はアプリがNode.jsだとする。netstatの結果から起動しているアプリのポートの一覧を取得し、先ほど書いたワンライナーに食わせる。
$ ports=$(netstat -antp | grep `hostname -i` | grep node | awk '{sub(/.*:/,"",$4); print $4}' | sort -u)
$ echo "$ports"
7060
7061
7062
7063
7064
7065
7066
7067
ポートの一覧は上記の通り。
/etc/sysconfig/iptablesに記載する文字列をワンライナーで生成する。
$ echo "$ports" | ruby -e 'arr = readlines.map(&:to_i); arr.zip(arr.rotate).select{|x| x[0] + 1 != x[1]}.flatten.rotate(-1).each_slice(2){|a| puts a[0] == a[1] ? a[0] : a.join(":")}' | xargs -I% echo -A RH-Firewall-1-INPUT -m multiport -p tcp --dports % -j ACCEPT
-A RH-Firewall-1-INPUT -m multiport -p tcp --dports 7060:7067 -j ACCEPT