tech.guitarrapc.cóm

Technical updates

PowerShellのコーディングスタイル

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

https://qiita.com/advent-calendar/2017/powershell

新しい言語を触るとき、その言語はどのように書くことを意図しているのか気になります。私が触ってきた言語の多くは「その言語の考えの基本」となるものを持っており、コーディングガイドライン上でもそれを明示していることが多いようです

PowerShellはどうなのでしょうか? 今回はPowerShellではどのようなコーディングスタイルで書くといいのかを考えてみます。

※ 本記事ではコーディング規則も同じ意味で用います

思いのほか記事内容がながくなってしまったので、結論だけ見たい方はまとめをどうぞ

※ 決してこの記事の内容が絶対正しいと思っていません。みなさんが書いていく中でどうすればいいのか、と思ったときの良いヒントになることを願っています。

軽い記事を書くつもりが大きくなりすぎたので、コーディングスタイルは主語が大きすぎということを感じました。補完しあう内容となるAPIデザインも5日目に書きました。

http://tech.guitarrapc.com/entry/2017/12/06/000000

コーディングスタイル

ここでは言語を設計した側が意図している、あるいはコミュニティの中で共通認識として持っているものを指して考えたいです。

各開発チームごとにルールは別に設けられることもあるので、そこには触れません。言語として意図している、言語的にこうするといいとコミュニティの合意が取れている例です。優しい世界の優しいやり方に努めたいです。

PowerShellの前に他の言語の例から見てみましょう。いろんな言語で、それぞれの考えがあって素敵です。

他言語の例: C#

Microsoftが出しているコーディングガイドラインがあります。そこには次のようにコーディング規則の意義を説明しています。

C# 言語仕様では、コーディング標準は定義されていません。 ただし、このトピックのガイドラインは、サンプルおよびドキュメントを開発するためにマイクロソフトによって使用されます。
コーディング規則には、次の目的があります。
コードの見た目が統一されるため、コードを読むときに、レイアウトではなく内容に重点を置くことができます。
これにより、経験に基づいて推測することで、コードをより迅速に理解できます。
コードのコピー、変更、および保守が容易になります。
コーディング規約により、C# のベスト プラクティスがわかります。

C# のコーディング規則 (C# プログラミング ガイド) | Microsoft Docs*1

合わせて .NET FrameworkのAPIデザインにおける名前付けのガイドラインもあります。

Framework デザイン ガイドライン

名前付けのガイドライン*2

.NET Framework 、特にC# を書き始める、どんなガイドラインで書けばいいか困ったなぁという時には.NETのクラスライブラリ設計が非常に良い内容だと思っています。今は書籍が絶版でKindle版のみです。内容はAPIデザインガイドという意味で、他言語を扱うときにも参考になっていると感じます。

.NETのクラスライブラリ設計

C# のインデントスタイルは、 Allman Styleで紹介されています。

他言語の例 : Swift

Swiftにも、API Design Guidelinesとしてガイドラインがあります。

https://swift.org/documentation/api-design-guidelines/

SwiftがGitHub上でオープンに開発される中で、いくつかのProposalは検討の末リジェクトされていますが、それら理由にはガイドラインからの引用も多く見られます。徹底されているの素晴らしいです、ね。

apple/swift-evolution | GitHub

https://qiita.com/ezura/items/0db0f05add6a8d115f30

特に、「明瞭さは簡潔さよりも重要です」としたガイドラインは、Swiftを書く上で自然と意識します。コミュニティの発信する情報も、多くがガイドラインに沿う努力がなされています。

Clarity is more important than brevity. Although Swift code can be compact, it is a non-goal to enable the smallest possible code with the fewest characters. Brevity in Swift code, where it occurs, is a side-effect of the strong type system and features that naturally reduce boilerplate.

Swiftのインデントスタイルは、 K&R 1TBS (OTBS) で紹介されています。このあたりの記事でもその辺が紹介されていますね。

Swift bracing

PowerShell のコーディングスタイル

さて、他言語の例をみたところで、PowerShellにも同様のガイドラインはあるのでしょうか? 答えは「あります」。ただ、PowerShellはCmdlet(C#) とScript(PowerShell) として記述ができるため長くなってしまいます。

※ これは語弊があり、実際は.NET Frameworkの言語であればF#で書くこともできます。簡単のためこの前提でいきます。

そこで本記事においては、C# でBinary Cmdletを記述する場合、PowerShellとしてFuncion を記述する場合の2面から見てみましょう。

PowerShell Cmdlet を C# で書く場合のコーディングスタイル

まずは、PowerShellのCmdlet = C# で記述するものとして見てみましょう。このガイドラインは、MSDNにてMicrosoftから公開されています。

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

中身は、3つに分かれています。

  • Required Development Guidelines : 必須とするガイドライン
  • Strongly Encouraged Development Guidelines : 守ることを強く要請するガイドライン
  • Advisory Development Guidelines : アドバイスとしてのガイドライン

順にみていきましょう。

Required Development Guidelines

必須とするガイドラインです。

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

Design GuidelinesとCoding Guidelinesがありますが、Coding Guidelinesのみ触れます。

(Coding Guidelines) Derive from the Cmdlet or PSCmdlet Classes (RC01)

  • PowerShell CmdletをC# で書くときにCmdlet型かPSCmdlet型を継承できる
    • Cmdletの .NET Frameworkからの呼び出しの自由さよりも、PSCmdletのRunSpace内での実行を推奨している
    • ここは若干微妙で、テストしやすさやWindows PowerShell RuntimeとCore Runtimeのずれがあるので今後の変更もありえる
  • またCmdletはpublicクラスである必要がある

(Coding Guidelines) Specify the Cmdlet Attribute (RC02)

  • Cmdletには属性でどのように使うかを表明する
  • verb-and-noun表明により、公開するクラス名とCmdlet命名規則のずれをカバーできる
  • デフォルトパラメータセットの表明やShouldProcessの表明もここで可能

C# はクラスをPascalCaseとしていて、PowerShellはVerb-Nounですからね。

(Coding Guidelines) Override an Input Processing Method (RC03)

  • PowerShell Cmdletは入力を3つのプロセスBegin, Process, Endを通す
  • Cmdlet.BeginProcessing
    • This method is called one time, and it is used to provide pre-processing functionality
  • Cmdlet.ProcessRecord
    • This method is called multiple times, and it is used to provide record-by-record functionality
  • Cmdlet.EndProcessing
    • This method is called one time, and it is used to provide post-processing functionality

(Coding Guidelines) Specify the OutputType Attribute (RC04)

  • OutputType属性を用いるとよい
  • この属性を公開することで、パイプラインの先に型が伝搬する
  • ただ、PowerShellのOutputは関数の返戻値型を強制しないため、ずれることがあるので注意

伝搬しないと? Objectになって型が伝わらず、インテリセンスが絶望的になります。

(Coding Guidelines) Do Not Retain Handles to Output Objects (RC05)

  • Cmdletは、WriteObjectメソッドで出力をパイプラインに渡すのですが、メソッドはオブジェクトを内部で保持しないようにする

ふつうにリークしたり予期しないエラーの原因になります。

(Coding Guidelines) Handle Errors Robustly (RC06)

エラーハンドルです。

  • 処理がこれ以上続行できない場合は、ThrowTerminatingErrorメソッドでエラーを返す

もし例外がCmdletで取得できず漏れてしまった場合、PowerShellランタイムがターミネートされます。しかもエラー情報であるErrorRecordオブジェクトの情報が欠落するので意味不明な状況になって使い心地が最悪になります。

  • もし処理ターミネートされるエラー出ない場合、ErrorRecord をWriteErrorメソッドに渡してハンドルする
  • 例外の表明に使うErrorRecordは、.NET GGrameworkのデザインガイドラインに沿って、すでにあるExceptionで表明できるならそれを使う。もし新規に作る必要があるなら、Exception型を継承する定石に乗ることを推奨

Errorrecordは、エラーをグルーピングするErrorCategoryの提供してください

  • Cmdletがスレッドを生成して、そのスレッドでエラーを起こした場合、PowerShellは例外をキャッチせずPowerShellプロセスをターミネートする

PowerShellでマルチスレッド処理を書くのが苦しい原因と言えます。

  • もしデストラクタでUnhandled Exceptionが生じた場合は、PowerShellは例外をキャッチせずPowerShellプロセスをターミネートする

DisposeでUnhandled Exceptionを起こした時も同様です。

(Coding Guidelines) Use a Windows PowerShell Module to Deploy your Cmdlets (RC07)

SnapInとか忘れましょう。私自身Moduleしか書きません。 実際PowerShell Getでのモジュール提供がインフラとして浸透した今、Module以外で提供するのはエコシステムからも外れているのでつらいでしょう

Strongly Encouraged Development Guidelines

ガイドラインは、守ることを強く要請します。

これもDesign GuidelinesとCoding Guidelinesがありますが、Coding Guidelinesのみ触れます。

(Coding Guidelines) Coding Parameters (SC01)

Parameterと読んでいるものは、public Property[Parameter()]属性をつけたものが該当します。パラメータは、Static Memberである必要がありません。

詳細は、Parameter Attribute Declarationで説明されています。

Support Windows PowerShell Paths

  • PSPathのようなNormalizeされたものを示している
  • PathはString型で表明する
  • Pathは、AliasとしてPSPathを与えてください。[Alias("PSPath")など
  • またパスはワイルドカード(*) をサポートする

もしワイルドカードをサポートしない場合、LiteralPathパラメータとして用意しましょう。個人的にはパスはパスワイルドカードのハンドルは、パス名に[などのもじが入っていた時に崩れやすいため、LiteralPathのほうが意図した挙動にはしやすいです。パスのほうが扱いやすいのも事実なので、提供側がどんな利用をするか手触りで選べばいいでしょう。

Support Wildcard Characters

  • もし可能であれば、パラメータはワイルドカード入力をサポートするとい
  • Process名を探すときにGet-Process git*などのように、ワイルドカードサポートは利便性を大きく向上する
  • ワイルドカード入力をサポートしても、出力は複数になるかは一致するとは限りません。適切に扱う

例えば、Set-Locationはパスにワイルドカードを受け付けますが、移動は一度だけです。

Define ObjectMembers to Be Used as Parameters

  • もしCmdletのためのオブジェクトを用意する場合、そのオブジェクトをParameterとして受け入れられるようにすることを考える
  • また、独自の型で出力する場合は、PowerShellがユーザーに表示する際やパイプラインでメンバー渡すのに向いていないことがある

この場合は、custom Types.ps1xmlを作成して用意するといいでしょう。その場合の名前は、<Your_Product_Name>.Types.ps1xmlが推奨されます。 例えば、FileInfoを表現するときにModeというScriptPropertyを用意してわかりやすくするなどが考えられます。 ってありますが、つくらないですねぇ。私は。Cmdletで用意するにしても、.NETで表現できた方が好きなので。Lengthの代わりにCountとかもあんまり好きじゃなかったりします。

Implement the IComparable Interface

わたしは作りません。いるケースをかかないっていうのもありますがPowerShellでのデータ処理を複雑に行うことはないです。

Update Display Information

  • <YourProductName>.Format.ps1xmを用いることで、表示を望む形に定義できる

私は書かないです。

(Coding Guidelines) Support Well Defined Pipeline Input (SC02)

Implement for the Middle of a Pipeline

  • Pipelineの途中でも動くように書くとパイプラインフレンドリー

たとえばGet-Processの結果を受けるように書くときなどです。 個人的にも、パイプラインは都度ストリーム処理なので速度という意味ではパフォーマンスは不利ですが、メモリ効率性は高くPowerShellっぽくパイプラインでつないでさくっとデータ処理を書けるという意味では「書くのが楽」なスタイルなのでおススメです

Support Input from the Pipeline

  • パイプラインを受けらるには、ParameterにValueFromPipelineValueFromPipelineByPropertyName属性を足す

Support the ProcessRecord Method

  • ストリームを都度処理することがパイプラインの特徴

ProcessRecordメソッドをオーバーライドすることで、ストリームからの都度入力を操作できます。

(Coding Guidelines) Write Single Records to the Pipeline (SC03)

  • Pipelineはストリーム処理をすることに良さがるので、出力を都度シングルレコードでWriteObjectメソッドを書き出す
  • バッファリングして出力をためたりするのは、一度立ち止まって考えてからのほうがいい

もしバッチ処理的に、出力をまとめた吐く場合は、System.Managemet.Automation.Cmdlet.WriteObject(System.Object,System.Boolean)の引数にtrueを渡しましょう。 私は、原則パイプラインでの処理を速やかに後続に渡すように心がけています。

(Coding Guidelines) Make Cmdlets Case-Insensitive and Case-Preserving (SC04)

  • Windows PowerShell自体がcase-insensitiveなため、処理もそれに従う

Advisory Development Guidelines

ガイドラインはアドバイスという立ち位置です。 Design GuidelinesとCoding Guidelinesがありますが、Coding Guidelinesのみ触れます。

(Coding Guidelines) Follow Cmdlet Class Naming Conventions (AC01)

Define a Cmdlet in the Correct Namespace

  • 名前空間におく
  • 名前空間は、.Commandsをつけるとよい

Name the Cmdlet Class to Match the Cmdlet Name

  • Cmdletのクラス名は、<Verb><Noun><Command>と命名する

たとえば、Get-Process Cmdletなら、クラス名は、GetProcessCommandとなります。

(Coding Guidelines) If No Pipeline Input Override the BeginProcessing Method (AC02)

  • もしパイプラインからの入力を受け付けない 場合、BeginProcessingメソッドをオーバーライドして処理を書く
  • パイプラインで動く場合、初めのCmdletが実行されたときに、他のパイプラインの処理が動く前に呼び出される

このbeginの動きが、一番混乱を生みやすいです。

(Coding Guidelines) To Handle Stop Requests Override the StopProcessing Method (AC03)

  • StopProcessingメソッドを可能であればオーバーライドしてください。これによって、Stop信号を制御できるようになる

たとえば、長時間実行するようなCmdletの場合なんですが、これまともに機能している例をあんまりみないんですよね。Jobなどの仕組みなのですが。

(Coding Guidelines) Implement the IDisposable Interface (AC04)

  • PowerShellは、EndProcessingを必ずしも呼ばないため、Disposeが必要なリソースを用いる場合は、IDisposableを実装して明示的にDisposeを呼び出す

例えばパイプラインの途中でエラーが出たときとかですね これ構文的にはダサいし漏れるんですよね。関数を書いていてつらい部分です。

(Coding Guidelines) Use Serialization-friendly Parameter Types (AC05)

  • もしリモート先で動作するようなCmdletを書く場合、Seriablizeしやすくする

以下がフレンドリーな型です。

Primitiveな型
- Byte, SByte, Decimal, Single, Double, Int16, Int32, Int64, Uint16, UInt32, and UInt64
- Boolean, Guid, Byte[], TimeSpan, DateTime, Uri, and Version
- Char, String, XmlDocument

Built-in型
- PSPrimitiveDictionary
- SwitchParmeter
- PSListModifier
- PSCredential
- IPAddress, MailAddress
- CultureInfo
- X509Certificate2, X500DistinguishedName
- DirectorySecurity, FileSecurity, RegistrySecurity

他いくつか
- SecureString
- Containers (lists and dictionaries of the above type)

(Coding Guidelines) Use SecureString for Sensitive Data (AC06)

  • センシティブなデータは、SecureString使いましょう

Naming Conventions

  • メソッドの名前付けに関して、PowerShell自体の原則であるVerb-Nounに合わせてLoadModuleなどのような-抜きの名前付けを推奨
  • private fieldに関してInstance FiledのPrefixとして、_、Static Fieldはs_ (加えてreadonlyを用いる場合static readonlyという順番)、Thread Static Fieldはt_という命名を示す

Prefixは、C#の一般的な名前付けの指針とズレています。https://msdn.microsoft.com/ja-jp/library/ms229012.aspx

- > X DO NOT use a prefix for field names. For example, do not use "g_" or "s_" to indicate static fields.

Layout Conventions

C# 同様のAllman Styleであることが見て取れます。

  • Avoid more than one blank empty line at any time
  • Avoid trailing spaces at the end of a line
  • Braces usually go on their own lines, with the exception of single line statements that are properly indented

Performance Considerations

LINQのゴミをさけること、for/foreachの選択に関して触れています。

  • Avoid LINQ - it can create lots of avoidable garbage. Instead, iterate through a collection directly using for or foreach loop
  • Between for and foreach, for is slightly preferred when you're uncertain if foreach allocates an iterator

他にも、メモリアロケーションを避けるためのUtilとして定義されたstatic fieldの利用やboxingの回避のためのGenericsの利用など細かな内容が続きます。

Best Practices

この中で興味深いのは、C# の新しいシンタックスの利用は推奨されていることです。ただし既存のコードに適用する場合は、別のPRにすることを言っています。コード公開時のコードはずいぶんと古い書き方に感じたことを覚えています。

Use of new C# language syntax is encouraged. But avoid refactoring any existing code using new language syntax when submitting a PR as it obscures the functional changes of the PR. A separate PR should be submitted for such refactoring without any functional changes.


PowerShell Scipt で書く場合のコーディングスタイル

PowerShell Teamからのものと、コミュニティからのものが公開されています。

また、別にPowerShell DSCのコーディングスタイルとしてHighQualityModuleGuidelinesが公開されています。これはModule特にDSCとしてに注視しているため、ここでは扱いません。

PowerShell scripting best practices

PowerShell Team Blogの内容です。

  1. ドキュメントコメントを用いて、name, date, author, purpose and keywordsを記述する
  2. 可能な限りコメントを書く。ただし行き過ぎないレベルで
  3. #Requiresタグを用いて、サポートするPowerShellミニマムバージョンを明示することで、後からわかりやすくする
  4. Set-StrictMode -Version Latestコマンドを使って、初期化されていない変数アクセス、存在しないプロパティやオブジェクトアクセスを防ぐ
  5. シンプルだが意味のある変数命名をしましょう。camelCaseがベストプラクティス ($serviceNameなど)

camelCaseはローカル変数のみ指します、Parameterは指しません。

  1. スクリプトの上部に、ユーザー定義変数を記述する
  2. 可能であれば、CodeSigningを使う (RemoteSigned / AllSignedをExecetion Policyに使います)
  3. Aliasは使わないように。フルCmdlet名、かつパラメータを明示する
  4. backticksは使用をさけ、パイプライン| やSplattingを活用する
  5. Filter left, format rightにする

.exe拡張子を外部コマンド呼び出し時につけます。scではなくsc.exeという感じ。

  1. $ErrorActionPreference = "SilentlyContinue"でパイプラインのエラーを消さないように。try. catch, finallytrapで構造的に制御
  2. PowerShell 挙動制御変数を使って、挙動を変えないようにする

$ConfirmPreferenceなどが該当します。

  1. CmdletBindingを用いて、-WhatIf, -Verbose, -Debugをサポートする
  2. Advanced Functionsをサポートする
  3. Verb-Noun形式の命名にし、標準に用意されたVerb(動詞)を使う
  4. パラメータは汎用的な命名にし、必要なら初期値を入れる

例えば、ServerよりComputerNameが望ましいです。

  1. 1 Function = 1動作にし、1動作をよく取り扱う

Linuxと同様ですね。

  1. 関数は、常にオブジェクトを返して、文字列を返すことは避ける
  2. returnキーワードの利用は避ける。パイプラインの中で自動的に返される

この挙動は結構悩ましいのお気をつけて

  1. Write-OutputWrite-Hostより望ましい

Write-Hostはホストに書き出しますが、パイプラインに書かないため

  1. Comment Base helpを用いる
  2. 接続を確認するときは、Test-Connection-Quietスイッチで使う
  3. スクリプトは、書式をきれいにそろえる

といいつつ、ここで書式の明示はないという。

  1. どこから実行しても動くようにする

$myInvocation.MyCommand.Path$PSScriptRootがPowerShell 3.0以降で利用できます。

  1. 関数はテストしましょう。昇格やバージョン、存在有無のテストを含む
  2. Productionリリース前にテスト環境でテストする
  3. ModuleやPowerShellGetを使って再利用する
  4. Scriptを書く、使う、レビューするパイプラインは用意する

パイプラインはこんな構成がシンプルです。

`#Requires`を書く
パラメータを定義
関数を書きます
変数を定義します
コードを実行します
コメントベースヘルプを用意します

PowerShellPracticeAndStyle

コミュニティの共通認識になっている、Style Guideについて触れます。これはPythonコミュニティを見習ってできた流れです。

The PowerShell Style Guide

いかについて触れられています。

  • Code Layout and Formatting
  • Function Structure
  • Documentation and Comments
  • Readability
  • Naming Conventions

(Code Layout & Formatting) Maintain consistency in layout

このガイドラインを強制するつもりがなく、一貫性を重視しています。1行の長さや行の数など、個人の主張があればどうぞ。という立場が貫かれています。

(Code Layout and Formatting) Capitalization Conventions

次の用語の意味で読み進めてください。

  • lowercase - 全部小文字、単語分離なし
  • UPPERCASE - 全部大文字、単語分離なし
  • PascalCase - 各単語の初めの一文字だけ大文字
  • camelCase - 最初の単語を除く各単語の初めの一文字だけ大文字

PoweShellは、Case-Insensitiveなので大文字、小文字を区別しません。しかし、PowerShellが .NET Frameworkでできていることから、次のルールに沿っています。

https://msdn.microsoft.com/en-us/library/ms229043

  • PowerShellは、publicなアクセスのものはすべてPascalCaseを使っている

PowerShellのパラメータも.NET ClassのプロパティなのでPascalCaseとします。

Function, Cmdlet名
Class
enum
Attribute(属性)名
パブリックフィールドやパブリックプロパティ
グローバル変数
グローバル定数
  • PowerShellのキーワードは、lowerCaseで表現する
foreachやdynamicparam
-eqなどのオペレータ
  • コメントベースヘルプはUPPERCASEで示す
.SYNOPSIS
.DESCRIPTION
他
  • Function(関数)名は、Verb-Noun形式にする。Verb/Nounは、PascalCaseとする
  • 特殊な2文字はキャピタルで示する

例えば$PSBoundParametersGet-PSDriveのPSがそうです。https://msdn.microsoft.com/en-us/library/ms229043#Anchor_1に沿ってください。

  • 関数やモジュール内の変数はcamelCaseにしてパラメータと区別する
  • 共有変数としての$Script:PSBoundParametersなどはスコープ名を付けて明示する
  • 特殊な2文字を含むローカル変数は、adComputerなどのように両方lowercase

(Code Layout & Formatting) Always Start With CmdletBinding

  • 関数は必ず[CmdletBinding()] param()で開始する

いわゆるAdvancedFunctionsということです。 begin/process/endブロックはオプショナルです。

  • Open Brace {は同じ行に書く (議論)
  • Close Brace }は、自分音表に書く (議論)

(Code Layout & Formatting) Indentation

  • 4スペース インデントを用いる(ソフトタブ)

これはPowerShell ISEの挙動に沿っています

  • 行を並べる時には、文字列を縦にそろえることを検討する

個人的に、アラインはIDEがしないならしないですが、DSCのような宣言型では有用に思える時があります

(Code Layout & Formatting) Maximum Line Length

  • 1行115文字以内に収めることを推奨する

PowerShellコンソールのデフォルトが120文字であることから示されています。 行を抑えるためにはSplattingが有用。

(Code Layout & Formatting) Blank lines

  • 関数やクラスは、2行あける
  • クラス内のメソッドは1行あける
  • 関連するワンライナーでは、空行は省略されることが多い
  • 関数の集合や処理をまとめるのに、控えめに空行を使うのもよい
  • パラメータに渡すときは、:よりも明示的に渡す
X: `$variable=Get-Content $FilePath -Wai:($ReadCount-gt0) -First($ReadCount*5)`
O: `$variable = Get-Content -Path $FilePath -Wait:($ReadCount -gt 0) -TotalCount ($ReadCount * 5)`

(Code Layout & Formatting) Spaces around special characters

  • カンマやセミコロン、{}の後に空白スペースをいれる
  • かっこや[]の中に余計な空白スペースが入らない
  • $( ... ){ ... }は、中の前後にスペースを1つ入れて読みやすくする

外にスペースは読みやすさのためいりません。

(Code Layout & Formatting) Avoid using semicolons (;) at the end of each line.

  • PoweShellはセミコロン;を使えますが必須ではない。編集やコピー&ペーストされることを考え、いらないならつけない
  • 同様にHashtableの定義時も;はつけない

(Function Structure) Functions

  • returnキーワードの利用は避ける
  • シンプルな関数を定義するときは、関数名とパラメータにスペースを空ける
function MyFunction ($param1, $param2)

(Function Structure) Advanced Functions

  • 命名は<verb-から始める
  • Noun (名詞) 部分は、1つ以上を結合した単語でPascalCase 、単数形
  • returnキーワードの利用は避ける
  • Process {}でのみ値を返し、Begin {}End {}では返さないようにする

これをしないと、パイプラインのメリットが損なわれます。returnを用いないのはこの意味で意味が通っています。

(Function Structure) Always use CmdletBinding attribute.

  • [CmdletBinding()]を必ず用いる

(Function Structure) Always have at least a process {} code block if any parameters takes values from the Pipeline.

  • Pipelineからの入力があるなら、process {}ブロックを用意する

(Function Structure) Specify an OutputType attribute if the advanced function returns an object or collection of objects.

  • 返戻値の型を[OutputType(T)]属性で明示する。日うように応じて、パラメータセットとともに明示する

私は、[OutputType([<TypeLiteral>])形式がインテリセンス補完効くので好みです。

[OutputType([<TypeLiteral>], ParameterSetName="<Name>")]
[OutputType("<TypeNameString>", ParameterSetName="<Name>")]

(Function Structure) When a ParameterSetName is used in any of the parameters, always provide a DefaultParameterSetName in the CmdletBinding attribute.

  • ParameterSetNameを用いる場合は、[CmdletBinding()]DefaultParameterSetNameを明示する

(Function Structure) When using advanced functions or scripts with CmdletBinding attribute avoid validating parameters in the body of the script when possible and use parameter validation attributes instead.

  • Script内部ではなく、パラメータの属性でバリデートを検討する

(Documentation and Comments) Comment

  • 常に更新する
  • 英語でコメント書く
  • 「何をしているか」ではなく「なぜ」を意識して書く

うまくPowerShellらしく書くと、それ自体が説明しているように表現できます。

(Documentation and Comments) Block comments

  • 一行ごとにコメントをつけたりはしない

コメントが長い場合は、<# ... #>で複数行のコメントを表現できます。

(Documentation and Comments) Inline comments

  • 気が散るためあまり好ましくない
  • が、もし短いコードに説明を要する状況ではやる価値がある
  • 2文字以上コードとスペースを挙げて表現する
  • また同じブロックの他のinline comment行と頭をそろえると読みやすい

(Documentation and Comments) Documentation comments

Comment-basedはクラスの説明ではなく「何をする関数なのか」を書きます。

  • 必要以上に強い言葉は避ける
  • 短い説明に徹する
  • だれかに印象付けるために書いているわけではなく、どう使えばいいかを伝えるために書く
  • 外国語を書く場合、とくにシンプルな言葉・文で書くと、よりネイティブには意味が伝わる
  • 必要十分を目指す

書く場所は、関数の上ではなく中に書きましょう

  • コメントの更新を意識づけるため、末尾より、頭がよい
  • NOTESに詳細を書く
  • Synopsisに概要を書く
  • パラメータにコメントを書き、可能なら.PARAMETERに書く

ただ、コード上に書いた方が良く更新される経験則もあります。

  • .EXAMPLEに利用例を示す

(Documentation and Comments) DOC-01 Write comment-based help

関数には毎度書きましょう。

function get-example {
    <#
    .SYNOPSIS
        A brief description of the function or script.

    .DESCRIPTION
        A longer description.

    .PARAMETER FirstParameter
        Description of each of the parameters
        Note:
        To make it easier to keep the comments synchronized with changes to the parameters,
        the preferred location for parameter documentation comments is not here,
        but within the param block, directly above each parameter.

    .PARAMETER SecondParameter
        Description of each of the parameters

    .INPUTS
        Description of objects that can be piped to the script

    .OUTPUTS
        Description of objects that are output by the script

    .EXAMPLE
        Example of how to run the script

    .LINK
        Links to further documentation

    .NOTES
        Detail on what the script does, if this is needed

    #>

(Readability) READ-01 Indent your code

  • インテンドは答えがありません。ただし、ScriptではなくPoweShellに解釈させたとき、結果が異なりえる

以下は、エラーが出ません

if ($this -gt $that) {
    Do-Something -with $that
}

以下はエラーがでます

if ($this -gt $that)
{
    Do-Something -with $that
}

構成要素をインテンドに収めるのがいいでしょう

ForEach ($computer in $computers) {
    Do-This
    Get-Those
}

(Readability) READ-02 Avoid backticks

  • Backticks`は避ける
  • 決してBackticksは嫌われていませんが、不便なことがあるので使う必要がないようにするとよい
  • コミュニティはBackticksを行継続として使っていると考えるが読みにくい、見落としやすい、ミスタイプしやすい傾向にある

Backticksの後ろにスペースを入れると動かなくなりまるのも困りものです。

Get-WmiObject -Class Win32_LogicalDisk `
              -Filter "DriveType=3" `
              -ComputerName SERVER2
  • 問題を引き起こしやすいため、テストやデバッグも難しくなる傾向にある
  • 代替としてこのようなSplattingを使うとよい
$GetWmiObjectParams = @{
    Class = "Win32_LogicalDisk"
    Filter = "DriveType=3"
    ComputerName = "SERVER2"
}
Get-WmiObject @GetWmiObjectParams

(Naming Conventions) Naming

  • 短縮した名前をコマンドやパラメータに用いるより、明示的な名前付けが好まれる
  • Expand-Aliasを使うことで、いくつかを修正できますが、問題をなくすことは難しい

(Naming Conventions) Use the full name of each command.

  • PowerShellを使う中でフル名を学びますが、Aliasは1つによって違う
  • 共有スクリプトの場合、汎用的に知られている名前がよい

Linuxユーザーにとってのlsは、DOSではdirです。GitHubハイライトがかかるボーナスもあります。

# Do not write:
gwmi -Class win32_service

# Instead write:
Get-WmiObject -Class Win32_Service

(Naming Conventions) Use full parameter names.

  • PowerShellには多くのコマンドがあるため、全コマンドを把握できない
  • パラメータ名を省略せずフルに書くことで、読む人がコマンドに詳しくなくても伝えることができる
  • また、不要なパラメータセットにまつわるバグを避けることができる
# Do not write:
Get-WmiObject win32_service name,state

# Instead write:
Get-WmiObject -Class win32_service -Property name,state

(Naming Conventions) Use full, explicit paths when possible.

  • 省略パスを用いないようにする

基本的に...が有効に機能するのは、特定のパスにいる時だけです

  • .NETメソッドが[Environment]::CurrentDirectoryを使いPowerShellの現在パスを尊重しないことを考慮しても、やはりこのような相対パス指定は避けるようにするとよい
# Do not write:
Get-Content .\README.md

# Especially do not write:
[System.IO.File]::ReadAllText(".\README.md")

# Instead write:
Get-Content (Join-Path $PSScriptRoot README.md)

# Or even use string concatenation:
[System.IO.File]::ReadAllText("$PSScriptRoot\README.md")

(Naming Conventions) Avoid the use of ~ to represent the home folder.

  • ~はプロバイダによって変わるので避ける
  • 代わりに${Env:UserProfile}(Get-PSProvider FileSystem).Homeが有用
PS> cd ~
PS> cd HKCU:\Software
PS HKCU:\Software> cd ~
cd : Home location for this provider is not set. To set the home location, call "(get-psprovider 'Registry').Home = 'path'".
At line:1 char:1
+ cd ~
+ ~~~~
    + CategoryInfo          : InvalidOperation: (:) [Set-Location], PSInvalidOperationException
    + FullyQualifiedErrorId : InvalidOperation,Microsoft.PowerShell.Commands.SetLocationCommand

C#で書くCmdletのCoding Style

Design Guideに含められいるのですが、PascalCaseなどのルールはC#のコード基準に準しています。ただ、prefixを推奨したりしていて必ずしもC#のこー準していません。

  • (Required) PSCmdletを継承する
  • (Required) CmdletはAtrributeで挙動や命名を表現できる
  • (Required) BeginProcessing/ProcessRecord/EndProcessingを用いて、入力を操作する
    • (Required) Pipelineで用いないならBeginProcessingのみで表現する
    • (Required) Pipelinで用いるなら、ProcessRecordで入力を操作する
      • (Strong) この時、入力ストリームはためず即座に出力する
  • (Required) 出力型はOutputType属性で明示する
  • (Required) 出力オブジェクトは、内部で保持しないように
  • (Required) 例外はErrorRecordを用いて適切に扱う
  • (Required) CmdletはModuleとして配布する
  • (Strong) パスを用いるCmdletのParameterにはパスやLiteralPathを容易するとよい
    • (Strong) Path = Wildcard入力を受け入れる場合に使う。PSPathをAliasとして公開する
    • (Strong) LiteralPath = Wildcard入力を受け入れない場合に使う
  • (Strong) 可能であればWildcard入力を受け入れる
  • (Strong) 自前の型を用意した場合、custom Types.ps1xml<YourProductName>.Format.ps1xmとして用意する
    • (Strong) <YourProductName>.Format.ps1xmで、出力された表示を適切にする
  • (Strong) 比較が必要ならIComparableを実装する
  • (Strong) パイプラインで動くようにする
    • (Strong) パイプラインの入力を受け入れるようにする
      • (Strong) その場合ProcessRecordでハンドルする
    • (Strong) パイプラインではシングルレコードを返する
  • (Strong) Cmdletはケースインセンシティブにする
  • (Advice) 名前空間に、.Commandをつける
  • (Advice) Cmdletのクラス名は<Verb><Noun><Command>とする
  • (Advice) StopProcessingをオーバーライドして操作を止める
  • (Advice) リソースを使う場合は、IDisposableを実装して明示的に呼び出する
  • (Advice) リモート動作させるなら、シリアライズしやすいとよい

PowerShellで書くFuncionのCoding Style

インテンドスタイルを除き、多くは .NETでCmdletを書いたらどうするのかを基本コンセプトにしています。ただ、PowerShellの言語仕様として、;を省略できるといった部分はいらないなら書かなくてもいい。Backticks避けようね、といったどうすれば読みやすいか、言語として書きやすいかを大事にしています。

こちらも最も重要なのは、Verb-Noun形式での公開でしょう。

  • AdvancedFunctionが標準なので検討する

[CmdletBindings]属性がないと各種属性が書けないので妥当。

  • [OutputType]は、PowerShellのオブジェクト = 型を伝える、パイプラインの伝搬という性質に強く影響があるため必須

ただし、OutputTypeと実際の返戻値は一致してなくても警告/エラーがでないので注意が必要です。ここがあまい。

  • #Requiresタグは有益。今後Coreが広まるなかで、より有益になるでしょう

実行時にしかわからないあたりが触り心地はよくないです。

  • Set-StrictMode -Version Latestは 、あって困ることはないのでいれましょう
  • Verb-Noun形式大事

外部に露出しない関数は、VerbNounのように普通のメソッド形式で書いています。公開されてないならVerb-Nounにする理由がないからです。(この例はPowerShell Teamのコードにも良く見ますね)

  • ローカル変数はcamelCase、パラメータなどはPascalCase

これ広まってほしい。

  • Splattingはやりすぎると読みにくくなりますが、多くの面で有益
  • 命名規則、省略を避ける

PowerShellの目指す「わかりやすさ」に沿っており大事にしたいところです。

  • Pipelineでの動作がいらない時はbeingのみ書く

省略したときの挙動との一致ですね

  • Pipelineでの動作がいる場合は、processで返却する

endで返すことも多いので納得です。

  • returnはなるべく使わない

これはreturnをすると制御、関数が終わることを意図しています。

※ ここにはありませんでしたが、ASCIIキーボードにおいて'"よりもShift入力が不要でタイポしにくいことから、文字列は'が推奨されています。日本語キーボード? どっちでもいいですよねぇ。

余談

PowerShell Coreのコーディングスタイル

PowerShellは、現在PowerShell CoreがOSSとして開発されています。これはC# で書かれています。そのため、このPowerShell本体で用いられているC# のコーディング規則が公開されています。

C# Coding Guidelines

基盤実行のため、わかりやすさとパフォーマンスに重点が置かれていることが見て取れます。

PowerShell のインデントスタイル

そもそもこれを書きたかったのです。

PowerShellは、式モード(Scriptの解釈) の文法解釈においてAllmanK&R 1TBS StyleStroupstrupのいずれも解釈できます。しかし、「PowerShell.exeのコマンドモード」と「PowerShell ISEなどの式モード」で構文解釈にずれがあるので、両方で安定した記法はK&R 1TBS Styleです。

私自身C# と一緒に書くことが多かったのでAllmanで書いていましたがPowerShellでも動くようにフォーマットのために制御文字で調整はナンセンスかな、と考えなおしました。同様にStroustrup`も不都合が多いと感じます。

K&R 1TBS Style

PowerShellの文解釈と式解釈にずれが生じません。

if ($this -gt $that) {
    Do-Something -with $that
} else {
    # NANIKA
}

BSD Allman

式解釈では問題ありませんが、PowerShellにはった時ifの下の{elseでエラーがでます。

if ($this -gt $that)
{                             # <- ここでエラー
    Do-Something -with $that
}
else
{
    # NANIKA
}

Stroupstrup

式解釈では問題ありませんが、PowerShellにはった時elseでエラーがでます。

if ($this -gt $that) {
    Do-Something -with $that
}
else {                         # <- ここでエラー
    # NANIKA
}

ただし、これは、普段どちらで書いているかなどに左右されますし、チームでの共通化がとれていればいいのでしょう。

ちなみに、コミュニティでは、Stroupstrupが意外と多い結果です。

https://github.com/PoshCode/PowerShellPracticeAndStyle/issues/24

ドキュメントフォーマッター

いまやデファクトスタンダードとなった、VS Codeでもフォーマット可能です。

https://marketplace.visualstudio.com/items?itemName=ms-vscode.PowerShell

このあたりのエディタに任せる、という判断が私は好ましいと思っています。コードフォーマットより、内容に注力したいですからねぇ。

まとめ

以上がPowerShell Coding Styleです。コーディングスタイルは、あくまでもその言語らしさを第一に、どうやれば言語の良さを活用できるかを念頭に考えられています。

特にPowerShellコミュニティは、強制を避けつつアドバイスとしてというスタイルで醸成を図っています。みなさんのコード公開時に悩むということが減ることを祈ります。

軽くまとめます。