PowerShell で ToDo 管理

これはPowerShell Advent Calendar 2014 : ATND 12日目の記事です。

PowerShellでToDo管理するスクリプトモジュールを作りました。

元々、PowerShellスクリプトを書く練習用に作り始めたのですが、ちょっとしたメモなどを手軽に書き留めておけるので、割と重宝しています。

  • コマンド1つでToDoの追加、削除、一覧できます。
  • ToDoの項目毎にステータスを付けられます。
    • Todo,Doing(着手・進行中),Done(完了)
    • Milestone (期日・イベント事等、マイルストーン)
  • 特に重要な項目にフラグ!を付けることが出来ます。
  • ステータス、フラグの状態で色分け表示

f:id:pierre3:20141126143138p:plain

コードは、github に置いてあります。 posh-todo.psm1 だけ落とせば使用できます。 https://github.com/pierre3/posh-todo

使い方

使い方は、以下の実行例を参照ください。
ちなみに、PowerShell_ISE 上での使用を推奨しています。(PowerShell.exe の方でも使用は可能ですが、色分けがちゃんとできないです。)

完全に自分用に作ったものなので、他の人にとって使いやすいかどうかは微妙なところですが、気になった方(がもしいれば)是非試してみて下さい。

TIPS

以下、小ネタです。

PowerShell起動時にposh-todoを開始してTODO一覧を表示

PowerShell(ISE)のプロファイルに以下の2行を追加しておくと、起動時にposh-todoが開始されて登録済みのToDo一覧が表示されるようになります。

Import-Module path\to\posh-todo.psm1
Start-PoshTodo path\to\todoFile.json

単にToDo一覧を表示するだけでは味気ないので、ようこそ!的な挨拶文も表示したいですね。
以下をStart-PoshTodo 内で呼ぶようにします。

function Show-StartingMessage
{
    $today = (Get-Date).ToString("yyyy/MM/dd")
    Write-Host "こんにちは $env:USERNAME さん! 今日は $today です。"
    Write-Host ('{0}も{1}{2}{3}{4}ぞい!' -f ('今日', '1', '日', 'がん', 'ばる' | % { ($_,'ぞい')[(random 2)] }))
}

これでPowerShellを起動するたびに以下のように表示されるようになります。

f:id:pierre3:20141202215520p:plain

日付の入力はダイアログで

ToDo追加時のAdd-Todoコマンドで -setDate スイッチを指定すると、次のようなダイアログから日付を指定できるようになります。

f:id:pierre3:20141126150054p:plain

このダイアログは、「Hey, Scripting Guy Windows PowerShell を使用して日付を入力する手間を省く方法はありますか 」から頂きました。

一部手を加えて、ダイアログのタイトルを指定したり、Enterキーでダイアログを閉じれるようにしたりしています。
コントロールにイベントハンドラを追加するにはAdd_KeyPressのようなAdd_ + イベント名メソッドを使用するのですね。

ToDoデータは、.Netオブジェクトで保持してJSON 形式で保存

ToDoデータの扱い

ToDoデータは、後で扱うのに型があった方が何かと便利であろうと考え、.Netオブジェクトで保持するようにしました。

Add-Type -TypeDefinition @'
using System;
public enum TodoStatus
{
    Todo, Milestone, Doing, Done
}
public class TodoItem
{
    public int index { get; set; }
    public string text { get; set; }
    public DateTime date { get; set; }
    public bool flag { get; set; }
    public TodoStatus status { get; set; }
}
'@

保存はJSON

データは、ConvertTo-Json コマンドとConvertFrom-Jsonコマンドを使ってJSON形式で保存、復元します。
書き出し時は、TodoItemの配列をそのままConvertTo-Jsonに渡すだけで楽ちんなのですが、
読み取った結果はPSCustomObjectで帰ってくるため、TodoItemオブジェクトに格納するのにひと手間かかります。

# 書き出しはTodoItem の配列をそのまま渡すだけでOK
PS C:\> ConvertTo-Json $todo.items | Out-File $path -Encoding utf8

# 読み取った結果はPSCustomObjectの配列で帰ってくる。TodoItemに変換するのがメンドイ
PS C:\> obj = Get-Content $todo.filePath -Raw -Encoding UTF8 | ConvertFrom-Json
PS C:\> $todo.items = $obj | % { 
  $todoitem = New-Object 'TodoItem' 
  $todoitem.date = $_.date
  $todoitem.flag = $_.flag
  $todoitem.index = $_.index
  $todoitem.status = $_.status
  $todoitem.text = $_.text
  $todoitem
}

変換部分の記述がちょっと面倒です。.Net(TodoItem)側で何とかしたいですね。

明示的型変換を実装

という事で、明示的型変換を実装してキャスト一発で変換可能にしてみます。

Add-Type -TypeDefinition @'
using System;
using System.Management.Automation;
public enum TodoStatus
{
    Todo, Milestone, Doing, Done
}
public class TodoItem
{
    public int index { get; set; }
    public string text { get; set; }
    public DateTime date { get; set; }
    public bool flag { get; set; }
    public TodoStatus status { get; set; }
    public static explicit operator TodoItem(PSObject source)
    {
        return new TodoItem()
        {
            index = (int)source.index,
            text = (string)source.text,
            date = (DateTime)source.date,
            flag = (bool)source.flag,
            status = (TodoStatus)source.status
        }
    }
}
'@

これでOK!と思いきや、これではコンパイルが通りません。
変換元のPSObjectは、PowerShellの世界では動的に型を解決してくれますが、.Net側では「そんなメンバ定義されていませんよ」と怒られてしまいます。

インデクサでできない? index =(int)source["index"] ...これもダメでした。

.Net側でPSObjectのプロパティにアクセスするには?

.Net側ではProperties というメンバからプロパティの値を取得する必要があるようです。
source.Properties["propertyName"].Value のように記述します。
(このPropertiesですが、PowerShell側からは見えない(参照できない)ため、その存在に気付けず苦労しました。)

Add-Type -TypeDefinition @'
using System;
using System.Management.Automation;
public enum TodoStatus
{
    Todo, Milestone, Doing, Done
}
public class TodoItem
{
    public int index { get; set; }
    public string text { get; set; }
    public DateTime date { get; set; }
    public bool flag { get; set; }
    public TodoStatus status { get; set; }
    public static explicit operator TodoItem(PSObject source)
    {
        return new TodoItem()
        {
            index = (int)source.Properties["index"].Value,
            text = (string)source.Properties["text"].Value,
            date = (DateTime)source.Properties["date"].Value,
            flag = (bool)source.Properties["flag"].Value,
            status = (TodoStatus)source.Properties["status"].Value
        }
    }
}
'@

これで、無事コンパイルできるようになりました。キャストもちゃんと動きます。

# 読み取り処理もスッキリ!
$obj = Get-Content $todo.filePath -Raw -Encoding UTF8 | ConvertFrom-Json
$todo.items = $obj | % { [TodoItem]$_ }

# Todoの追加時も、HashTableのキャストでOK!
$todo.items += [TodoItem]@{
    index = 5;
    text = "todo";
    date = "2014/12/1";
    flag = false;
    status = [TodoStatus]::Todo;
}

PSObjectのプロパティにアクセスする方法の別解

Dynamicを使えばPSObjectのプロパティに.でアクセス可能になります。
但し、Dynamicを使用するには"Microsoft.CSharp"を参照アセンブリに指定してあげる必要があります。

public class TodoItem
{
    public int index { get; set; }
    public string text { get; set; }
    public DateTime date { get; set; }
    public bool flag { get; set; }
    public TodoStatus status { get; set; }
    public static explicit operator TodoItem(PSObject source){
        dynamic d = source;
        return new TodoItem(){
            index = (int)d.index,
            text = (string)d.text,
            date = (DateTime)d.date,
            flag = (bool)d.flag,
            status = (TodoStatus)d.status
        };
    }
}
'@ -ReferencedAssemblies "Microsoft.CSharp"