tech.guitarrapc.cóm

Technical updates

PowerShell のAPIデザインガイドライン

この記事は、PowerShell Advent Calendar 2017 5日目の記事です。

qiita.com

昨日は @atworks さんの PSRemoting を用いたリモートプロセス実行でした。 qiita.com

3日目の前回 PowerShell のコーディングスタイルについて触れました。

tech.guitarrapc.com

次は、残りのAPI デザインを見てみましょう。

※ 2回に分けて書きましたが個人的には個人/チームが書きやすいようにすればいい話だと思っています。しかし、「曖昧だった基本的な指針がわからず困ってた方」にとって、ベストプラクティスはいい材料となると思います。PowerShell コミュニティは結構活発なので、コミュニティの中で皆様がよい PowerShell 生活を送られることを祈っています。

目次

C#のデザインガイド

Required Development Guidelines

https://msdn.microsoft.com/en-us/library/dd878238.aspx

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

次は強く要請するガイドラインです。

https://msdn.microsoft.com/en-us/library/dd878270.aspx

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 のような複数名を用いる場合は、そのbパラメータがいつでも複数要素を受け入れる場合にしましょう
  • パラメータはPascalCaseで。C# であれば Property を用いるので、C# デザインガイドと同じで違和感はないかと思います
    • errorAction や erroraction より、ErrorAction が好ましいです
  • パラメータの組み合わせで操作が変わる Cmdlet を提供する場合2つの方法があります
    • enum を用いて、enumごとに操作を分岐し パラメータを処理する方法
    • ValudateSet属性.aspx) を用いて、パラメータの入力を制約する方法
    • 私の経験上、複数の操作を提供する Cmdlet は作りたくなります。パラメータ1つだけが特定のパラメータ時に用いないようなの「単純な組み合わせ」であれば、ValidateSet が楽でしょう。が、複数の パラメータの組み合わせを ParameterSet で表現するのはおススメしません。Cmdlet を分離することを検討するといいでしょう
  • パラメータ名にはStandard Type を用いましょう
    • あらかじめ、どんな用途(Activity) にどんなパラメータが期待されるのかリストされています
    • 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 を用いる場合、DefaultParameterSetCmdlet 属性に指定しましょう

(Design Guidelines) Provide Feedback to the User (SD04)

ユーザーは実行中ただ待つのは苦痛です。実行に対して何かしらのフィードバックを返しましょう。

  • WriteWarning, WriteVerbose, WriteDebug メソッドのサポート

    • もし意図しない結果が起こった場合は、WriteWarning メソッドで結果をユーザーに伝えましょう
    • もしユーザーがさらなる詳細情報を求める場合、WriteVerbose で結果を返しましょう。例えば、実行シナリオが意図した状態になっているかを伝えることもいいでしょう
    • 開発者がプロダクトサポートのために必要とする情報は、WriteDebug メソッドで返すといいでしょう
  • 長時間実行時の WriteProgress サポート

    • 長時間実行する場合、進捗を WriteProgress メソッドで表示するといいでしょう
  • Host Interface を用いた対話実行

    • 時に ShouldProcess 以外に、Host を通してユーザーとやり取りをする必要に迫られます。そんなときに Host プロパティを用いましょう
    • たとえば、PromptForChoiceWriteLine/ReadLine などです
    • もしCmdlet が GUI を生成しないなら、Out-GridView Cmdlet の利用も検討できます
    • また Cmdlet は、Console API は利用すべきではありません
  • Cmdlet ヘルプファイルの生成

    • Help.xml ファイルで、Cmdlet のヘルプを伝えることができます

(Design Guidelines) Advisory Development Guidelines

アドバイスとしてのガイドラインです。

https://msdn.microsoft.com/en-us/library/dd878291.aspx

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-Item Cmdlet の場合、通常は 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 が存在します。

github.com

一度目を通してみると面白いのではないでしょうか?

  • 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-Verb Cmdlet で標準の 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 にしか出力しないので、「見せるためだけ」「フォーマットするだけ」に利用しましょう
  • 特に Show Verb を使っていたり、Format Verb を使っている関数を書いた時にしか、使わないぐらいがいいです
  • なるべく他の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 で制御しましょう
    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 を受け取ることgあできます
  • 万が一 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]型デフォルトに起因する型をつかった操作が影響受けやすいこと

tech.guitarrapc.com

  • $null の扱い

tech.guitarrapc.com

winscript.jp

  • パイプラインを通したときの実行速度と式の違い

tech.guitarrapc.com

tech.guitarrapc.com

  • 型の明示をしない場合の暗黙の型変換 (左辺合わせ)

tech.guitarrapc.com

  • 単一要素配列が返却時に自動的なアンラップがかかる

tech.guitarrapc.com

  • より安全に書くためには StrictMode の利用がいいでしょう

blog.shibata.tech

まとめ

PowerShell Script で書く場合も、C# で書く場合と同じように気を付ければ問題なさそうです。

特に、パラメータ入力、パイプラインが最も入り組んでいる印象が強いです。独自の構文$? はコンソールでの入力以外は使わないんですよねぇ。実際、私はほぼ使わないです。

PowerShell も .NET に限らず、一般的なプログラミング言語のやり方が生きます。言語自体の構文サポートの弱さやなど癖がありますが、ゆるく付き合うといいでしょう。