パターン指定型パスワード ジェネレーター

目がチカチカしないパスワードが欲しいので、パターンを指定する形式のパスワード生成プログラムを python で作る。

**パターン指定型パスワード ジェネレーターの目的は、好みのパスワードを得ることなので、生成されたパスワードを、そのまま使う必要はありません。**

<details><summary>Python プログラム</summary><div>

```python:ppwgen.py
#!/usr/bin/env python

flag_debug = False
flag_verbose = False
flag_syntax = False


def debug(msg):
    if flag_debug:
        print(msg)


def verbose(msg):
    if flag_verbose:
        print(msg)


def import_random():
    try:
        from Crypto.Random import random
        verbose('import Crypto.Random')
        return random
    except ImportError:
        pass

    try:
        import secrets
        verbose('import secrets')
        return secrets
    except ImportError:
        pass

    import random
    verbose('import random')
    return random


def charset_(r, x=''):
    s = ''
    for c in r:
        c = chr(c)
        if c not in x:
            s = s + c
    return s


def charset(s, x=''):
    r = ''
    while len(s) >= 3:
        if s[1] != '-':
            break
        cs = ord(s[0])
        ce = ord(s[2])
        r = r + charset_(range(cs, ce+1), x)
        s = s[3:]

    for c in s:
        if c not in x:
            r = r + c
    return r


def isescape(c):
    return (len(c) > 1) and (c[0] == '\\')


def unescape(c):
    if isescape(c):
        return c[1:]
    return c


class InvalidPattern(Exception):
    def __init__(self):
        Exception.__init__(self)


class PatIter:
    def __init__(self, pat):
        self.pos = 0

        s = []
        for c in list(pat):
            if len(s) and s[-1] == '\\':
                s[-1] = s[-1] + c
            else:
                s.append(c)
        if len(s) and s[-1] == '\\':
            if flag_syntax:
                raise InvalidPattern()
            s = s[:-1]
        self.pat = s

    def __iter__(self):
        return self

    def __next__(self):
        return self.next()

    def next(self):
        p = self.pos
        if p >= len(self.pat):
            raise StopIteration()
        self.pos = p + 1
        return self.pat[p]

    def rewind(self):
        self.pos = self.pos - 1
        return self


class Generator:
    def __init__(self, data):
        self.data = data

    def generate(self, random):
        s = self.data
        if type(s) == str:
            return random.choice(s)

        if type(s) == tuple:
            n = s[0]
            s = [s[1]]
        elif type(s) == list:
            n = 1
        else:
            raise Exception("Bug!")

        r = ''
        for i in range(n):
            for g in s:
                r = r + g.generate(random)
        return r


PATTERN_MAP = {
    'b': '01',
    'o': '01234567',
    'd': charset('0-9'),
    'X': charset('0-9A-F'),
    'x': charset('0-9a-f'),

    'A': charset('A-Za-z'),
    'C': charset('A-Z'),
    'c': charset('a-z'),

    'B': 'AEIOUaeiou',
    'V': 'AEIOU',
    'v': 'aeiou',

    'D': charset('A-Za-z', 'AEIUOaeiou'),
    'Q': charset('A-Z', 'AEIOU'),
    'q': charset('a-z', 'aeiou'),

    'Y': charset('0-9A-Za-z'),
    'Z': charset('0-9A-Z'),
    'z': charset('0-9a-z'),

    'W': charset('0-9A-Za-z_'),
    'L': charset('0-9A-Z'),
    'l': charset('0-9a-z'),

    '%': '%',
}


def check_not_close(c, q):
    return ((c is None) or (c != q)) and flag_syntax


class CustomPasswordGenerator:
    def __init__(self, pattern):
        ptr = PatIter(pattern)
        r = []
        for c in ptr:
            if c != '%':
                g = Generator(unescape(c))
            else:
                g = self.parse_pattern(ptr, False)
            if g != None:
                r.append(g)
        self.generator = Generator(r)

    def parse_pattern(self, ptr, nest):
        n = None
        c = None
        for c in ptr:
            if not c.isdigit():
                break
            if n is None:
                n = 0
            n = (n * 10) + int(c, 10)

        if n is None:
            n = 1
        if c is None:
            if flag_syntax:
                raise InvalidPattern()
            return None

        if c in PATTERN_MAP:
            return Generator((n, Generator(PATTERN_MAP[c])))
        if c == '{':
            return self.parse_subpat(ptr, n)
        if c == '[':
            return self.parse_charset(ptr, n)

        if isescape(c):
            c = unescape(c)
        elif (not nest) or (c in ']}'):
            raise InvalidPattern()
        return Generator((n, Generator(c)))

    def parse_subpat(self, ptr, rep):
        r = []
        c = None
        for c in ptr:
            if c == '}':
                break
            g = self.parse_pattern(ptr.rewind(), True)
            if g != None:
                r.append(g)
        if check_not_close(c, '}'):
            raise InvalidPattern()
        return Generator((rep, Generator(r)))

    def parse_charset(self, ptr, rep):
        r = []
        s = []
        c = None
        for c in ptr:
            if c == ']':
                break
            if len(s) < 2:
                s.append(c)
                continue
            a = unescape(s[0])
            if s[-1] != '-':
                r.append(a)
                s = s[1:]
                s.append(c)
                continue
            b = unescape(c)
            r.append(charset_(range(ord(a), ord(b)+1)))
            s = []
        if check_not_close(c, ']'):
            raise InvalidPattern()
        for a in s:
            r.append(unescape(a))
        return Generator((rep, Generator(''.join(r))))

    def generate(self, rnd=None, length=0):
        if rnd is None:
            rnd = import_random()
        pw = self.generator.generate(rnd)
        if length == 0:
            return pw
        while length > len(pw):
            pw = pw + self.generator.generate(rnd)
        return pw[:length]


def main():
    import argparse
    global flag_debug
    global flag_verbose
    global flag_syntax

    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--debug', action='store_true', default=False)
    parser.add_argument('-v', '--verbose', action='store_true', default=False)
    parser.add_argument('-l', '--length', type=int, default=0)
    parser.add_argument('-U', '--upper', action='store_true', default=False)
    parser.add_argument('-L', '--lower', action='store_true', default=False)
    parser.add_argument('-n', '--count', type=int, default=0)
    parser.add_argument('-p', '--syntax', action='store_true', default=False)
    parser.add_argument('pattern', type=str, default='%l')

    args = parser.parse_args()
    flag_debug = args.debug
    flag_verbose = args.verbose or flag_debug
    flag_syntax = args.syntax

    converter = str
    if args.upper:
        converter = str.upper
    if args.lower:
        converter = str.lower

    pattern = args.pattern
    gen_length = args.length
    gen_count = args.count
    if gen_count == 0:
        gen_count = {False: 20, True: 2}[flag_debug]

    verbose('pattern = \'%s\'' % pattern)
    verbose('length = %d' % gen_length)
    verbose('count = %d' % gen_count)

    if flag_debug:
        verbose('PATTERN_MAP')
        for k in sorted(PATTERN_MAP):
            verbose('  \'%s\' : \'%s\'' % (k, PATTERN_MAP[k]))
        verbose('')

    try:
        gen = CustomPasswordGenerator(pattern)
    except InvalidPattern:
        print('invalid pattern: \'%s\'' % pattern)
        return

    rnd = import_random()
    fmt = '%' + str(len(str(gen_count))) + 'd: %s'
    for i in range(gen_count):
        print(fmt % (i+1, converter(gen.generate(rnd, gen_length))))


if __name__ == '__main__':
    main()
```

</div></details>

# コマンド ライン引数

生成するパターンを C言語の書式風で記述します。

## 指定子

文字 % の直後が、乱数で選択される文字になります。

| 指定子 | 種類 | 備考 |
|:--|:--|:--|
| b | 2進数 | [01] |
| o | 8進数 | [0-7] |
| d | 数字 | [0-9] |
| X | 16進大文字 | [0-9A-F] |
| X | 16進小文字 | [0-9a-f] |
| A | 英文字 | [A-Za-z] |
| C | 英大文字 | [A-Z] |
| c | 英小文字 | [A-Z] |
| B | 英母音 | [AEIOUaeiou] |
| V | 英母音(大) | [AEIOU] |
| v | 英母音(小) | [aeiou] |
| D | 英子音 | [AEIOUaeiuo] を除く [A-Za-z] |
| Q | 英子音(大) | [AEIOU] を除く [A-Z] |
| q | 英子音(小) | [AEIOU] を除く [A-Z] |
| Y | 英数 | [0-9A-Za-z] |
| Z | 英数(大) | [0-9A-Z] |
| z | 英数(小) | [0-9a-z] |
| W | ラベル文字 | [0-9A-Za-z_] |
| L | ラベル文字(大) | [0-9A-Z_] |
| l | ラベル文字(小) | [0-9a-z_] |

指定子を並べた例

```console
$ python ppwgen.py -n 5 'qiita-%d%X%x%x%A%C%c%c%B%V%v%D%Q%q%Y%Z%z%W%L%l'
1: qiita-2D1cHSuhIEotMrdXh53r
2: qiita-2454ZXzlOIeXHc9GpD3n
3: qiita-2481ZFxhOUeZGjjHoP5y
4: qiita-7BffcNydeEaGZnUMyDUe
5: qiita-52f5nZqtAAiWTbGTphHj
```

## 文字数指定

文字 % 直後の数値は、乱数で選択される文字の文字数になります。
　　%[数値]指定子
の形式です。例えば '%3d' とすると、3桁の数字になります。

```console
$ python ppwgen.py -n 5 'qiita-%3d'
1: qiita-419
2: qiita-879
3: qiita-452
4: qiita-054
5: qiita-025
```

## 省略形

上記サンプル
　　'qiita-%d%X%x%x%A%C%c%c%B%V%v%D%Q%q%Y%Z%z%W%L%l'
は、'%{...}' 形式で省略
　　'qiita-%{dXxxACccBVvDQqYZzWLl}'
できます。

```console
$  python ppwgen.py  -n 5 'qiita-%{dXxxACccBVvDQqYZzWLl}'
1: qiita-32b3eMjmeUolYkdFjsNj
2: qiita-3F84MNzfAOoLNt1Y8GSi
3: qiita-9A3dURxtAAovPg7V8p9b
4: qiita-4D47iTquuEiDKjsQ3bM4
5: qiita-87dcXGjkuIuCMk1H5u9x
```

## 選択文字指定

指定子の代わりに '[...]' で選択する文字を指定します。

```console
# 8桁2進数となる例
$ python ppwgen.py  -n 5 '%8[01]'     
1: 01010110
2: 00100001
3: 10100111
4: 11011000
5: 00101110
```

'[' 開始文字 '-' 終了文字 ']' で選択する文字を指定します。

```console
# 文字 STUVWXYZ から選択
$ python ppwgen.py -n 5 '%8[S-Z]'
1: XXTTZWTS
2: YUTTXUTV
3: UVUWXTXX
4: UTSZUWZS
5: UTWWYUVX
```

## エスケープ
バックスラッシュ \\ の直後の文字は特別な意味を持ちません。

```console
# 文字 S,-,Z の３つから選択
$ python ppwgen.py -n 5 '%8[S\-Z]'
1: -ZZ---S-
2: ZZZ--ZZZ
3: -ZZSS-SS
4: ZSS-SS-Z
5: Z----SZ-
```

---
# 実行例

ライセンス キー風

```console
$ python ppwgen.py -n 10 'qiita%4{-c3d}'
 1: qiita-w119-i910-y458-q980
 2: qiita-j159-p127-u802-b125
 3: qiita-f923-o256-o568-h824
 4: qiita-k280-q899-a826-q831
 5: qiita-d264-a506-v904-q475
 6: qiita-u866-p655-n009-o483
 7: qiita-m101-a054-p896-x707
 8: qiita-o524-b339-j527-n802
 9: qiita-o030-k832-y107-s425
10: qiita-w842-a925-h295-p339
```

'- 子音 母音 子音 数字' の繰り返し。

```console
$ python ppwgen.py -n 10 'qiita%4{-Qvqd}'
 1: qiita-Sap0-Woy0-Yeg6-Vig0
 2: qiita-Viq4-Giq7-Yoq0-Cam4
 3: qiita-Luz7-Woc8-Wek8-Xek0
 4: qiita-Saf8-Mac9-Kar4-Mif1
 5: qiita-Xen3-Kur3-Gix9-Mom4
 6: qiita-Xew6-Var0-Luf5-Guq9
 7: qiita-Gew8-Ded2-Jon0-Qac5
 8: qiita-Tiq2-Yef7-Reg3-Qiz9
 9: qiita-Paw8-Lap9-Jex7-Xuc2
10: qiita-Rud5-Yos6-Vam8-Pip8
```

入れ子構造を持つ複雑なパターン。

```console
$ python ppwgen.py -n 10 'qiita%2{-C3{2c[13579]}}'
 1: qiita-Oqv5ct1eg9-Cfv3ha3yo1
 2: qiita-Vtw3yq5ka3-Nha9zh7vr1
 3: qiita-Ulk5yb3ne3-Dpz7wu5xu3
 4: qiita-Yqb1lg3uf5-Iqn5qy9xa1
 5: qiita-Qck5jr1ru1-Tgo9ab1rh3
 6: qiita-Vdc7tj3oj5-Iba3eu1yc3
 7: qiita-Pqn9wn1ws1-Ksb3ys5bt1
 8: qiita-Vpo7hq1wi7-Iyh3uc3lt1
 9: qiita-Fmq5mu3wh7-Tdd3gq7ea1
10: qiita-Sqb1iv1gb5-Boy9va7ve9
```