読者です 読者をやめる 読者になる 読者になる

私をペロペロするがよい

lazy_dog のブログです。

Google Calendar と todo.txt を連携させた「todo-txt_gcaladd.sh」

shell

TODO 管理を 2Do から todo.txt に替えた。todo.txt の場合、データファイルが通常のテキストであり、CLI のテキストユーティリティから参照や連携が容易。CLI で連携できるということで、Google Calendar と連携できるようにした。 下記、作ったスクリプトの紹介と使いかたについて。

todo-txt_gcaladd.sh について

  • due:yyyy-mm-ddgcal:on が入力されている行のタスクを、due:yyyy-mm-dd の日のスケジュールとして Google Calendar に登録する。
  • タスクがコンプリートした場合、具体的には todo.txtdone.txt に記載されているタスクの先頭に x が入力されたタスクについては、Google Calendar からスケジュールを削除する。 具体的な todo-txt_gcaladd.sh の動作は、下記「具体的に todo-txt_gcaladd.sh はどういう動きをしているのか?」項を参照。 色々テストはしているが、 うちの環境では きちんと動いている。"AT YOUR OWN RISK" でお願いします。

使用に必要なもの

$ gcalcli list でリスト一覧が取れれば問題なく動作すると思われる。

使い方

1. gcalcli をインストール及び設定

https://github.com/insanum/gcalcli の通り。

2. todo-txt_gcaladd.sh をインストール及び設定

https://bitbucket.org/lazy_dog/todo-txt_options/src からダウンロード願います。適当にパスが通るところなどに保存する。 スクリプト内の変数にオプションの指定があるので、下記の通り設定する。

## Temporary data directory
TMPDIR=/tmp

## Target calendar name
TARGETCALENDAR=TODOs

# todo.txt
TARGETTODOTXT=${TODODIR}/todo.txt

# done.txt
TARGETDONETXT=${TODODIR}/done.txt
  • TMPDIR=: 一時ファイルの作成ディレクトリ。 # アクセスできて write ができないと todo.txt が更新されないバグに、今これを書いている途中気付いた。。。
  • TARGETCALENDAR=: Google Calendar のスケジュールを登録するカレンダーの指定。
  • TARGETTODOTXT=: ターゲットとなる todo.txt へのパス。
  • TARGETDONETXT=: ターゲットとなる done.txt へのパス。

3. todo-txt_gcaladd.sh を cron に登録する

cron など自動実行ができるようにする。もちろん、手動で todo-txt_gcaladd.sh を叩くでも問題はない。

4. todo.txt へタスクを追加

todo.txt に Google Calendar と連携したい TODO を追加する。その行のどこにでもいいので、due:yyyy-mm-dd という形で日付を指定。gcal:on というオプションを付与する。ただし、上記 key:value の前後に半角スペースがないと上手く動かないと思うので注意。

図書館に本を返す due:2016-10-20 gcal:on

行頭に x が付いた完了タスクは、自動的に Google Calendar から削除される。

具体的に todo-txt_gcaladd.sh はどういう動きをしているのか?

todo.txt の内容を読み込み、Google Calendar にスケジュールをポストする。例えば、todo.txt に下記のようなタスクを追加したとする。

図書館に本を返す due:2016-10-20 gcal:on

due の日付は yyyy-mm-dd が必須とした。理由は SwiftoDo http://swiftodoapp.com/ という iPhone アプリが左記フォーマットの ISO 8601 https://ja.wikipedia.org/wiki/ISO_8601 を必須としているので、それに倣った。また gcal: という key に対し value が on でなければ連携は発動しない。 cron などで実行された todo-txt_gcaladd.sh は該当の行を確認し、gcalcli を使用して Google Calendar にスケジュールをポストする。

f:id:lazy_dog:20161016021713p:plain

スケジュールの詳細には tuid:.... という文字列が追記されているが、これが todo.txt と Google Calendar のスケジュールを紐付けている。また、この時 todo.txt の該当のタスクの行は下記の通り gcal: の value が uid に書き変わる。

図書館に本を返す due:2016-10-20 gcal:.5801c61eTa

タスクが完了した場合、todo.txt または done.txt の行の頭には x を付けるというルールがある。

x 図書館に本を返す due:2016-10-20 gcal:.5801c61eTa

todo-txt_gcaladd.sh は該当の行を確認し、Google Calendar から該当のスケジュールを削除する。同時に、該当の行の gcal: の value を done に変更する。

x 図書館に本を返す due:2016-10-20 gcal:done

todo-txt_gcaladd.sh が定期的に実行されれば、特に Google Calendar を手動で操作する必要はなく、全て todo.txt 側の操作のみで完結する。

余談: スクリプト書いていたときの雑多なこと

uid について

gcal:... 及び Google Calendar のスケジュールに記入される tuid:... について、ユニークな値をどうするか悩んだ。結論は、1970-01-01 00:00:00 UTC からの現在の秒数を 16 進数にし、それにランダムな文字 2 つとした。

GETDATE=`date +"%s"`
TODOUID=`printf '%x\n' ${GETDATE}`
TODOUID=`echo ${TODOUID}``cat /dev/urandom | env LC_CTYPE=C tr -dc '0-9a-zA-Z' | fold -w 2 | head -n 1`

兎に角、ユニークな値にしたかったので、処理時の時間であればユニーク値になるであろうとした。それを 16 進数にすることで桁数(文字数)を稼いだ。ただし、処理が早いから同じ秒で実行され、uid が被るという現象が確認できたので、/dev/urandom から 2 つの文字を持ってくることにした。 date コマンドでナノ秒を出すこともできるが、BSD 版の date コマンドでは使用できず。

# from "man date" from Cygwin
%N     nanoseconds (000000000..999999999)

uid の桁は単純に 16 進数ではなく、工夫すればもっと桁を稼げると思うが、とりあえずはこうした。

正規表現はどの環境でも使えるように

どのUNIXコマンドでも使える正規表現 - Qiita: http://qiita.com/richmikan@github/items/b6fb641e5b2b9af3522e

こちらの記事を参考に、正規表現を書くことを意識した。と言っても、下記ぐらいしか気をつけるべきところはなかったが。

  • 1 文字以上の繰り返しである + を使わず \{1,\} とした。
  • 最短一致は使用せず、その状態でもマッチするようにした。

他、半角スペース * などで繰り返しを行う場合、[ ] と指定して見易くなるように心掛けてみた。

三項演算子な書き方を多様してスリムにした

if で複数行に渡るのではなく、可能であれば三項演算子のような書き方を多様した。

## Is exist target todo.txt file
[ -e "${TARGETTODOTXT}" ] || { echo "Target todo.txt file could not be found."; exit 1; }
## check if ${line} is "^$"
### 2016-10-26: break だと while read の loop が終了してしまうので、continue に修正。
[ -z "${line}" ] && continue

ref: 初心者向け、「上手い」シェルスクリプトの書き方メモ - Qiita: http://qiita.com/m-yamashita/items/889c116b92dc0bf4ea7d

複雑なことでなければ、ワンライナーかつシンプルに記載ができる。 なお、{ CMD; }( CMD ) は違うことを意識する。 ( CMD ) はサブシェルであり、子プロセスとして動く。

# 上手く動かない
[ -e "${TARGETTODOTXT}" ] || ( echo "Target todo.txt file could not be found." && exit 1 )

上記は、|| 以降がサブシェルとして動いてしまうので、exit 1 はサブシェルである子プロセスの bash を exit するだけになる。そのため、後続のスクリプトも動いてしまう。

[ -e "${TARGETTODOTXT}" ] || { echo "Target todo.txt file could not be found."; exit 1; }

こちらが正解。

ref: bashの";", "&&", "||" に関する補足ネタ。コマンドグルーピングとの併用例 - Qiita: http://qiita.com/jpshadowapps/items/3f3fa3b214a998afd819

type コマンドでファイル(コマンド)があるかを確認

## Is exit gcalcli command
type gcalcli || { echo "gcalcli command could not be found."; exit 3; }

Bashでコマンドの存在チェックはwhichよりhashの方が良いかも→いやtypeが最強 - Qiita: http://qiita.com/kawaz/items/1b61ee2dd4d1acc7cc94

上記の記事に倣った。対象のファイルやディレクト/dev 以下のデバイスにも使えるので、これからも積極的に使っていこうと思う。

変数に格納した改行のあるテキストをヒアドキュメント(while read line)にする場合

while read line
do
    # check if ${line} is "^$"
    ## 2016-10-26: continue に修正
    [ -z "${line}" ] && continue


...

done <<< "${TARGET}"

この形式なら間違いなく処理してくれると思う。変数に空行が入っていても continue が聞くので、その空行は処理されない。色々試してみたが、これが一番シンプルで確実に動く。

ref: bash - How do I "read" a variable on a while loop - Stack Overflow: http://stackoverflow.com/questions/13122441/how-do-i-read-a-variable-on-a-while-loop

sed の -i オプションは使わない

有名な話であるが、sed コマンドの -i オプションは GNUsed のみであり、BSD 環境などでは使用できない場合がある。それを Twitter で呟いたら、perl を使用するという案を頂いた。

というご意見を頂いたのにもかかわらず、POSIX にこだわりたいというのがあったので中間ファイルを作るということで妥協した。perlPOSIX には入っていないようだ。

ref: POSIXコマンドチートシート(を作る) - Qiita: http://qiita.com/richmikan@github/items/e4cb1537d38966c10f4b

gcalcli の delete や search はスケジュールのタイトルのみならず、詳細も見てくれる

表題の通り。

~ $ gcalcli search "uid:1234abcd"

2016-10-15   9:00am  テスト "delete" me!

~ $ gcalcli delete "uid:1234abcd"

2016-10-15   9:00am  テスト "delete" me!
Delete? [N]o [y]es [q]uit: n

~ $ gcalcli --iamaexpert delete "uid:1234abcd"

2016-10-15   9:00am  テスト "delete" me!
Deleted!

~ $