python標準ライブラリにあった対話型UIフレームワーク紹介する

こんにちは、超久しぶりに記事を書く六角レンチです。
今日は標準ライブラリを見ていたら使えそうなライブラリを見つけたので、紹介したいと思います。
# 紹介するやつ
https://docs.python.org/ja/3/library/cmd.html

簡単に言うと、対話型UIのフレームワークです。
iwctlみたいな感じのものが作れそう

今回はこれを使って、**マクドナルドの略称か判別する対話型UIアプリ**を作ろうと思います(謎)
# 略称か判別する関数
マクドナルドの略称か判別する関数を作ります。
といっても昔作ったので、コピペしてきました
```py
def is_macdonald_ryakusyou(ryakusyou: str) -> bool:
    MACDONALD = "マクドナルド"
    if type(ryakusyou) is not str:
        print("文字列ちゃうやんけ")
    elif len(ryakusyou) == 0:
        print("空文字やんけ")
    else:
        index = 0
        for i in MACDONALD:
            if ryakusyou[index] == i:
                index += 1
                if index == len(ryakusyou):
                    break
        else:
            return False
        return True
```
試しに`マクド`を入れてみると...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594202/c9f5479a-c039-e961-2216-a751867c1436.png)
ちゃんと動きます
# 対話型UI化
判別する関数が動くことを確認できたので、いよいよ対話型UIを作っていきます
```py
from cmd import Cmd


class MacdonaldCheckShell(Cmd):
    def do_macdonaldcheck(self, ryakusyou: str) -> None:
        """マクドナルドの略称か判別するコマンド"""
        MACDONALD = "マクドナルド"
        if type(ryakusyou) is not str:
            self.error("文字列ちゃうやんけ")

        elif len(ryakusyou) == 0:
            self.error("空文字やんけ")

        else:
            index = 0
            for i in MACDONALD:
                if ryakusyou[index] == i:
                    index += 1
                    if index == len(ryakusyou):
                        break
            else:
                print(f"略称: [{ryakusyou}]は不正。")
                return
            print(f"略称: [{ryakusyou}]は正しい。")

    def do_exit(self, _: str) -> bool:
        """終了するコマンド"""
        print("exit...")
        return True

    @staticmethod
    def error(*text: str) -> None:
        print("Error: " + "\n".join(text))


if __name__ == "__main__":
    MacdonaldCheckShell().cmdloop()

```
staticmethodのerrorはエラー表示をするために作った

コマンドの作り方は関数名を`do_{コマンド名}`って感じにすることでできる
また、コマンドの関数の返り値として`True`を返すことで対話型UIを終了させることが可能

docstring(関数の下に書いてる`"""`で囲んでる文字列)がヘルプメッセージになる

関数の引数はコマンドの引数が入る...と思ったら**入力された文字のコマンド以降の部分が入る**。
例えば、`test honi hoya`が入力されると`do_test`関数の第一引数に`honi hoya`の文字列が入ってくる
前後の空白文字は消されるので`入力された文字.lstrip(コマンド名).strip()`された物が引数に来ると考えるといいかも(ちょっと使いにくいような...)


## 実行してみた
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594202/42c99c07-d544-c394-7152-6ec3e0f5b1c2.png)

`help コマンド名`でヘルプメッセージを表示できる

Tabキーによるコマンド推測、上下矢印キーのコマンド履歴も使用可能だった(すごい)
# 改良
対話型UIにする事ができたものの、一つバグがある。
それは、「マック」が略称として判別されないということである。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594202/e43ec015-6962-e772-d7e6-e7335919f1f3.png)
「マック」の略称は、~~私個人の意見としては正しくないと考えているが（思想強め）~~　公式でも「[朝マック](https://www.mcdonalds.co.jp/menu/morning/)」や「[マックシェイク ](https://www.mcdonalds.co.jp/products/8521/)」という名称があるので、略称として正しくなければならない。
(というかマックの名前がついた商品はあるのに、マクドの名前がついた商品ないんだな...)

そこで、マクドナルドの略称を追加、削除するコマンドを追加して対応する。
改良したプログラムがこちら
```py
from cmd import Cmd
from typing import Literal


class MacdonaldCheckShell(Cmd):
    def __init__(self) -> None:
        super().__init__()
        self.macdonald_strings: set[str] = set()

    def do_macdonaldcheck(self, ryakusyou: str) -> None:
        """マクドナルドの略称か判別するコマンド"""
        if ryakusyou in self.macdonald_strings:
            print(f"略称: [{ryakusyou}]は正しい。")
            return

        MACDONALD = "マクドナルド"
        if type(ryakusyou) is not str:
            self.error("文字列ちゃうやんけ")

        elif len(ryakusyou) == 0:
            self.error("空文字やんけ")

        else:
            index = 0
            for i in MACDONALD:
                if ryakusyou[index] == i:
                    index += 1
                    if index == len(ryakusyou):
                        break
            else:
                print(f"略称: [{ryakusyou}]は不正。")
                return
            print(f"略称: [{ryakusyou}]は正しい。")

    def do_macdonaldadd(self, ryakusyou: str) -> None:
        """マクドナルドの略称を追加するコマンド"""
        if ryakusyou in self.macdonald_strings:
            self.error("もう略称になっています")
        else:
            self.macdonald_strings.add(ryakusyou)
            print("追加完了")

    def do_macdonalddelete(self, ryakusyou: str) -> None:
        """マクドナルドの略称を削除するコマンド"""
        if ryakusyou not in self.macdonald_strings:
            self.error("略称に登録されていません")
        else:
            self.macdonald_strings.discard(ryakusyou)
            print("削除完了")

    def do_macdonaldstrings(self, _: str) -> None:
        """マクドナルドの略称に追加された文字を表示するコマンド"""
        if len(self.macdonald_strings) == 0:
            print("マクドナルドの略称は登録されていません")
        else:
            print("マクドナルドの略称")
            for i in self.macdonald_strings:
                print(f"|{i}")

    def do_exit(self, _: str) -> Literal[True]:
        """終了するコマンド"""
        print("exit...")
        return True

    @staticmethod
    def error(*text: str) -> None:
        print("Error: " + "\n".join(text))


if __name__ == "__main__":
    MacdonaldCheckShell().cmdloop()

```
新たに`macdonaldadd`、`macdonalddel`、`macdonaldstrings`コマンドを追加
それぞれ`追加`、`削除`、`確認`ができるコマンドになっている

## 実際に`マック`を追加してみると...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594202/8e635de9-0f32-0e73-3640-f564ef527961.png)
マックを略称と認識することに成功した
# さらにいろいろ...
公式ドキュメントを見た感じ、変数を変えたりすることで挙動や見た目を変えることが可能らしい。
(難しく言うと、変数をオーバーライドする。)
ということでいろいろ試してみた
## intro
対話型UIに入ったときいれた文字を出力させることができる
`マクドナルド判別アプリへようこそ！`の文字列を入れてみた
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594202/1b430b43-2c61-c33f-c540-f77d41bf7d9f.png)

## prompt
入力する場所の左の`(Cmd)`の部分の文字列を変更することができる
CUIで対話型なので`>>>`のほうが見やすいかも
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594202/bc504d54-1ccc-33b7-c3f7-0f6b18d97b56.png)

他にもヘルプメッセージ関係でいじれそうなところあるけど、このプログラムにはこの2つで十分かも
# 完成
```py
from cmd import Cmd
from typing import Literal


class MacdonaldCheckShell(Cmd):
    intro = "マクドナルド判別アプリへようこそ！"
    prompt = ">>> "

    def __init__(self) -> None:
        super().__init__()
        self.macdonald_strings: set[str] = set()

    def do_macdonaldcheck(self, ryakusyou: str) -> None:
        """マクドナルドの略称か判別するコマンド"""
        if ryakusyou in self.macdonald_strings:
            print(f"略称: [{ryakusyou}]は正しい。")
            return

        MACDONALD = "マクドナルド"
        if type(ryakusyou) is not str:
            self.error("文字列ちゃうやんけ")

        elif len(ryakusyou) == 0:
            self.error("空文字やんけ")

        else:
            index = 0
            for i in MACDONALD:
                if ryakusyou[index] == i:
                    index += 1
                    if index == len(ryakusyou):
                        break
            else:
                print(f"略称: [{ryakusyou}]は不正。")
                return
            print(f"略称: [{ryakusyou}]は正しい。")

    def do_macdonaldadd(self, ryakusyou: str) -> None:
        """マクドナルドの略称を追加するコマンド"""
        if ryakusyou in self.macdonald_strings:
            self.error("もう略称になっています")
        else:
            self.macdonald_strings.add(ryakusyou)
            print("追加完了")

    def do_macdonalddelete(self, ryakusyou: str) -> None:
        """マクドナルドの略称を削除するコマンド"""
        if ryakusyou not in self.macdonald_strings:
            self.error("略称に登録されていません")
        else:
            self.macdonald_strings.discard(ryakusyou)
            print("削除完了")

    def do_macdonaldstrings(self, _: str) -> None:
        """マクドナルドの略称に追加された文字を表示するコマンド"""
        if len(self.macdonald_strings) == 0:
            print("マクドナルドの略称は登録されていません")
        else:
            print("マクドナルドの略称")
            for i in self.macdonald_strings:
                print(f"|{i}")

    def do_exit(self, _: str) -> Literal[True]:
        """終了するコマンド"""
        print("exit...")
        return True

    @staticmethod
    def error(*text: str) -> None:
        print("Error: " + "\n".join(text))


if __name__ == "__main__":
    MacdonaldCheckShell().cmdloop()

```
傍から見てわかりやすくかけるようになっていると思う
## 実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594202/f96eb4f9-8709-af98-8857-312cfc704d3d.png)
ちゃんと動いている
# おわり
標準ライブラリにこんな便利なものがあるとは...
他にも便利そうなライブラリがあったので使いたいです

簡単にクラスを使ってアプリを作ることができるので、プログラミング初心者にもおすすめできるかも
クラスの使い方を知れるし、対話型UIの部分を作らなくても良いのがおすすめポイント

あと個人的にオーバーライドは何が良いんだ？と考えていたので驚いた
こういう使い方もあるんだなぁって感じで勉強になった