6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PowerShellAdvent Calendar 2017

Day 17

PowerShellで簡易ToDoリストを作る

Last updated at Posted at 2017-12-16

概要

  • この記事はPowerShell Advent Calendar 2017の17日目だよ:blush:
  • PowerShellで簡易ToDoリストを作っていく、誰向けかわからない記事だよ:dizzy_face:
  • ところで、Poshってphosのアナグラムだよね(宝石の国脳):gem:

バージョン確認

.ps1
$PSVersionTable.PSVersion
<# 
Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      14393  1770    
#>
  • v5でやっていくよ:laughing:
  • コマンドの実行結果は'<# ... #>'のコメント形式で書いていくよ:scroll:

ToDoリストを表す変数を作る

.ps1
$script:ToDo = @()
  • 配列でToDoリストを表すことにするよ。
  • 効率は度外視、速度も気にしないので、System.Collections系は使わないよ。
  • ToDoリストは複数の関数で操作するので、(あんまり意味は無いけど)変数のスコープscriptにしておくよ。

ToDoを追加する関数を作る

.ps1
filter Add-ToDo
{
    Param
    (
        [Parameter(Mandatory=$True)]
        [string] $Value
    )

    $script:ToDo += New-Object PSObject -Property $PSBoundParameters
}
  • とりあえず関数を作ったよ:wrench:
  • 下記のような感じで動くよ。
.ps1
Add-ToDo "A +Test"
Add-ToDo "B +Test"
$script:ToDo
<#
Value  
-----  
A +Test
B +Test
#>

:doughnut:ひとくちメモ

ToDoリストを表示する関数を作る

  • ToDoリストを呼び出すのにいちいち変数を使うのは何かかっこ悪い感じがするので関数にするよ:tired_face:
  • ついでに何番目のToDoかわかるようにIndexも表示してみるよ:muscle:
.ps1
filter Get-ToDo
{
    $script:ToDo |
        ForEach-Object {$i = 0} {$_; $i++} |
        Select-Object @(
            @{Name = 'Index'; Expression = {$i}}
            'Value'
        )
}
  • 次のように表示できるようになったよ:tada:
.ps1
Get-ToDo
<#
Index Value  
----- -----  
    0 A +Test
    1 B +Test
#>

:doughnut:ひとくちメモ

  • ForEach-Object ...は、スクリプトブロックをいくつか渡せるよ。

    • スクリプトブロックを2つ渡した場合、
    • 1番目のスクリプトブロックがBeginとして一回だけ評価されるよ。
    • 2番目のスクリプトブロックがProcessとして上流からオブジェクトが来るたびに評価されるよ。
  • Select-Objectは、Propertyパラメータに@{Name = ...; Expression = {...}}形式のHashTableを渡せるよ。

    • 元のオブジェクトにない値を格納できるから、ちょっとした計算結果をまとめるのに便利だよ。

Add-ToDoからToDoのプロパティ名を取得する

  • Get-ToDoの実装で'Value'と書いているのが何か負けた気がしてくるので、Add-ToDoからToDoのプロパティ名を取得してみるよ:triumph:
.ps1
filter Get-ToDoPropertyName
{
    Get-Help Add-ToDo |
        Select-Object -ExpandProperty Parameters |
        Select-Object -ExpandProperty Parameter |
        Sort-Object Position |
        Select-Object -ExpandProperty Name
}

filter Get-ToDo
{
    $script:ToDo |
        ForEach-Object {$i = 0} {$_; $i++} |
        Select-Object @(
            @{Name = 'Index'; Expression = {$i}}
            Get-ToDoPropertyName
        )
}

:doughnut:ひとくちメモ

  • Get-HelpGet-Commandで定義した関数のメタデータを拾えたりするよ:hatching_chick:
  • ${function:Add-ToDo}.Astでも拾えるかもしれないよ:evergreen_tree:

取得するToDoをIndexで指定してみる

.ps1
filter Get-ToDo
{
    Param
    (
        [ValidateRange(0, [int]::MaxValue)]
        [int[]] $Index
    )

    $bIndex = @{}
    if ($PSBoundParameters.ContainsKey('Index'))
        {$bIndex['Index'] = $Index}

    $script:ToDo |
        ForEach-Object {$i = 0} {$_; $i++} |
        Select-Object @bIndex |
        Select-Object @(
            @{Name = 'Index'; Expression = {$i}}
            Get-ToDoPropertyName
        )
}
  • 次のようにIndexを指定して表示できるよ:zap:
  • 指定しないと、今まで通り全てのToDoが出力されるよ。
.ps1
Get-ToDo 1
<#
Index Value  
----- -----  
    1 B +Test
#>
  • これだったらGet-ToDo | Select-Object -Index 1で良いじゃないかって:question:
  • そうだね。ただ、スプラッティングを使いたかっただけだよ:innocent:

ToDoリストをクリアする

  • 喋り方に疲れたので、一旦リセットします。
  • ついでに、ToDoリストもリセットします。
.ps1
filter Clear-ToDo
    {$script:ToDo = @()}
.ps1
Add-ToDo "C +Test"
Clear-ToDo
Get-ToDo
<#
#>

ToDoに完了/未完了のプロパティを持たせる

完了できないと、ToDoが無限に増えてしまいます。そこで、完了を表すプロパティをToDoに持たせるため、Add-ToDoを修正します。今回は完了/未完了を表すboolean型のプロパティを追加します。

.ps1
filter Add-ToDo
{
    Param
    (
        [Parameter(Mandatory=$True)]
        [string] $Value,
        
        [boolean] $IsClosed = $False
    )

    $bp = $PSBoundParameters

    Get-ToDoPropertyName |
        Where-Object {-not $bp.ContainsKey($_)} |
        ForEach-Object {$bp[$_] = Get-Variable $_ -ValueOnly}

    $script:ToDo += New-Object PSObject -Property $bp
}
.ps1
Clear-ToDo
Add-ToDo "A"
Add-ToDo "B" $false
Add-ToDo "C" $true
Get-ToDo
<#
Index Value IsClosed
----- ----- --------
    0 A        False
    1 B        False
    2 C         True
#>

メモ

  • $PSBoundParametersは呼び出し側が引数を割り当てたパラメータしか束縛しないため、デフォルト引数の値は別途取ってくる必要がある。
  • 今回はGet-Variable ... -ValueOnlyで変数に格納されているデフォルト値を動的に取得している。

ToDoを未完了から完了にする

いくら完了状態を持たせても、状態を変更できないと意味が無いので、それ用の関数を作ります。

.ps1
filter Close-ToDo
{
    Param
    (
        [Parameter(Mandatory=$True)]
        [ValidateRange(0, [int]::MaxValue)]
        [int[]] $Index
    )

    $script:ToDo |
        Select-Object -Index $Index |
        ForEach-Object {$_.IsClosed = $True}
}
.ps1
Clear-ToDo
Add-ToDo "A"
Add-ToDo "B"
Add-ToDo "C"
Close-ToDo 0, 2
Get-ToDo
<#
Index Value IsClosed
----- ----- --------
    0 A         True
    1 B        False
    2 C         True
#>

既定では完了済みのToDoを表示しないようにする

終わったToDoをいつまで眺めててもしょうがないので、Get-ToDoを修正します。

.ps1
filter Get-ToDo
{
    Param
    (
        [ValidateRange(0, [int]::MaxValue)]
        [int[]] $Index,

        [switch] $Force
    )

    $bIndex = @{}
    if ($PSBoundParameters.ContainsKey('Index'))
        {$bIndex['Index'] = $Index}

    $script:ToDo |
        ForEach-Object {$i = 0} {$_; $i++} |
        Where-Object {$Force -or -not $_.IsClosed} |
        Select-Object @bIndex |
        Select-Object @(
            @{Name = 'Index'; Expression = {$i}}
            Get-ToDoPropertyName
        )
}
.ps1
Clear-ToDo
Add-ToDo "A"
Add-ToDo "B" $true
Add-ToDo "C"
Get-ToDo
<#
Index Value IsClosed
----- ----- --------
    0 A        False
    2 C        False
#>

Get-ToDo -Force
<#
Index Value IsClosed
----- ----- --------
    0 A        False
    1 B         True
    2 C        False
#>

メモ

  • 関数の挙動を切り替えられるようにスイッチパラメータForceを追加
  • Forceに応じて出力するToDoをフィルタリングするようWhere-Objectを追加

完了にするToDoのIndexをパイプライン経由のオブジェクトから受け取る

パイプラインで繋げられるように少し改造。

.ps1
filter Close-ToDo
{
    Param
    (
        [Parameter(
            Mandatory=$True,
            ValueFromPipelineByPropertyName=$True
        )]
        [ValidateRange(0, [int]::MaxValue)]
        [int[]] $Index
    )

    $script:ToDo |
        Select-Object -Index $Index |
        ForEach-Object {$_.IsClosed = $True}
}
.ps1
Clear-ToDo
Add-ToDo "A"
Add-ToDo "B"
Get-ToDo | Close-ToDo
Get-ToDo -Force
<#
Index Value IsClosed
----- ----- --------
    0 A         True
    1 B         True
#>

Clear-ToDo
Add-ToDo "A +test"
Add-ToDo "B"
Add-ToDo "C +test"
Get-ToDo | ? {$_.Value -like "*+test"} | Close-ToDo
Get-ToDo -Force
<#
Index Value   IsClosed
----- -----   --------
    0 A +test     True
    1 B          False
    2 C +test     True
#>

メモ

  • ValueFromPipelineByPropertyNameを指定すると、パイプライン経由で飛んでくるオブジェクトのプロパティを引数の値として取得できる。

パイプライン経由で引数を取得する

パイプラインで繋げられると便利そうなので、Add-ToDoGet-ToDoClose-ToDoと同様パイプラインのプロパティから値を取得するよう変更する。

.ps1
function Add-ToDo
{
    Param
    (
        [Parameter(
            Mandatory=$True,
            ValueFromPipelineByPropertyName=$True
        )]
        [string] $Value,

        [Parameter(ValueFromPipelineByPropertyName=$True)]        
        [boolean] $IsClosed = $False
    )

    $bp = $PSBoundParameters

    Get-ToDoProperty |
        Where-Object {-not $bp.ContainsKey($_)} |
        ForEach-Object {$bp[$_] = Get-Variable $_ -ValueOnly}

    $script:ToDo += New-Object PSObject -Property $bp
}

filter Get-ToDo
{
    Param
    (
        [Parameter(ValueFromPipelineByPropertyName=$True)]
        [ValidateRange(0, [int]::MaxValue)]
        [int[]] $Index,

        [switch] $Force
    )

    $bIndex = @{}
    if ($PSBoundParameters.ContainsKey('Index'))
        {$bIndex['Index'] = $Index}

    $script:ToDo |
        ForEach-Object {$i = 0} {$_; $i++} |
        Where-Object {$Force -or -not $_.IsClosed} |
        Select-Object @bIndex |
        Select-Object @(
            @{Name = 'Index'; Expression = {$i}}
            Get-ToDoProperty
        )
}
.ps1
Clear-ToDo
Add-ToDo "A +test"
Get-ToDo | % {$_.Value = $_.Value -replace "A", "B"; $_} | Add-ToDo
Get-ToDo | Get-ToDo
<#
Index Value   IsClosed
----- -----   --------
    0 A +test    False
    1 B +test    False
#>

どうせならもっとパイプラインでつなげる

Add-ToDoClose-ToDoの後ろにもパイプラインを繋げられるように改造してみます。

.ps1
filter Add-ToDo
{
    Param
    (
        [Parameter(
            Mandatory=$True,
            ValueFromPipelineByPropertyName=$True
        )]
        [string] $Value,

        [Parameter(ValueFromPipelineByPropertyName=$True)]        
        [boolean] $IsClosed = $False,

        [switch] $PassThru
    )

    $bp = $PSBoundParameters

    Get-ToDoPropertyName |
        Where-Object {-not $bp.ContainsKey($_)} |
        ForEach-Object {$bp[$_] = Get-Variable $_ -ValueOnly}

    $script:ToDo += New-Object PSObject -Property $bp

    if ($PassThru)
        {Get-ToDo -Index ($script:ToDo.Count - 1) -Force}
}

filter Get-ToDoPropertyName
{
    Get-Help Add-ToDo |
        Select-Object -ExpandProperty Parameters |
        Select-Object -ExpandProperty Parameter |
        Where-Object {$_.pipelineInput -match 'ByPropertyName'} |
        Sort-Object Position |
        Select-Object -ExpandProperty Name
}

filter Close-ToDo
{
    Param
    (
        [Parameter(
            Mandatory=$True,
            ValueFromPipelineByPropertyName=$True
        )]
        [ValidateRange(0, [int]::MaxValue)]
        [int[]] $Index,

        [switch] $PassThru
    )

    $script:ToDo |
        Select-Object -Index $Index |
        ForEach-Object {$_.IsClosed = $True}

    if ($PassThru)
        {Get-ToDo -Index $Index -Force}
}
.ps1
Clear-ToDo
Add-ToDo "A +test"
Add-ToDo "B +test" -PassThru | Close-ToDo -PassThru
<#
Index Value   IsClosed
----- -----   --------
    1 B +test     True
#>

メモ

  • スイッチパラメータPassThruを付けて、PassThruが有効であれば内部でGet-ToDoを呼べば後続のパイプラインに繋げられるようになります。
  • ただし、Add-ToDoPassThruのパラメータが増える関係上、Get-ToDoPropertyNameも改造する必要があります。

ToDoを未完了にする

CloseがあるならOpenもあるよね。

.ps1
filter Open-ToDo
{
    Param
    (
        [Parameter(
            Mandatory=$True,
            ValueFromPipelineByPropertyName=$True
        )]
        [ValidateRange(0, [int]::MaxValue)]
        [int[]] $Index,

        [switch] $PassThru
    )

    $script:ToDo |
        Select-Object -Index $Index |
        ForEach-Object {$_.IsClosed = $False}

    if ($PassThru)
        {Get-ToDo -Index $Index -Force}
}
.ps1
Clear-ToDo
Add-ToDo "A +test" -PassThru | Close-ToDo -PassThru | Open-ToDo -PassThru
<#
Index Value   IsClosed
----- -----   --------
    0 A +test    False
#>

ToDoを削除する

完了にするだけじゃなくて、削除もしたいよね。

.ps1
function Remove-ToDo
{
    Param
    (
        [Parameter(
            Mandatory=$True,
            ValueFromPipelineByPropertyName=$True
        )]
        [ValidateRange(0, [int]::MaxValue)]
        [int[]] $Index,

        [switch] $PassThru
    )

    Begin
        {$is = @()}

    Process
        {$is += $Index}

    End
    {
        if ($PassThru)
            {$ret = Get-ToDo -Index $is -Force}

        $i = 0
        $script:ToDo = @(
            $script:ToDo | Where-Object {$is -notcontains $i; $i++}
        )

        if ($PassThru)
            {$ret}    
    }
}
.ps1
Clear-ToDo
Add-ToDo "A +test"
Add-ToDo "B +test"
Add-ToDo "C +test"
Get-ToDo 0, 2 | Remove-ToDo -PassThru
<#
Index Value   IsClosed
----- -----   --------
    0 A +test    False
    2 C +test    False
#>

Get-ToDo
<#
Index Value   IsClosed
----- -----   --------
    0 B +test    False
#>

ToDoを編集する

編集できないとか不便過ぎるので。

.ps1

filter Set-ToDo
{
    Param
    (
        [Parameter(
            Mandatory=$True,
            ValueFromPipelineByPropertyName=$True
        )]
        [ValidateRange(0, [int]::MaxValue)]
        [int[]] $Index,

        [Parameter(ValueFromPipelineByPropertyName=$True)]
        [string] $Value,

        [Parameter(ValueFromPipelineByPropertyName=$True)]        
        [boolean] $IsClosed,

        [switch] $PassThru
    )

    $keys =
        Get-ToDoPropertyName |
        Where-Object {$PSBoundParameters.ContainsKey($_)}

    $script:ToDo |
        Select-Object -Index $Index |
        ForEach-Object {
            foreach ($key in $keys)
                {$_.$key = $PSBoundParameters[$key]}
        }

    if ($PassThru)
        {Get-ToDo -Index $Index -Force}
}
.ps1
Clear-ToDo
Add-ToDo "A +test"
Add-ToDo "B +test"
Get-ToDo | % {$_.Value = $_.Value -replace "test", "set"; $_} | Set-ToDo
Get-ToDo
<#
Index Value  IsClosed
----- -----  --------
    0 A +set    False
    1 B +set    False
#>

唐突なおわり

おわりです。急に思い立ってアドベントカレンダーに参加したもんだからよくわからない記事ができてしまった。正直すまんかった。

なお、下記コードでToDoを並べ替えることはできますが、その機能をまとめたSort-ToDoの実装は読者の演習課題とします。

.ps1
Get-ToDo -Force | Remove-ToDo -PassThru | Sort-Object IsClosed, Value | Add-ToDo
6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?