Pythonで対話式CLIツールを作る

PythonでこんなCLIツールが実装できる
![questionary.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/039f6dc1-7099-86a4-9c01-5ccf14af764b.gif)

上記のスクリプトはこちら
(`questionary==1.10.0`を使用)

```python
import questionary

name = questionary.text('Name?').ask()
age = int(questionary.text('Age?').ask())
sex = questionary.select(
    'Sex?',
    choices=["Male", "Female"]
).ask()
if sex == "Male":
    print(f"Hello {name}! {age} years old gentleman!")
else:
    print(f"Hello {name}! {age} years old lady!")
```

# 使用例

## テキスト入力

```python3
import questionary
name = questionary.text('Name?').ask()
```

### デフォルト値を設定する

```python
import questionary
name = questionary.text('Name?', default='My Default Name').ask()
print(name)
```

![text-default.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/74137a36-389d-a592-aaab-4ff305393f0d.gif)

`click`のように`type`引数は使えない。
入力値を数値にしたい場合は`int()`や`float()`でキャストする。
入力値のバリデーションを別途行う必要がある（後述）。

## 対話式に選択(単一)

```python
import questionary
sex = questionary.select(
    'Sex?',
    choices=["Male", "Female"],
).ask()
print(sex)
```

![select.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/26cadc0c-542c-b406-0001-0d6253b6c93a.gif)

### 選択肢に表示する名前と実際の変数値を使い分ける

```python
import questionary
from questionary import Choice

sex = questionary.select(
    'Sex?',
    choices=[
        Choice(title="Male", value=0),
        Choice(title="Female", value=1),
    ],
).ask()
print(sex)
```
![select-use-choice.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/06535c0b-c719-443b-70de-79d0c681be58.gif)


### デフォルト値を設定する

指定した値が選択された状態の対話モードになる

```python
import questionary
sex = questionary.select(
    'Sex?',
    choices=["Male", "Female"],
    default="Female",
).ask()
print(sex)
```

![select-default.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/a54107ae-edf5-c438-f36f-27fccf591bcc.gif)

### 選択肢に連番を振る

`use_shortcuts`引数。

```python
import questionary
sex = questionary.select(
    'Sex?',
    choices=["Male", "Female"],
    use_shortcuts=True
).ask()
print(sex)
```
例の場合、`1`キーまたは`2`キーを入力することで選択肢を移動することができる。
![select-shortcut.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/65557af0-20fc-966d-3988-d4d0c600dde9.gif)

### 区切り線を設ける

```python
import questionary
from questionary import Separator

sex = questionary.select(
    'Sex?',
    choices=[
        "Male",
        Separator("---"),
        "Female",
    ],
).ask()
print(sex)
```
![select-delimiter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/520d2cbc-bb03-52ae-dfce-facd0eb32f1e.gif)

### 無効な選択肢

```python
import questionary
from questionary import Choice

sex = questionary.select(
    'Sex?',
    choices=[
        "Male",
        "Female",
        Choice(title="Disabled", disabled="Cannot use!")
    ],
).ask()
print(sex)
```

※動画は全て「↓キー」を押してます
![select-disabled.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/d5a8457d-29a9-bda3-21e6-0753f18cebc1.gif)


## 対話式に選択(複数)

```python
import questionary
choiced_items = questionary.checkbox(
    "Select items.",
    choices=[
        'item1',
        'item2',
        'item3',
    ]
).ask()
print(choiced_items)
```

スペースキーで選択/解除
![checkbox.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/486f77ed-5b32-1686-88ed-f6be5bf25a8f.gif)

### 選択肢に表示する名前と実際の変数値を使い分ける

`questionary.select`の場合と同様に`questionary.Choice`を使用

```python
import questionary
from questionary import Choice

choiced_items = questionary.checkbox(
    "Select items.",
    choices=[
        Choice('item1', value=1),
        Choice('item2', value=2),
        Choice('item3', value=3),
    ]
).ask()
print(choiced_items)
```
![checkbox-choice.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/618e371b-4f43-4e20-349c-22983f1651d1.gif)

### デフォルトで選択済みにする

`item2`を選択済みにする

```python
import questionary
from questionary import Choice

choiced_items = questionary.checkbox(
    "Select items.",
    choices=[
        "item1",
        Choice("item2", checked=True),
        "item3",
    ]
).ask()
print(choiced_items)
```
![checkbox-checked.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/0979550e-5239-4d1f-69ed-e89eddf89a33.gif)

## ファイルパス

```python
import questionary

path = questionary.path(
    "What's the path to the projects version file?"
).ask()
print(path)
```

スラッシュキーで候補が表示される（すごい）。
タブキーで自動補完入力される（すごい）。
![path.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/f75141e8-b67e-d975-cb5a-a51e33360c1a.gif)

## Yes|No (真偽値)

```python
import questionary

is_engineer = questionary.confirm('Are you engineer?').ask()
print(is_engineer)
```
![confirm.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/68ed3e9c-d68b-5c23-0fcc-c723a4f1510a.gif)

「No」を選ぶ場合「n」キーを押すだけで次へ進む。
![confirm-no.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/ea7f3b0a-d017-bb5f-e6d2-552c34a93504.gif)



### デフォルトをFalseにする

```python
import questionary

is_engineer = questionary.confirm(
    'Are you engineer?',
    default=False
).ask()
print(is_engineer)
```
![confirm-default-false.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/11b540fe-4345-c0b5-beef-d170bf614604.gif)

## その他

### オートコンプリートなSelect

ローマ字表記の都道府県を入力するケースを考える。

<details><summary>都道府県一覧</summary><div>

```python:prefectures.py
PREFECTURES = [
    "hokkaido",
    "aomori",
    "iwate",
    "miyagi",
    "akita",
    "yamagata",
    "fukushima",
    "ibaraki",
    "tochigi",
    "gunma",
    "saitama",
    "chiba",
    "tokyo",
    "kanagawa",
    "niigata",
    "toyama",
    "ishikawa",
    "fukui",
    "yamanashi",
    "nagano",
    "gifu",
    "shizuoka",
    "aichi",
    "mie",
    "shiga",
    "kyoto",
    "osaka",
    "hyogo",
    "nara",
    "wakayama",
    "tottori",
    "shimane",
    "okayama",
    "hiroshima",
    "yamaguchi",
    "tokushima",
    "kagawa",
    "ehime",
    "kochi",
    "fukuoka",
    "saga",
    "nagasaki",
    "kumamoto",
    "oita",
    "miyazaki",
    "kagoshima",
    "okinawa",
]
```

</div></details>

```python
import questionary
from prefectures import PREFECTURES

prefecture = questionary.autocomplete(
    "お住まいの都道府県は?",
    choices=PREFECTURES
).ask()
print(prefecture)
```

(エディタのカラーテーマとの相性で見づらい・・・)
![autocomplete.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/ef1132c9-7cce-9136-030e-591df81d2c1b.gif)

### 入力値バリデーション

入力値が整数であることを検証する例。

```python
import questionary
from questionary import Validator, ValidationError

class IntegerValidator(Validator):
    def validate(self, document):
        try:
            int(document.text)
        except:
            raise ValidationError(
                message=f"{document.text} is not valid integer.",
                cursor_position=len(document.text)
            )

age = questionary.text("Age?", validate=IntegerValidator).ask()
print(age)
```
![integer-validator.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/30b90bc5-325d-6f3f-ec13-3a09216e50ee.gif)

### プロンプトを動的に分岐

```python
import questionary

sex = questionary.select(
    "Sex?",
    choices=["Male", "Female"]
).ask()
is_male = sex == "Male"

questionary.text(
    "女性の方にお聞きします。愛用する化粧品を教えてください:"
).skip_if(is_male).ask()
```
![skip-if.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154711/b52495ff-c9f1-7be6-fa8d-b5e134a0b94c.gif)