labunix's blog

labunixのラボUnix

ブレース展開と1文字を対象にした正規表現の連続リスト形式への変換

■ブレース展開と1文字を対象にした正規表現の連続リスト形式への変換
 bashは5.0.3(1)、awkはgawkで4.2.1, API: 2.0 (GNU MPFR 4.0.2, GNU MP 6.1.2)

$ dpkg -l | awk '/^ii/&&$2 ~ /awk|bash$/{print $2,$3}'
bash 5.0-4
gawk 1:4.2.1+dfsg-1
mawk 1.3.3-17+b3

$ echo $BASH_VERSION
5.0.3(1)-release

$ bash --version
GNU bash, バージョン 5.0.3(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2019 Free Software Foundation, Inc.
ライセンス GPLv3+: GNU GPL バージョン 3 またはそれ以降 <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

$ awk --version
GNU Awk 4.2.1, API: 2.0 (GNU MPFR 4.0.2, GNU MP 6.1.2)
Copyright (C) 1989, 1991-2018 Free Software Foundation.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see http://www.gnu.org/licenses/.
■ブレース展開はその速さよりも、以下のような感覚的な書き方が出来る点が良い。
 ※「速さ」の点での向き、不向きは後述の余談で。

$ time printf "%s," {0..9} {a..z} {A..Z};echo
0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,
real	0m0.000s
user	0m0.000s
sys	0m0.000s

■現実にはブレース展開して作成した後、欠番が出ることもあるが、
 正規表現の連続リスト形式ではなかなか気づくことが出来ない。

$ touch {0..9} {a..z} {A..Z}
$ ls [0-9A-Za-z]
0  2  4  6  8  A  C  E  G  I  K  M  O  Q  S  U  W  Y  a  c  e  g  i  k  m  o  q  s  u  w  y
1  3  5  7  9  B  D  F  H  J  L  N  P  R  T  V  X  Z  b  d  f  h  j  l  n  p  r  t  v  x  z

$ rm Z b
$ ls {0..9} {A..Z} {a..z} > /dev/null
ls: 'Z' にアクセスできません: そのようなファイルやディレクトリはありません
ls: 'b' にアクセスできません: そのようなファイルやディレクトリはありません

$ ls [0-9A-Za-z]
0  2  4  6  8  A  C  E  G  I  K  M  O  Q  S  U  W  Y  c  e  g  i  k  m  o  q  s  u  w  y
1  3  5  7  9  B  D  F  H  J  L  N  P  R  T  V  X  a  d  f  h  j  l  n  p  r  t  v  x  z

■存在を予測した正しいブレース展開と正規表現の対は以下のようになる。

$ ls {0..9} {A..Y} a {c..z} >/dev/null
$ ls [0-9A-Yac-z]
0  2  4  6  8  A  C  E  G  I  K  M  O  Q  S  U  W  Y  c  e  g  i  k  m  o  q  s  u  w  y
1  3  5  7  9  B  D  F  H  J  L  N  P  R  T  V  X  a  d  f  h  j  l  n  p  r  t  v  x  z

■これを機械的にやることを考える。
 かなり力技な気もするけど、先頭行の挿入と最終行の置換でコマンドを作成。

$ echo {0..8} | wc -c
18

$ echo {A..Z} | wc -c
52

$ (echo {0..9};echo {1..8};echo {2..7};echo {3..6};echo {4..5}) | \
  awk 'BEGIN{print $0} \
       {for(n=1;n<17;n+=2) \
         {print substr($0,n);print substr($0,1,(length($0)-(n+1)))} \
     }END{print substr($0,17)}' | awk '(NF>1){print "  -e \047s/"$0"/["$1"-"$NF"]/g\047 \134" | "sort -uV"}' > seq-list.sh

$ (echo {A..Z};echo {B..Y};echo {C..X};echo {D..W};echo {E..V};echo {F..U};echo {G..T};echo {H..S};echo {I..R};echo {J..Q}; \
   echo {K..P};echo {L..O;echo {M..N}) | \
  awk 'BEGIN{print $0} \
       {for(n=1;n<49;n+=2) \
           {print substr($0,n);print substr($0,1,(length($0)-(n+1)))} \
       }END{print substr($0,49)}' | awk '(NF>1){print "  -e \047s/"$0"/["$1"-"$NF"]/g\047 \134" | "sort -uV"}' >> seq-list.sh

$ (echo {a..z};echo {b..y};echo {c..x};echo {d..w};echo {e..v};echo {f..u};echo {g..t};echo {h..s};echo {i..r};echo {j..q}; \
   echo {k..p};echo {l..o;echo {m..n}) | \
  awk 'BEGIN{print $0} \
       {for(n=1;n<49;n+=2) \
           {print substr($0,n);print substr($0,1,(length($0)-(n+1)))} \
       }END{print substr($0,49)}' | awk '(NF>1){print "  -e \047s/"$0"/["$1"-"$NF"]/g\047 \134" | "sort -uV"}' >> seq-list.sh

$ sed -i -e '1s%.*%#!/bin/bash\nsed \\\n&%' seq-list.sh 
$ sed -i -e '$ s/ \\//g' seq-list.sh
$ chmod +x seq-list.sh 

■試してみる。

$ ls [0-9A-Za-z] | tr '\n' ' ' | ./seq-list.sh;echo
[0-9] [A-Y] a [c-z] 

$ rm 6
$ ls [0-9A-Za-z] | tr '\n' ' ' | ./seq-list.sh;echo
[0-5] [7-9] [A-Y] a [c-z] 

$ rm A S
$  ls [0-9A-Za-z] | tr '\n' ' ' | ./seq-list.sh;echo
[0-5] [7-9] [B-R] [T-Y] a [c-z] 

$ rm Y
$ ls [0-9A-Za-z] | tr '\n' ' ' | ./seq-list.sh;echo
[0-5] [7-9] [B-R] [T-X] a [c-z] 

$ rm M
$ ls [0-9A-Za-z] | tr '\n' ' ' | ./seq-list.sh;echo
[0-5] [7-9] [B-L] [N-R] [T-X] a [c-z] 

$ rm H
$ ls [0-9A-Za-z] | tr '\n' ' ' | ./seq-list.sh;echo
[0-5] [7-9] [B-G] [I-L] [N-R] [T-X] a [c-z] 

■「test」で始まるファイルを作成、正規表現の連続リスト形式への変換で試してみる。
 最後の1文字を対象にするように抽出して変換、再度ファイル名に書き戻す。

$ touch test{0..9};touch test{A..Z};touch test{a..z}

$ ls test* | awk '{print substr($0,length($0))}' | tr '\n' ' ' | ./seq-list.sh | awk '{for(a=1;a<=NF;a++){print "test"$a}}'
test[0-9]
test[A-Z]
test[a-z]

$ rm testQ

$ ls test* | awk '{print substr($0,length($0))}' | tr '\n' ' ' | ./seq-list.sh | awk '{for(a=1;a<=NF;a++){print "test"$a}}'
test[0-9]
test[A-P]
test[R-Z]
test[a-z]

■最後の1文字以外を変数にすると、変数はfor文に与えるなどしてループ処理が出来る。

$ rm B
$ filename=test;ls ${filename}* | \
  awk '{print substr($0,length($0))}' | tr '\n' ' ' | ./seq-list.sh | awk -v file="${filename}" '{for(a=1;a<=NF;a++){print file$a}}'
test[0-9]
test[A-P]
test[R-Z]
test[a-z]

■以下は余談。
 seqはそのままでは複数の指定には向かない。
 短く書けるブレース展開は便利ではあるけれど、
 例えば/8のIPアドレスのリストを出すとしたら、ブレース展開よりもawkの方が速い。

$ echo $(( (2**8) **3))
16777216

$ time  printf "%s\n" 10.{0..255}.{0..255}.{0..255} | wc -l
16777216

real	2m22.792s
user	1m56.130s
sys	1m40.191s

$ echo 1 > zero;time awk '{for(a=0;a<256;a++){for(b=0;b<256;b++){for(c=0;c<256;c++){print "10."a"."b"."c}}}}' zero | wc -l
16777216

real	0m8.995s
user	0m8.989s
sys	0m0.822s

■mawkは制限が多くて使っていないので、余談として。

$ mawk -W version
mawk 1.3.3 Nov 1996, Copyright (C) Michael D. Brennan

compiled limits:
max NF             32767
sprintf buffer      2040

$ time mawk '{for(a=0;a<256;a++){for(b=0;b<256;b++){for(c=0;c<256;c++){print "10."a"."b"."c}}}}' zero | wc -l
16777216

real	0m11.907s
user	0m11.794s
sys	0m0.787s

■シンプルに16進数の一覧を出すなら、seqやawkよりもブレース展開の方が速い。

$ time printf "0x%x\n" {0..255} > /dev/null

real	0m0.001s
user	0m0.001s
sys	0m0.000s

$ time printf "0x%x\n" $(seq 0 255) > /dev/null

real	0m0.010s
user	0m0.001s
sys	0m0.009s

$ time awk '{for(a=0;a<256;a++){printf "0x%x\n",a}}' zero > /dev/null

real	0m0.010s
user	0m0.001s
sys	0m0.009s