「この仕事、3日後でも良いか」と思って先延ばししたら、やり忘れてしまったという経験はないだろうか。そんなときに、ターミナルから使える簡単なリマインダーCLIを作ってみよう。Pythonのコマンドラインツールの作り方や、ファイル操作の方法を学ぶ良い機会にもなるだろう。

  • ターミナルで使えるリマインダーを作ってみよう

    ターミナルで使えるリマインダーを作ってみよう

Pythonライブラリ「Typer」で手軽にCLIツールを作ろう

今回は、手軽なコマンドラインツール(CLI)を作るために、Pythonライブラリの「Typer」を使ってみよう。TyperはPythonでコマンドラインツールを簡単に作るための専用ライブラリだ。特徴は、Pythonの型ヒントを使って、コマンドの引数やオプションを自然に定義できる点にある。

Typerをインストールするには、ターミナル(WindowsならPowerShell、macOSならターミナル.app)で下記のようなコマンドを実行しよう。また、CLIでの出力を見やすくするためのライブラリ「Rich」も一緒にインストールしよう。

# Pythonパッケージをインストールする
pip install typer==0.25.1
pip install rich==15.0.0

N日後を計算するには?

N日後の日時を計算するには、Pythonの標準ライブラリである「datetime」を使うと便利だ。datetimeを使うと、現在の日付や時間を取得したり、日付を加算したりすることが簡単にできる。例えば、現在の日付からN日後の日付を計算するには、以下のようなコードが使える。

from datetime import datetime, timedelta

def calc_n_days(days: int) -> datetime:
    """N日後の日付を計算する関数"""
    current_date = datetime.now()
    future_date = current_date + timedelta(days=days)
    return future_date

d = calc_n_days(3)
print(f"現在の日付: {datetime.now()}")
print(f"3日後の日付: {d}")

ここで定義した関数calcndays(days:int)は、引数としてN日を受け取り、現在の日付にその日数を加算して未来の日付を返す。これをリマインダーCLIの中で使うことで、ユーザーが指定したN日後の日時を簡単に計算できるようになる。

PythonのREPL(即時実行モード)で、このコードを実行してみよう。すると、現在の日付と3日後の日付が表示されるはずだ。これをリマインダーCLIの中で活用して、ユーザーが指定したN日後に通知する機能を実装してみよう。

  • [3日後の日付を計算する関数を定義して試したところ

    3日後の日付を計算する関数を定義して試したところ

ところで、3日後にタスクを実行したいという場合、多くは16時に登録したとしても、その日の朝にタスクを通知して欲しいはずだ。そこで、N日後の日時を計算する際には、時間も考慮して、例えば16時に登録した場合は、3日後の16時ではなく、その日の朝8時に通知するようなロジックを組み込むようにしよう。そのため、上記の関数を、次のように修正する。

def calc_n_days(days: int) -> datetime:
    """N日後の朝8時の日付時刻を計算する関数"""
    current_date = datetime.now()
    future_date = current_date + timedelta(days=days)
    future_date_8am = future_date.replace(
        hour=8,
        minute=0,
        second=0,
        microsecond=0
    )
    return future_date_8am

プログラムを完成させよう

それでは、プログラムを完成させよう。以下のプログラムは、タスクを追加したり、期限が来たタスクを表示したり、期限が過ぎたタスクを削除したりする機能を作ってみよう。少しプログラムが長いので、用途ごとにファイルを分けてみよう。

  • later.py: メインプログラム - タスクの追加や表示などを行う
  • storage.py: タスクの情報を保存したり、読み込んだりする関数を定義したもの

こちらに完成したプログラムをアップロードした。

データファイルに保存しよう

ただし、実際にリマインダーを作る場合、単にN日後の日時を計算するだけでは不十分だ。ユーザーが登録したリマインダーの内容や日時をどこかに保存しておく必要がある。そこで、今回はJSON形式でリマインダーの情報をファイルに保存する方法を紹介しよう。

Pythonの標準ライブラリである「json」を使うと、Pythonのデータ構造を簡単にJSON形式に変換してファイルに保存したり、逆にJSONファイルからデータを読み込んだりすることができる。ここでは、下記のように、JSONを読み書きする関数を定義した。以下のプログラムを「storage.py」という名前で保存して利用しよう。

import json
import os

# タスクを保存する JSON ファイルのパス --- (*1)
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_FILE = os.path.join(ROOT_DIR, "tasks.json")

def save_tasks(tasks):
    """タスクを JSON ファイルに保存する""" # --- (*2)
    with open(DATA_FILE, "w", encoding="utf-8") as f:
        json.dump(tasks, f, indent=4, ensure_ascii=False)

def load_tasks():
    """タスクを JSON ファイルから読み込む""" # --- (*3)
    if not os.path.exists(DATA_FILE):
        return []
    with open(DATA_FILE, "r", encoding="utf-8") as f:
        tasks = json.load(f)
        tasks.sort(key=lambda x: x["date"])
        return tasks

プログラムの(*1)では、タスクを保存するJSONファイルのパスを定義している。(*2)では、タスクのリストをJSONファイルに保存する関数を定義している。(*3)では、JSONファイルからタスクのリストを読み込む関数を定義している。

ただし、この処理はJSONファイルの単純な読み書きとなっており、同時にタスクを読み書きすると、ファイルが壊れる可能性もある。複数人による読み書きが前提の場合は、排他処理がしっかりしているSQLiteやその他のデータベースを利用すると良いだろう。今回は、プログラムの分かりやすさを優先するために、簡単なファイル処理にした。

メインプログラムを確認しよう

続いて、メインプログラム「later.py」を確認しよう。以下のコードは、タスクの追加、表示、期限切れの削除、期限到来の確認などの機能を実装している。Typerを使ってコマンドライン引数を処理し、Richを使ってタスクのリストを表形式で表示している。

#!/usr/bin/env python
""" CLIでタスクを管理するプログラム """

from datetime import datetime, timedelta
import re
import typer
from rich.console import Console
from rich.table import Table
from storage import load_tasks, save_tasks

# TyperやConsoleのインスタンスを作成 --- (*1)
app = typer.Typer()
console = Console()

def calc_due_date(due: str) -> datetime:
    """期限の表現を解析して、通知日時を計算する""" # --- (*2)
    now = datetime.now()
    normalized = due.strip().lower()
    match = re.fullmatch(r"(\d+)([dh])", normalized)
    if not match:
        raise typer.BadParameter("期限は '3d' / '2h' の形式で指定してください。")
    amount = int(match.group(1))
    unit = match.group(2)
    if unit == "d":
        return (now + timedelta(days=amount)).replace(hour=8, minute=0, second=0, microsecond=0)
    if unit == "h":
        return now + timedelta(hours=amount)
    return now

@app.command()
def add(due: str, task: str):
    """タスクを追加する (例: later.py add "3d" "レポート提出")""" # --- (*3)
    tasks = load_tasks()
    notify_at = calc_due_date(due)
    notify_at_s = notify_at.strftime("%Y-%m-%d %H:%M:%S")
    # 既存の date キーは維持しつつ、時刻付き情報を notify_at に保存
    tasks.append({"date": notify_at_s, "task": task})
    save_tasks(tasks)
    print(f"タスクを追加しました: {task} (通知日時: {notify_at_s})")

def show_tasks(tasks: list[dict], title: str):
    """タスクのリストを表形式で表示する""" # -- (*4)
    if len(tasks) == 0:
        return print("タスクはありません。")
    table = Table(title=title, show_lines=True)
    table.add_column("番号", justify="right")
    table.add_column("タスク", style="red")
    table.add_column("期限", style="green")
    for idx, task in enumerate(tasks, start=1):
        table.add_row(f"{idx}", task["task"], task["date"])
    console.print(table)

@app.command()
def show():
    """保存されたタスクを表示する""" # --- (*5)
    tasks = load_tasks()
    show_tasks(tasks, "■ 保存したタスク一覧")

@app.command()
def clear():
    """期限が過ぎたタスクを削除する""" # --- (*6)
    tasks = load_tasks()
    now = datetime.now()
    tasks_due = []
    for task in tasks:
        notify_at = datetime.strptime(task["date"], "%Y-%m-%d %H:%M:%S")
        remove_date = notify_at - timedelta(days=1)  # 期限が1日過ぎたもの
        if remove_date > now:
            tasks_due.append(task)
    save_tasks(tasks_due)
    print(f"期限が過ぎたタスクを削除しました。残りのタスク数: {len(tasks_due)}")
    show()  # 更新後のタスクを表示

@app.command()
def check():
    """期限が来たタスクを表示する""" # --- (*7)
    tasks = load_tasks()
    now = datetime.now()
    tasks_due = []
    for task in tasks:
        notify_at = datetime.strptime(task["date"], "%Y-%m-%d %H:%M:%S")
        if notify_at <= now:
            tasks_due.append(task)
    show_tasks(tasks_due, "■ 期限が来たタスク")

if __name__ == "__main__":
    app()

簡単にプログラムを確認しよう。

(*1)では、TyperのアプリケーションとRichのコンソールのインスタンスを作成している。(*2)では、ユーザーが指定した期限の表現を解析して、通知日時を計算する関数を定義している。(*3)では、タスクを追加するコマンドを定義している。その他の関数では、タスクの表示や期限切れの削除、期限到来の確認などの機能を実装している。(*4)では、タスクのリストを表形式で表示する関数を定義している。(*5)では、保存されたタスクを表示するコマンドを定義している。(*6)では、期限が過ぎたタスクを削除するコマンドを定義している。(*7)では、期限が来たタスクを表示するコマンドを定義している。なお、タスクの「date」キーに期限を文字列で記録している。そこで、datetime.strptimeを使って、期限の日付をdatetimeオブジェクトに変換してから現在の日時と比較している。これによって、期限が来たタスクや期限が過ぎたタスクを正しく判定できるようになっている。

なお、関数の定義の上で「@app.command()」というデコレーターを記述することで、その関数がコマンドとして認識されるようになる。例えば、「add」関数は「later.py add」というコマンドで呼び出せるようになる。Typerは、関数の引数や型ヒントをもとに、コマンドの引数やオプションを自動的に処理してくれるため、コマンドラインツールの作成が非常に簡単になる。

タスクツールの使い方を確認しよう

Typerを使って作ったプログラムでは、自動的にプログラムの使い方を表示できるようになっている。Typerが関数の定義から使い方を読み取ってくれるのだ。ターミナルで下記のように「--help」オプションを付けてコマンドを実行してみよう。

# プログラムの使い方を表示する
python later.py --help

すると、下記のように、コマンドの使い方が表示される。コマンドの引数やオプションの説明も表示されていることが確認できるだろう。

  • helpを表示したところ

    helpを表示したところ

2日後に表示するタスクを追加するには、下記のように『later.py add 2d "タスク内容"』の書式でコマンドを実行する。3日後であれば『later.py add 3d "タスク内容"』のように実行する。以下は、2日後の期限を持つ「エアコンの掃除」というタスクを追加する例だ。

# タスクを追加する
python later.py add 2d エアコンの掃除
# タスクの一覧を表示する
python later.py show

実行すると、次のように表示される。なお、『python later.py show』を実行するとタスクの一覧が表示できる。

  • タスクを追加して、タスクの一覧を表示したところ

    タスクを追加して、タスクの一覧を表示したところ

期限を迎えたタスクを表示するには、下記のように『later.py check』のコマンドを実行する。期限が来たタスクが表形式で表示される。

# 期限が来たタスクを表示する
python later.py check
  • 期限が来たタスクを表示したところ

    期限が来たタスクを表示したところ

そして、タスクを確認した後、期限が過ぎたタスクを削除するには、下記のように『later.py clear』のコマンドを実行する。期限が過ぎたタスクが削除され、残りのタスク数が表示される。

# 期限が過ぎたタスクを削除する
python later.py clear

ターミナルを開いた時にタスク期限を確認するように設定しよう

なお、新規でターミナルを開いたときに、自動的に期限が来たタスク一覧を表示するように設定してみよう。そうすれば、期限の確認忘れを防止できるだろう。基本的には、ターミナル起動時に自動実行されるスクリプトに「later.py check」を記述するだけだ。OSごとに設定方法を確認しよう。

【Windowsの場合】

WindowsのPowerShellであれば、ユーザーフォルダにある「~\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1」というファイル($PROFILEの値)をテキストエディタで開いて、下記のような内容を追加しよう。なお、ファイルやフォルダがない場合は作成して追加する必要がある。

$script = Join-Path (Split-Path $PROFILE) "later.py"
python $script check

そして、このファイルと同じフォルダに、先ほど作成した「later.py」と「storage.py」を保存しよう。そうすると、新規PowerShellウィンドウを開いたときに、このスクリプトが自動的に実行され、期限が来たタスクが表示されるようになる。

  • PowerShellを起動したときに

    PowerShellを起動したときに

【macOSの場合】

macOSでデフォルトの「Z shell(zsh)」を使っている場合には、ユーザーフォルダにある「~/.zshrc」というファイルをテキストエディタで開いて、下記のような内容を追記しよう。

LATER_SCRIPT="$HOME/later.py"
alias later='python $LATER_SCRIPT'
later check

ユーザーフォルダに「later.py」と「storage.py」を保存しよう。そうすると、新規ターミナルを開いたときに、このスクリプトが自動的に実行され、期限が来たタスクが表示されるようになる。なお、aliasでlaterコマンドも定義しているので、ターミナルを開いた後に『later check』と入力しても、期限が来たタスクを表示できるようになる。

  • Zshを起動した時にタスクが実行

    Zshを起動した時にタスクが実行

まとめ

今回は、PythonのTyperを使って、N日後(あるいは、N時間後)に通知するリマインダーCLIを作ってみた。ユーザーが指定した期限の表現を解析して、通知日時を計算し、タスクの情報をJSONファイルに保存することで、簡単なリマインダー機能を実装した。さらに、ターミナルを開いたときに期限が来たタスクを自動的に表示するように設定する方法も紹介した。このようなCLIツールは、日常のタスク管理に便利に使えるだけでなく、Pythonのコマンドラインツールの作り方や、ファイル操作の方法を学ぶ良い機会にもなるだろう。ぜひ、自分の環境に合わせてカスタマイズしてみよう。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。これまで50冊以上の技術書を執筆した。直近では、「大規模言語モデルを使いこなすためのプロンプトエンジニアリングの教科書(マイナビ出版)」「Pythonでつくるデスクトップアプリ(ソシム)」「実践力を身につける Pythonの教科書 第2版」「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」など。