概要
- この記事はPowerShell Advent Calendar 2017の17日目だよ
- PowerShellで簡易ToDoリストを作っていく、誰向けかわからない記事だよ
- ところで、Poshってphosのアナグラムだよね(宝石の国脳)
バージョン確認
$PSVersionTable.PSVersion
<#
Major Minor Build Revision
----- ----- ----- --------
5 1 14393 1770
#>
- v5でやっていくよ
- コマンドの実行結果は'<# ... #>'のコメント形式で書いていくよ
ToDoリストを表す変数を作る
$script:ToDo = @()
- 配列でToDoリストを表すことにするよ。
- 効率は度外視、速度も気にしないので、System.Collections系は使わないよ。
- ToDoリストは複数の関数で操作するので、(あんまり意味は無いけど)変数のスコープを
script
にしておくよ。
ToDoを追加する関数を作る
filter Add-ToDo
{
Param
(
[Parameter(Mandatory=$True)]
[string] $Value
)
$script:ToDo += New-Object PSObject -Property $PSBoundParameters
}
- とりあえず関数を作ったよ
- 下記のような感じで動くよ。
Add-ToDo "A +Test"
Add-ToDo "B +Test"
$script:ToDo
<#
Value
-----
A +Test
B +Test
#>
ひとくちメモ
-
Param(...)
の意味がわからない場合は、about_Functions_Advanced_Parametersを読むと良いよ。 -
+=
の意味がわからない場合は、about_Assignment_Operatorsを読むと良いよ。 -
$PSBoundParameters
の意味がわからない場合は、about_Automatic_Variablesを読むと良いよ。 -
New-Object
については、New-Objectを読んでもよくわからないので、カスタムオブジェクトでググろう。
ToDoリストを表示する関数を作る
- ToDoリストを呼び出すのにいちいち変数を使うのは何かかっこ悪い感じがするので関数にするよ
- ついでに何番目のToDoかわかるようにIndexも表示してみるよ
filter Get-ToDo
{
$script:ToDo |
ForEach-Object {$i = 0} {$_; $i++} |
Select-Object @(
@{Name = 'Index'; Expression = {$i}}
'Value'
)
}
- 次のように表示できるようになったよ
Get-ToDo
<#
Index Value
----- -----
0 A +Test
1 B +Test
#>
ひとくちメモ
-
ForEach-Object ...
は、スクリプトブロックをいくつか渡せるよ。- スクリプトブロックを2つ渡した場合、
- 1番目のスクリプトブロックがBeginとして一回だけ評価されるよ。
- 2番目のスクリプトブロックがProcessとして上流からオブジェクトが来るたびに評価されるよ。
-
Select-Object
は、Propertyパラメータに@{Name = ...; Expression = {...}}
形式のHashTableを渡せるよ。- 元のオブジェクトにない値を格納できるから、ちょっとした計算結果をまとめるのに便利だよ。
Add-ToDoからToDoのプロパティ名を取得する
-
Get-ToDo
の実装で'Value'
と書いているのが何か負けた気がしてくるので、Add-ToDo
からToDoのプロパティ名を取得してみるよ
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
)
}
ひとくちメモ
-
Get-Help
やGet-Command
で定義した関数のメタデータを拾えたりするよ -
${function:Add-ToDo}.Ast
でも拾えるかもしれないよ
取得するToDoをIndexで指定してみる
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を指定して表示できるよ
- 指定しないと、今まで通り全てのToDoが出力されるよ。
Get-ToDo 1
<#
Index Value
----- -----
1 B +Test
#>
- これだったら
Get-ToDo | Select-Object -Index 1
で良いじゃないかって - そうだね。ただ、スプラッティングを使いたかっただけだよ
ToDoリストをクリアする
- 喋り方に疲れたので、一旦リセットします。
- ついでに、ToDoリストもリセットします。
filter Clear-ToDo
{$script:ToDo = @()}
Add-ToDo "C +Test"
Clear-ToDo
Get-ToDo
<#
#>
ToDoに完了/未完了のプロパティを持たせる
完了できないと、ToDoが無限に増えてしまいます。そこで、完了を表すプロパティをToDoに持たせるため、Add-ToDo
を修正します。今回は完了/未完了を表すboolean型のプロパティを追加します。
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
}
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を未完了から完了にする
いくら完了状態を持たせても、状態を変更できないと意味が無いので、それ用の関数を作ります。
filter Close-ToDo
{
Param
(
[Parameter(Mandatory=$True)]
[ValidateRange(0, [int]::MaxValue)]
[int[]] $Index
)
$script:ToDo |
Select-Object -Index $Index |
ForEach-Object {$_.IsClosed = $True}
}
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
を修正します。
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
)
}
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をパイプライン経由のオブジェクトから受け取る
パイプラインで繋げられるように少し改造。
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}
}
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-ToDo
とGet-ToDo
をClose-ToDo
と同様パイプラインのプロパティから値を取得するよう変更する。
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
)
}
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-ToDo
とClose-ToDo
の後ろにもパイプラインを繋げられるように改造してみます。
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}
}
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-ToDo
にPassThru
のパラメータが増える関係上、Get-ToDoPropertyName
も改造する必要があります。
ToDoを未完了にする
Close
があるならOpen
もあるよね。
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}
}
Clear-ToDo
Add-ToDo "A +test" -PassThru | Close-ToDo -PassThru | Open-ToDo -PassThru
<#
Index Value IsClosed
----- ----- --------
0 A +test False
#>
ToDoを削除する
完了にするだけじゃなくて、削除もしたいよね。
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}
}
}
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を編集する
編集できないとか不便過ぎるので。
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}
}
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
の実装は読者の演習課題とします。
Get-ToDo -Force | Remove-ToDo -PassThru | Sort-Object IsClosed, Value | Add-ToDo