重複する行を順序を保ったまま整理したい

## 経緯

`.gitignore` が秘伝のタレ状態になると、対象が重複することがある。

```.gitignore
### C ###
# Prerequisites
*.d

# Object files
*.o
*.ko
*.obj
*.elf

# Linker output
*.ilk
*.map
*.exp


### C++ ###
# Prerequisites
*.d  # <- 重複

# Compiled Object files
*.slo
*.lo
*.o  # <- 重複
*.obj  # <- 重複

# Precompiled Headers
*.gch
*.pch

# Linker files
*.ilk  # <- 重複
```

このせいで修正したつもりが「あれ、 `.gitignore` が反映されないな」みたいな時間の無駄が発生する。
ググると、「ソートしてから `(^.*$)(\n(^\1$)){1,}` で検索！」みたいな情報が出てくるんだが、
そうやって消すと順序が破壊されて「この `*.map` って何のために無視してるんだっけ？」が起きる。

面倒だけど、スクリプトにしておく。

## 実装

コピペしてさっと動かしたいので、Python3.6の標準ライブラリだけで。

```arrange-duplicate-lines.py
#!/usr/bin/env python3

from argparse import ArgumentParser
from logging import basicConfig, getLogger, DEBUG, INFO
from pathlib import Path
from re import compile, escape, match

basicConfig(level=INFO)
logger = getLogger(__name__)


def main():
    parser = ArgumentParser(description='Remove or comment out duplicate lines, keeping them in order.')
    parser.add_argument('path', type=Path, help='Input file path.')
    delete_or_comment = parser.add_mutually_exclusive_group()
    delete_or_comment.add_argument('--delete', '-d', action='store_true', help='Flag to remove duplicate lines.')
    delete_or_comment.add_argument('--comment', '-c', type=str, default='#',
                                   help='The character to comment out duplicate lines.')
    parser.add_argument('--output', '-o', type=Path, default=None, help='Path to save.')
    args = parser.parse_args()

    result = []
    with open(args.path, mode='r', encoding='utf_8') as f:
        pattern_commented = compile(r'^{0}.*$'.format(escape(args.comment)))
        pattern_blank = compile(r'^\s+$')
        for i, line in enumerate(f):
            if match(pattern_commented, line) or match(pattern_blank, line):
                logger.debug(f'L{i:04}: [ AS IS  ] {line}')
                result.append(line)
            else:
                if line not in result:
                    logger.debug(f'L{i:04}: [  HIT   ] {line}')
                    result.append(line)
                else:
                    if not args.delete:
                        logger.debug(f'L{i:04}: [COMMENT] {line}')
                        result.append(f'{args.comment} {line}')
                    else:
                        logger.debug(f'L{i:04}: [ DELETE ] {line}')

    logger.debug('\n'.join(result))

    if args.output:
        with open(args.output, mode='w') as f:
            f.writelines(result)
        logger.info(f'Save: {args.output}')
    logger.info('Completed!')


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

せっかくなので、空行と元々コメントされているところはそのまま残し、重複行を削除するかコメントアウトするか選べるようにした。

## 結果

```shell
$ python3 arrange-duplicate-lines.py .gitignore -d -o .gitignore.delete
```

```.gitignore.delte
### C ###
# Prerequisites
*.d

# Object files
*.o
*.ko
*.obj
*.elf

# Linker output
*.ilk
*.map
*.exp


### C++ ###
# Prerequisites

# Compiled Object files
*.slo
*.lo

# Precompiled Headers
*.gch
*.pch

# Linker files

```

```shell
$ python3 arrange-duplicate-lines.py .gitignore -c '###' -o .gitignore.comment
```

```.gitignore.comment
### C ###
# Prerequisites
*.d

# Object files
*.o
*.ko
*.obj
*.elf

# Linker output
*.ilk
*.map
*.exp


### C++ ###
### # Prerequisites
### *.d

# Compiled Object files
*.slo
*.lo
### *.o
### *.obj

# Precompiled Headers
*.gch
*.pch

# Linker files
### *.ilk
```

愚直、ゆえに、もし `*.txt` と `log-*.txt` みたいな意味的な重複は残してしまうが、まぁ良しとしよう。
[gitignore.io](https://www.toptal.com/developers/gitignore) の名誉のために付記しておくと、
`https://www.toptal.com/developers/gitignore/api/c,c++` のように一緒に指定した場合は上記のように重複が削除された状態で生成されるのを確認しました。