この記事は、PowerShell Advent Calendar 2017 5日目の記事です。
昨日は @atworksさんのPSRemotingを用いたリモートプロセス実行でした。
3日目の前回PowerShellのコーディングスタイルについて触れました。
次は、残りのAPIデザインを見てみましょう。
※ 2回に分けて書きましたが個人的には個人/チームが書きやすいようにすればいい話だと思っています。しかし、「曖昧だった基本的な指針がわからず困ってた方」にとって、ベストプラクティスはいい材料です。PowerShellコミュニティは結構活発なので、コミュニティの中で皆様がよいPowerShell生活を送られることを祈っています。
- C#のデザインガイド
- Required Development Guidelines
- Design Guidelines Use Only Approved Verbs (RD01)
- (Design Guidelines) Cmdlet Names: Characters that cannot be Used (RD02)
- (Design Guidelines) Support Confirmation Requests (RD04)
- (Design Guidelines) Support Force Parameter for Interactive Sessions (RD05)
- (Design Guidelines) Document Output Objects (RD06)
- Strongly Encouraged Development Guidelines
- (Design Guidelines) Advisory Development Guidelines
- Required Development Guidelines
- PowerShell のデザインガイド
- Building Reusable Tools
- TOOL-01 Decide whether you're coding a 'tool' or a 'controller' script
- TOOL-02 Make your code modular
- TOOL-03 Make tools as re-usable as possible
- TOOL-04 Use PowerShell standard cmdlet naming
- TOOL-05 Use PowerShell standard parameter naming
- TOOL-06 Tools should output raw data
- TOOL-07 Controllers should typically output formatted data
- WAST-01 Don't re-invent the wheel
- WAST-02 Report bugs to Microsoft
- Output and Formatting
- Don't use Write-Host unless you really mean it
- Use Write-Progress to give progress information to someone running your script
- Use Write-Debug to give information to someone maintaining your script
- Use CmdletBinding if you are using output streams
- Use Format Files for your custom objects
- Only output one "kind" of thing at a time
- Two important exceptions to the single-type rule
- Error Handling
- Performance
- Security
- Language, Interop and .Net
- Building Reusable Tools
- 余談 : 個人的に注意していること
- まとめ
C#のデザインガイド
Required Development Guidelines
Design GuidelinesとCoding Guidelinesがありますが、Design Guidelinesのみ触れます。
Design Guidelines Use Only Approved Verbs (RD01)
- PowerShellのCmdlet規則であるVerb-Nown形式で公開する時に、あらかじめ定義されてある動詞(Verb) を用いる
- 動詞は用途ごとにクラス分離されている
- VerbsCommon
- VerbsCommunications
- VerbsData
- VerbsDiagnostic
- T:System.Management.Automation.VerbsLifeCycle
- VerbsSecurity
- VerbsOther
- どの動詞をいつ使うかのガイドラインも公開されている
(Design Guidelines) Cmdlet Names: Characters that cannot be Used (RD02)
- Cmdletに使えない特殊文字がある
Parameters Names that cannot be Used (RD03)
- PowerShell Cmdletのパラメータには予約語があるので避ける
Confirm, Debug, ErrorAction, ErrorVariable, OutBuffer, OutVariable, WarningAction, WarningVariable, WhatIf, UseTransaction, and Verbose
(Design Guidelines) Support Confirmation Requests (RD04)
- もしシステム変更を伴う操作を提供する場合は、PowerShellが持っている確認機構を用いる
いわゆるShouldProcessを指しています。
(Design Guidelines) Support Force Parameter for Interactive Sessions (RD05)
- 対話的実行を提供する場合に、
Forceパラメータは提供しましょう
これはPowerShellが自動化を念頭に置かれた言語なため、対話実行でそれを妨害することを防ぐためです 以下のような操作が特に注意。
Prompt PSHostUserInterface.PromptForChoice IHostUISupportsMultipleChoiceSelection.PromptForChoice Overload:System.Management.Automation.Host.PSHostUserInterface.PromptForCredential ReadLine ReadLineAsSecureString
(Design Guidelines) Document Output Objects (RD06)
- 出力の記述。C#ドキュメントXMLで説明を公開するとよい
Strongly Encouraged Development Guidelines
次は強く要請するガイドラインです。
Design GuidelinesとCoding Guidelinesがありますが、Design Guidelinesのみ触れます。
(Design Guidelines) Use a Specific Noun for a Cmdlet Name (SD01)
Serverのような汎用的な言葉ではなく、操作対象を明示したProcessのような命名が好まれます。
(Design Guidelines) Use Pascal Case for Cmdlet Names (SD02)
Cmdlet名は、PascalCaseで表現しましょう。Clear-ItemPropertyのほうがclear-itemPropertyより好ましいです。
(Design Guidelines) Parameter Design Guidelines (SD03)
- 標準的なパラメータ名が公開されているのでここにあるものはそれを利用しましょう
たとえば、名前ならName、出力ならOutputといった具合です
ただ、処理によってはServiceNameのようなより明示的な名前も提供したい場合があるでしょう。その場合は、パラメータ用プロパティにAlias属性を用いることで[Alias("ServiceName")]のように表現できます
- 単一要素を受けるパラメータには単数名を用いて表現しましょう
もし-esのような複数名を用いる場合は、そパラメータがいつでも複数要素を受け入れる場合にしましょう
- パラメータはPascalCaseで。C# であればPropertyを用いるので、C# デザインガイドと同じで違和感はないでしょう
errorActionやerroractionより、ErrorActionが好ましいです
- パラメータの組み合わせで操作が変わるCmdletを提供する場合2つの方法がある
enumを用いて、enumごとに操作を分岐しパラメータを処理する方法 ValudateSet属性.aspx)を用いて、パラメータの入力を制約する方法 経験上、複数の操作を提供するCmdletは作りたくなります。パラメータ1つだけが特定のパラメータ時に用いないようなの「単純な組み合わせ」であれば、ValidateSetが楽でしょう。が、複数のパラメータの組み合わせをParameterSetで表現するのはおススメしません。Cmdletを分離することを検討するといいでしょう
- パラメータ名にはStandard Typeを用いましょう
あらかじめ、どんな用途(アクティビティ) にどんなパラメータが期待されるのかリストされています
Appendなど利用者が直感的に利用しやすいAPIデザインとして一貫性を保つため、該当するものを利用するといいでしょう
- 強く型付けされた .NET Framework型を利用する
Objectを利用するということは、型に対する意識が強いということです。
.NET Frameworkの型を意識して利用すると型を使って値が保障できます。たとえば、URIにはUri型を用いれば、String型が入ってこないことを保証できます。
- パラメータの型に首尾一貫性をもたせる
たとえば、ProcessパラメータというのにInt16型を当てた場合、他のCmdletのProcessパラメータでUint16を用いるのは避けましょう。
利用者の直感に反するので触り心地に大きく影響します。
- true/falseをとるパラメータは避けて、Switch Parameterを用いる
- Switch Parameterは、もし利用していれば
true、なければfalseとみなす - もし3値 (true, false, Unspecified) が必要な場合は、
Nullable<bool>が適切でしょう
Unspecifiedとnullを合わせるかは一考の余地があります。
- 可能であれば、パラメータに配列を許容しましょう
例えばGet-ProcessはNameにString配列を許容します。
利用者の使い心地として、複数回Cmdletを実行するより、1回で済む方がうれしいことは多いでしょう。
PassThruパラメータのサポートを検討しましょう
Stop-Processのような値を返さないCmdlet (Void型) であっても、時に結果オブジェクトが必要です。
こういった場合に、PassThruパラメータを与えることで、結果オブジェクトを返すオプションを提供しましょう。
Add, Set, NewといったVerbのCmdletはサポートしているものが多いです。
- ParameterSetのサポート
Cmdletは1つの目的のために作ります。
が、時に1つの操作を複数の表現で呼べることがあるでしょう。つまり、パラメータの組み合わせということです。
このパラメータの組み合わせの表現に、ParameterSetを用いることが多いです。
ParameterSetを用いる場合、DefaultParameterSetをCmdlet属性に指定しましょう。
(Design Guidelines) Provide Feedback to the User (SD04)
実行中ただ待つのは苦痛です。実行に対して何かしらのフィードバックを返しましょう。
- WriteWarning, WriteVerbose, WriteDebugメソッドのサポート
もし意図しない結果が起こった場合は、WriteWarningメソッドで結果をユーザーに伝えましょう
もしユーザーがさらなる詳細情報を求める場合、WriteVerboseで結果を返しましょう。例えば、実行シナリオが意図した状態になっているかを伝えることもいいでしょう
開発者がプロダクトサポートのために必要とする情報は、WriteDebugメソッドで返すといいでしょう
- 長時間実行時のWriteProgressサポート
長時間実行する場合、進捗をWriteProgressメソッドで表示するといいでしょう
- Host Interfaceを用いた対話実行
時にShouldProcess以外に、Hostを通してユーザーとやり取りをする必要に迫られます。そんなときにHostプロパティを用いましょう
たとえば、PromptForChoiceやWriteLine/ReadLineなどです
もしCmdletがGUIを生成しないなら、Out-GridView Cmdletの利用も検討できます
またCmdletは、Console APIは利用すべきじゃない。
- Cmdletヘルプファイルの生成
Help.xmlファイルで、Cmdletのヘルプを伝えられます。
(Design Guidelines) Advisory Development Guidelines
アドバイスとしてのガイドラインです。
Design GuidelinesとCoding Guidelinesがありますが、Design Guidelinesのみ触れます。
適用時は、Code Guidelineも参考にしてください。
(Design Guidelines) Support an InputObject Parameter (AD01)
- 特定の操作で良く用いられる名前が
InputObject - パイプラインからの入力をサポートしてプロセッシングするパラメータ名によく用いられ、.NET Frameworkのオブジェクトを取り扱う
(Design Guidelines) Support the Force Parameter (AD02)
Forceパラメータを用いたユーザーの権限処理や対話を操作できるようにするRemove-ItemCmdletの場合、通常はreadonlyファイルを消せません。しかしForceパラメータを用いることで消すことができる
もしユーザーがそもそもそのファイルにアクセスする権限がない場合、Forceをつけても何ら変わらず「失敗」します。
(Design Guidelines) Handle Credentials Through Windows PowerShell (AD03)
Credentialパラメータをサポートし魔装。このパラメータはPSCredential型を受け認証を処理することを期待する- このサポートにより、ユーザーに対して自動的にポップアップを表示し、ユーザー名やパスワード入力ができる
- Credentialパラメータには、Credential属性をあてる
Support Encoding Parameters (AD04)
- テキストやバイナリを扱うときは、
Encodingパラメータをサポートする
Test Cmdlets Should Return a Boolean (AD05)
Test-とつくCmdletはBooleanを返すことが期待する
PowerShell のデザインガイド
実は、コーディングスタイルに含まれてしまっている部分が強いので、APIデザインとしては存在しません。
ただし、Best Practiceが存在します。
https://github.com/PoshCode/PowerShellPracticeAndStyle/blob/master/Best-Practices/Introduction.md
一度目を通してみると面白いのではないでしょうか?
- Naming Conventions
- Building Reusable Tools
- Output and Formatting
- Error Handling
- Performance
- Security
- Language, Interop and .Net
- Metadata, Versioning, and Packaging
ざくっと上げます。PUREとあるものは、議論の余地があるため記載しません。
Building Reusable Tools
再利用性に注目しています。
TOOL-01 Decide whether you're coding a 'tool' or a 'controller' script
- 自分がツールを作ろうとしているのか、ツールの操作を作ろうとしているのか意識しましょう
なにかをするためのツールとして書かれている場合、re-usableでしょう。 ツールをビジネスロジックに合わせて「操作」するために書かれている場合、re-usableではないと考えられます。
TOOL-02 Make your code modular
- 処理を、関数にすることで、re-usableになる
TOOL-03 Make tools as re-usable as possible
- 入力をパラメータで受け取り、パイプラインに出力する
- この仕組みはre-usableさが最大限高まる
TOOL-04 Use PowerShell standard cmdlet naming
- PowerShellの標準のネーミングをしましょう
Verb-Noun大事。Get-VerbCmdletで標準のVerb一覧が見られる
TOOL-05 Use PowerShell standard parameter naming
- 標準のパラメータ名を用いましょう
TOOL-06 Tools should output raw data
- ツールの場合、Cmdletの処理中に、データをなるべく触らず生で出力することをコミュニティとしては期待することが多い
- もし出力データを操作する場合でも、最小限にとどめましょう。そうすることで、多くのシーンでre-usableになる
TOOL-07 Controllers should typically output formatted data
- 操作する場合、re-usableは主眼ではないので適切にわかるデータへフォーマットして返す
WAST-01 Don't re-invent the wheel
- 車輪の再発明だめ
下の例は、Test-Connection $computername -Quietで表現できます。
function Ping-Computer ($computername) { $ping = Get-WmiObject Win32_PingStatus -filter "Address='$computername'" if ($ping.StatusCode -eq 0) { return $true } else { return $false } }
WAST-02 Report bugs to Microsoft
- バグは共有しよう
Output and Formatting
出力に関してです。
Don't use Write-Host unless you really mean it
- Write-Hostだめ。Hostにしか出力しないので、「見せるためだけ」「フォーマットするだけ」に利用しましょう
- 特に
ShowVerbを使っていたり、FormatVerbを使っている関数を書いた時にしか、使わないぐらいがいい - なるべく他の
Write-*Cmdletの利用を検討してください
Use Write-Progress to give progress information to someone running your script
- ユーザーに何かしら進捗を示すとき
Write-Progressが最適 - ただし、パイプライン上のなんでも流せばいいというものではありません。伝えたいことにしぼる
Use Write-Debug to give information to someone maintaining your script
- スクリプトのメンテナンスをする人に向けて、
Write-Debugでメッセージを送る $DebugPreference = "Continue"とすることで、Breakpointで止まらず結果をみることもできる
Use CmdletBinding if you are using output streams
[CmdletBinding()]を使うだけで、出力ストリームを操作する-Verboseなどが利用できる
Use Format Files for your custom objects
- カスタム型を使う場合は、
modulename.format.ps1xmlを使ってフォーマットを検討してください
Only output one "kind" of thing at a time
- 1つの関数で、複数の型を返すことを避けてください
[OutputType()]で伝える型とのずれが生じるのは相当なコストをユーザーに強いる
Two important exceptions to the single-type rule
- もし内部関数の場合は、複数の型を返すのはあり
$user, $group, $org = Get-UserGroupOrgのように分けて受け取られる
- もし複数の型を返す場合、個別に
Out-Defaultに包んで返すことでフォーマットが混在することを避けられる
Error Handling
エラー処理です。
ERR-01 Use -ErrorAction Stop when calling cmdlets
- Cmdletの呼び出し時は、
-ErrorAction Stopをつけてエラー時に捕まえましょう
ERR-02 Use $ErrorActionPreference='Stop' or 'Continue' when calling non-cmdlets
- Cmdletではない場合、呼び出し前に
$ErrorActionPreference='Stop'を実行し、呼び出し後に$ErrorActionPreference='Continue'へ戻す - 特に自動化時に適切にエラーで止めることは重要
ERR-03 Avoid using flags to handle errors
- フラグで失敗制御はしない
try { $continue = $true Do-Something -ErrorAction Stop } catch { $continue = $false } if ($continue) { Do-This Set-That Get-Those }
- try, catchで制御しましょう
try { Do-Something -ErrorAction Stop Do-This Set-That Get-Those } catch { Handle-Error }
ERR-04 Avoid using $?
$?の利用は避けましょう
$?はエラーが前回のコマンドで発生したか示すものではなく、前回のコマンドが成功したかみるだけです。この結果に関しては、ほぼ意味がないでしょう
ERR-05 Avoid testing for a null variable as an error condition
- nullチェックを全部いれるとかやめましょう
ERR-06 Copy $Error[0] to your own variable
- 直前のエラーが
$Error[0]に収められているcatch句の$_も同様 - ただ、次のエラーですぐに上書きされるので必要なら変数にいれてください
$Error配列に過去のものは入っています。
Performance
PERF-01 If performance matters, test it
- PowerShellのパフォーマンスは、妙なくせだらけ
- パフォーマンスかな、とおもったらテストする
- たとえば、以下の例なら2つ目が早い
[void]Do-Something Do-Something | Out-Null
- いくつか方法が思いつく場合、計測しましょう
PERF-02 Consider trade-offs between performance and readability
- パフォーマンスと読みやすさはトレードオフな場合があることを考慮する
- 例えば、式で表現とパイプラインで表現でも変わる
$content = Get-Content file.txt ForEach ($line in $content) { Do-Something -input $line }
Get-Content file.txt | ForEach-Object -Process { Do-Something -input $\_ }
あるいは、.NET Frameworkを直接触ることでも変わります。
$sr = New-Object -Type System.IO.StreamReader -Arg file.txt while ($sr.Peek() -ge 0) { $line = $sr.ReadLine() Do-Something -input $line }
さらにこんな書き方もあるでしょう。
$handle = Open-TextFile file.txt while (-not Test-TextFile -handle $handle) { Do-Something -input (Read-TextFile -handle $handle) }
- どれがいいかといえば、なるべくPowerShellに沿った書き方が読みやすいでしょう。が、基本的には .NET Frameworkのラッパーにすぎない
- いくつものパターンがある中から、パフォーマンスとご自身の美学に沿って選択する
Security
Always use PSCredential for credentials/passwords
- Credentialやパスワードには、
PSCredentailを使う - SecureStringでパスワードが保持されるため、基本的にこれを使う
param ( [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credentials )
- どうしても生パスワードを拾う必要がある場合、メソッドから取得しましょう。なるべくさけてください
$Credentials.GetNetworkCredential().Password
Other Secure Strings
- 他にも、
Read-Host -AsSecureStringでSecureStringを受け取られる - 万が一SecureStringをStringにする必要があるなら、
ZeroFreeBSTREを用いてメモリリークを抑える
# Decrypt a secure string. $BSTR = [System.Runtime.InteropServices.marshal]::SecureStringToBSTR($this); $plaintext = [System.Runtime.InteropServices.marshal]::PtrToStringAuto($BSTR); [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR); return $plaintext
- もしディスクに認証を保持する必要がある場合、
Export-CliXmlを使ってパスワードを守ってください
# Save a credential to disk Get-Credential | Export-CliXml -Path c:\creds\credential.xml # Import the previously saved credential $Credential = Import-CliXml -Path c:\creds\credential.xml
- さらにもし、Stringがセンシティブでディスクに保持する必要がある場合は、
ConvertFrom-SecureStringで暗号化する
ConvertTo-SecureStringで戻すことができます。
Windows Data Protection API (DPAPI)をつかっているため、同一Windowsマシンの同一ユーザーでのみDecryptできるので注意です。処理として、AES共通鍵での暗号化もサポートしています。
# Prompt for a Secure String (in automation, just accept it as a parameter) $Secure = Read-Host -Prompt "Enter the Secure String" -AsSecureString # Encrypt to Standard String and store on disk ConvertFrom-SecureString -SecureString $Secure | Out-File -Path "${Env:AppData}\Sec.bin" # Read the Standard String from disk and convert to a SecureString $Secure = Get-Content -Path "${Env:AppData}\Sec.bin" | ConvertTo-SecureString
Language, Interop and .Net
VER-01 Write for the lowest version of PowerShell that you can
- サポートする、もっとも低いPowerShellバージョンのために書く
ただし、新しいほどパフォーマンスメリットがあります。たとえば、PoweShell v3では2番目の書き方のほうが早いです。
Get-Service | Where-Object -FilterScript { $\_.Status -eq 'Running' }
Get-Service | Where Status -eq Running
VER-02 Document the version of PowerShell the script was written for
#requires -version 3.0といった形でサポートしているバージョンを明記する- Moduleの場合、
PowerShellVersion = '3.0'とマニフェストのpsd1に設定することで表明できる
余談 : 個人的に注意していること
私が特に多くの人から苦しいと耳にすることで、個人的に気を付けているものは次のものです。だいたい記事にしていたので参考にしていただけると幸いです。
- [Object]型デフォルトに起因する型をつかった操作が影響受けやすいこと
- $nullの扱い
- パイプラインを通したときの実行速度と式の違い
- 型の明示をしない場合の暗黙の型変換 (左辺合わせ)
- 単一要素配列が返却時に自動的なアンラップがかかる
- より安全に書くためにはStrictModeの利用がいいでしょう
まとめ
PowerShell Scriptで書く場合も、C# で書く場合と同じように気を付ければ問題なさそうです。
特に、パラメータ入力、パイプラインが最も入り組んでいる印象が強いです。独自の構文$?はコンソールでの入力以外は使わないんですよねぇ。実際、私はほぼ使わないです。
PowerShellも .NETに限らず、一般的なプログラミング言語のやり方が生きます。言語自体の構文サポートの弱さやなど癖がありますが、ゆるく付き合うといいでしょう。