diff --git a/Best-Practices/Building-Reusable-Tools.md b/Best-Practices/Building-Reusable-Tools.md index ff3bacf..47c5fa9 100644 --- a/Best-Practices/Building-Reusable-Tools.md +++ b/Best-Practices/Building-Reusable-Tools.md @@ -2,20 +2,18 @@ For this discussion, it's important to have some agreed-upon terminology. While the terminology here isn't used universally, the community generally agrees that several types of "script" exist: -1. Some scripts contain tools, when are meant to be reusable. These are typically functions or advanced functions, and they are typically contained in a script module or in a function library of some kind. These tools are designed for a high level of re-use. -2. Some scripts are controllers, meaning they are intended to utilize one or more tools (functions, commands, etc) to automate a specific business process. A script is not intended to be reusable; it is intended to make use of reuse by leveraging functions and other commands +1. Some scripts contain tools, which are meant to be reusable. These are typically functions or advanced functions, and they are typically contained in a script module or in a function library of some kind. These tools are designed for a high level of reuse. +2. Some scripts are controllers, meaning they are intended to utilize one or more tools (functions, commands, etc) to automate a specific business process. A controller script is not intended to be reusable; it is intended to make use of reuse by leveraging functions and other commands. For example, you might write a "New-CorpUser" script, which provisions new users. In it, you might call numerous commands and functions to create a user account, mailbox-enable them, provision a home folder, and so on. Those discrete tasks might also be used in other processes, so you build them as functions. The script is only intended to automate that one process, and so it doesn't need to exhibit reusability concepts. It's a standalone thing. -Controllers, on the other hand, often produce output directly to the screen (when designed for interactive use), or may log to a file (when designed to run unattended). - +Controllers often produce output directly to the screen (when designed for interactive use), or may log to a file (when designed to run unattended). # TOOL-02 Make your code modular Generally, people tend to feel that most working code - that is, your code which does things - should be modularized into functions and ideally stored in script modules. -That makes those functions more easily re-used. Those functions should exhibit a high level of reusability, such as accepting input only via parameters and producing output only as objects to the pipeline - +That makes those functions more easily reused. Those functions should exhibit a high level of reusability, such as accepting input only via parameters and producing output only as objects to the pipeline. # TOOL-03 Make tools as re-usable as possible @@ -29,9 +27,9 @@ You can get a list of the verbs by typing 'get-verb' at the command line. # TOOL-05 Use PowerShell standard parameter naming -Tools should be consistent with PowerShell native cmdlets in regards parameter naming. +Tools should be consistent with PowerShell native cmdlets in regard to parameter naming. -For example, use $ComputerName and $ServerInstance rather than something like $Param_Computer or $InstanceName +For example, use $ComputerName and $ServerInstance rather than something like $Param_Computer or $InstanceName. # TOOL-06 Tools should output raw data @@ -45,7 +43,6 @@ For example, a function named Get-DiskInfo would return disk sizing information An intermediate step is useful for tools that are packaged in script modules: views. By building a manifest for the module, you can have the module also include a custom .format.ps1xml view definition file. The view can specify manipulated data values, such as the default view used by PowerShell to display the output of Get-Process. The view does not manipulate the underlying data, leaving the raw data available for any purpose. - # WAST-01 Don't re-invent the wheel There are a number of approaches in PowerShell that will "get the job done." In some cases, other community members may have already written the code to achieve your objectives. If that code meets your needs, then you might save yourself some time by leveraging it, instead of writing it yourself. @@ -77,16 +74,12 @@ It has been argued by some that, "I didn't know such-and-such existed, so I wrot On the flip side, it's important to note that writing your own code from the ground up can be useful if you are trying to learn a particular concept, or if you have specific needs that are not offered by another existing solution. - # WAST-02 Report bugs to Microsoft -An exception: if you know that a built-in technique doesn't work properly (e.g., it is buggy or doesn't accomplish the exact task), then obviously it's fine to re-invent the wheel. However, in cases where you're doing so to avoid a bug or design flaw, then you should - as an upstanding member of the community - report the bug on [connect.microsoft.com](http://connect.microsoft.com/) also. - - +An exception: if you know that a built-in technique doesn't work properly (e.g., it is buggy or doesn't accomplish the exact task), then obviously it's fine to re-invent the wheel. However, in cases where you're doing so to avoid a bug or design flaw, then you should - as an upstanding member of the community - report the bug on [github.com/powershell](https://github.com/PowerShell/PowerShell/issues) also. TODO: The "PURE" section is dubious at best. We need to discuss it. - # PURE-01 Use native PowerShell where possible This means not using COM, .NET Framework classes, and so on when there is a native Windows PowerShell command or technique that gets the job done. @@ -104,4 +97,3 @@ Document the reason for using tools other than PowerShell in your comments. That said, you truly become a better PowerShell person if you take the time to wrap a less-preferred way in an advanced function or cmdlet. Then you get the best of both worlds: the ability to reach outside the shell itself for functionality, while keeping the advantages of native commands. Ignorance, however, is no excuse. If you've written some big wrapper function around Ping.exe simply because you were unaware of Test-Connection, then you've wasted a lot of time, and that is not commendable. Before you move on to a less-preferred approach, make sure the shell doesn't already have a way to do what you're after. - diff --git a/Best-Practices/Introduction.md b/Best-Practices/Introduction.md index b32ef39..ca8cad6 100644 --- a/Best-Practices/Introduction.md +++ b/Best-Practices/Introduction.md @@ -2,13 +2,13 @@ ## Foreword -If you scan through code projects on [PoshCode](https://github.com/PoshCode) or the [Technet ScriptCenter Gallery](http://gallery.technet.microsoft.com/scriptcenter), it will be immediately apparent that people in the PowerShell community have vastly different ideas about what's "right and wrong" in the world of PowerShell scripting. +If you scan through code projects on [PoshCode](https://github.com/PoshCode) or the [Microsoft Learn Code Samples Gallery](https://learn.microsoft.com/en-us/samples/browse/?languages=powershell), it will be immediately apparent that people in the PowerShell community have vastly different ideas about what's "right and wrong" in the world of PowerShell scripting. Over the years several attempts have been made to arrive at a consensus, most notably the "Great Debate" series of blog posts on [PowerShell.org](https://powershell.org/?s=great+debate) following the 2013 Scripting Games, which outlined some of the more controversial issues and asked for community discussions. This project has been created, in part, as a public place to continue those debates as well as to document the consensus of the community when we _do_ arrive at a consensus. -Remember that best practices are not hard-and-fast rules. We don't even consider them as solid as the style guide rules. They are the things you should _usually_ do as a starting point, and should deviate from when it's appropriate. +Remember that best practices are not hard-and-fast rules. We don't even consider them as solid as the [Style Guide](../Style-Guide/Introduction.md) rules. They are the things you should _usually_ do as a starting point, and should deviate from when it's appropriate. One final note about these Best Practices: the perspective of these guidelines has so-far been strongly influenced by system administrator practitioners, less-so by language geeks and developers. We're trying to balance that and provide perspective, but when it comes to best practices, we definitely allow the experience of administrators to drive what the PowerShell community considers best and worst practices -- so if you are working from a different perspective, you'll have to take all of this with a grain of salt. @@ -16,9 +16,10 @@ One final note about these Best Practices: the perspective of these guidelines h - [Naming Conventions](Naming-Conventions.md) - [Building Reusable Tools](Building-Reusable-Tools.md) +- [Writing Parameter Blocks](Writing-Parameter-Blocks.md) - [Output and Formatting](Output-and-Formatting.md) - [Error Handling](Error-Handling.md) - [Performance](Performance.md) - [Security](Security.md) -- [Language, Interop and .Net](Language-Interop-and-.Net.md) +- [Language, Interop, and .NET](Language-Interop-and-.NET.md) - [Metadata, Versioning, and Packaging](Metadata-Versioning-and-Packaging.md) diff --git a/Best-Practices/Language-Interop-and-.Net.md b/Best-Practices/Language-Interop-and-.NET.md similarity index 96% rename from Best-Practices/Language-Interop-and-.Net.md rename to Best-Practices/Language-Interop-and-.NET.md index e5df50b..f0d697b 100644 --- a/Best-Practices/Language-Interop-and-.Net.md +++ b/Best-Practices/Language-Interop-and-.NET.md @@ -30,7 +30,7 @@ The `#requires` statement will prevent the script from running on the wrong vers ### PowerShell Supported Version -When working in an environment where there are multiple versions of PowerShell make sure to specify the lowest version your script will support by prividing a Requires statement at the top of the script. +When working in an environment where there are multiple versions of PowerShell make sure to specify the lowest version your script will support by providing a Requires statement at the top of the script. ```PowerShell #Requires -Version 2.0 diff --git a/Best-Practices/Output-and-Formatting.md b/Best-Practices/Output-and-Formatting.md index 702fa5a..d1a7fc0 100644 --- a/Best-Practices/Output-and-Formatting.md +++ b/Best-Practices/Output-and-Formatting.md @@ -6,7 +6,7 @@ TODO: This whole document is STILL ROUGH DRAFT Previous to PowerShell 5, Write-Host has no functionality at all in non-interactive scripts. It cannot be captured or redirected, and therefore should only be used in functions which are "Show"ing or "Format"ing output, or to display something as part of an interactive prompt to the user. -That is: you should not use Write-Host to create script output unless your script (or function, or whatever) uses the Show verb (as in, Show-Performance) or the Format verb (as in, Format-Hex), or has a `-Formatted` switch parameter. You may also use it to build a interactions with the user in other cases (e.g. to write extra information to the screen before prompting the user for a choice or input). +That is: you should not use Write-Host to create script output unless your script (or function, or whatever) uses the Show verb (as in, Show-Performance) or the Format verb (as in, Format-Hex), or has a `-Formatted` switch parameter. You may also use it to build interactions with the user in other cases (e.g., to write extra information to the screen before prompting the user for a choice or input). Generally, you should consider the other Write-* commands first when trying to give information to the user. @@ -22,7 +22,7 @@ You should use verbose output for information that contains details about the va ## Use Write-Debug to give information to someone maintaining your script -You should use the debug output stream for output that is useful for script debugging (ie: "Now entering main loop" or "Result was null, skipping to end of loop"), or to display the value of a variable before a conditional statement, so the maintainer can break into the debugger if necessary. +You should use the debug output stream for output that is useful for script debugging (e.g., "Now entering main loop" or "Result was null, skipping to end of loop"), or to display the value of a variable before a conditional statement, so the maintainer can break into the debugger if necessary. > TIP: When debugging you should be aware that you can set `$DebugPreference = "Continue"` to see this information on screen without entering a breakpoint prompt. @@ -48,7 +48,7 @@ When you combine the output of multiple types objects, they should generally be ### Two important exceptions to the single-type rule -**For internal functions** it's ok to return multiple different types because they won't be "output" to the user/host, and can offer significant savings (e.g. one database call with three table joins, instead of three database calls with two or three joins each). You can then call these functions and assign the output to multiple variables, like so: +**For internal functions** it's ok to return multiple different types because they won't be "output" to the user/host, and can offer significant savings (e.g., one database call with three table joins, instead of three database calls with two or three joins each). You can then call these functions and assign the output to multiple variables, like so: ```PowerShell $user, $group, $org = Get-UserGroupOrg diff --git a/Best-Practices/Performance.md b/Best-Practices/Performance.md index 8c93574..0b0a50e 100644 --- a/Best-Practices/Performance.md +++ b/Best-Practices/Performance.md @@ -2,14 +2,16 @@ PowerShell comes equipped with 3.2 million performance quirks. Approximately. -For example, the first line below executes a lot faster than the second: +If you're aware of multiple techniques to accomplish something, and you're writing a production script that will be dealing with large data sets (meaning performance will become a cumulative factor), then test the performance using Measure-Command or the Profiler module, or some other tool. + +For example: ```PowerShell -[void]Do-Something -Do-Something | Out-Null +foreach ($result in Do-Something) { $result.PropertyOne + $result.PropertyTwo } +Do-Something | ForEach-Object { $_.PropertyOne + $_.PropertyTwo } ``` -If you're aware of multiple techniques to accomplish something, and you're writing a production script that will be dealing with large data sets (meaning performance will become a cumulative factor), then test the performance using Measure-Command or some other tool. +In this case, the `foreach` language construct is faster than piping to the `ForEach-Object` cmdlet -- but the point is that you should measure, and do so on the hardware and PowerShell version where the performance matters to you. # PERF-02 Consider trade-offs between performance and readability @@ -20,10 +22,10 @@ This is an important area for people in the PowerShell community. While everyone For example: ```PowerShell -$content = Get-Content file.txt +$content = Get-Content -Path file.txt -ForEach ($line in $content) { - Do-Something -input $line +foreach ($line in $content) { + Do-Something -Input $line } ``` @@ -32,37 +34,47 @@ Most folks will agree that the basic aesthetics of that example are good. This s Now consider this alternate approach: ```PowerShell -Get-Content file.txt | +Get-Content -Path file.txt | ForEach-Object -Process { - Do-Something -input $\_ + Do-Something -Input $_ } ``` -As described elsewhere in this guide, many folks in the community would dislike this approach for aesthetic reasons. However, this approach has the advantage of utilizing PowerShell's pipeline to "stream" the content in file.txt. Provided that the fictional "Do-Something" command isn't blocking the pipeline (a la Sort-Object), the shell can send lines of content (String objects, technically) through the pipeline in a continuous stream, rather than having to buffer them all into memory. +As described elsewhere in this guide, many folks in the community would dislike this approach for aesthetic reasons. However, this approach has the advantage of utilizing PowerShell's pipeline to "stream" the content in file.txt. Provided that the fictional "Do-Something" command isn't blocking the pipeline (à la Sort-Object), the shell can send lines of content (String objects, technically) through the pipeline in a continuous stream, rather than having to buffer them all into memory. Some would argue that this second approach is always a poor one, and that if performance is an issue then you should devolve from a PowerShell-native approach into a lower-level .NET Framework approach: ```PowerShell -$sr = New-Object -Type System.IO.StreamReader -Arg file.txt +$sr = New-Object -TypeName System.IO.StreamReader -ArgumentList file.txt while ($sr.Peek() -ge 0) { - $line = $sr.ReadLine() - Do-Something -input $line + $line = $sr.ReadLine() + Do-Something -Input $line } ``` There are myriad variations to this approach, of course, but it solves the performance problem by reading one line at a time, instead of buffering the entire file into memory. It maintains the structured programming approach of the first example, at the expense of using a potentially harder-to-follow .NET Framework model instead of native PowerShell commands. Many regard this third example as an intermediate step, and suggest that a truly beneficial approach would be to write PowerShell commands as "wrappers" around the .NET code. For example (noting that this fourth example uses fictional commands by way of illustration): ```PowerShell -$handle = Open-TextFile file.txt +$handle = Open-TextFile -Path file.txt -while (-not Test-TextFile -handle $handle) { - Do-Something -input (Read-TextFile -handle $handle) +while (-not (Test-TextFile -Handle $handle)) { + Do-Something -Input (Read-TextFile -Handle $handle) } ``` -This example reverts back to a native PowerShell approach, using commands and parameters. The proposed commands (Open-TextFile, Test-TextFile, and Read-TextFile) are just wrappers around .NET Framework classes, such as the StreamReader class shown in the third example. +This example reverts back to a native PowerShell approach, using commands and parameters. The proposed commands (`Open-TextFile`, `Test-TextFile`, and `Read-TextFile`) are just wrappers around .NET Framework classes, such as the StreamReader class shown in the third example. You will generally find that it is possible to conform with the community's general aesthetic preferences while still maintaining a good level of performance. Doing so may require more work - such as writing PowerShell wrapper commands around underlying .NET Framework classes. Most would argue that, for a tool that is intended for long-term use, the additional work is a worthwhile investment. The moral here is that both aesthetic and performance are important considerations, and without some work context, neither is inherently more important than the other. It is often possible, with the right technique, to satisfy both. As a general practice, you should avoid giving up on aesthetics solely because of performance concerns - when possible, make the effort to satisfy both performance and aesthetics. + +# PERF-03 Language > Framework > Script > Pipeline + +This is just a rough guideline, but as a general rule: + +1. Language features are faster than features of the .NET Framework +2. Compiled methods on objects and .NET classes are still faster than script +3. Simple PowerShell script is still faster than calling functions or cmdlets + +It's counter-intuitive that script is faster than calling cmdlets that are compiled, but it's frequently true, unless there is a lot of work being done by each cmdlet. The overhead of calling cmdlets and passing data around is significant. Of course, this is just a guideline, and you should always **measure**. diff --git a/Best-Practices/Security.md b/Best-Practices/Security.md index e70f0ce..50c4472 100644 --- a/Best-Practices/Security.md +++ b/Best-Practices/Security.md @@ -16,11 +16,11 @@ param ( ) ``` -If you absolutely must pass a password in a plain string to a .Net API call or a third party library it is better to decrypt the credential as it is being passed instead of saving it in a variable. +If you absolutely must pass a password in a plain string to a .NET API call or a third party library, it is better to decrypt the credential as it is being passed instead of saving it in a variable. ```PowerShell # Get the cleartext password for a method call: - $Insecure.SetPassword( $Credentials.GetNetworkCredential().Password ) + $Insecure.SetPassword($Credentials.GetNetworkCredential().Password) ``` #### Other Secure Strings @@ -32,10 +32,10 @@ Note, if you ever need to turn a SecureString into a string, you can use this me ```PowerShell # 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 + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($this) + $plaintext = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) + $plaintext ``` * For credentials that need to be saved to disk, serialize the credential object using @@ -63,4 +63,3 @@ computer where it was generated. # Read the Standard String from disk and convert to a SecureString $Secure = Get-Content -Path "${Env:AppData}\Sec.bin" | ConvertTo-SecureString ``` - diff --git a/Best-Practices/TODO.md b/Best-Practices/TODO.md index 66e0641..2fe8cd6 100644 --- a/Best-Practices/TODO.md +++ b/Best-Practices/TODO.md @@ -1,6 +1,6 @@ These documents are in an extremely rough state, not suitable for inclusion in the main guide yet. -### Using The .Net Framework +### Using the .NET Framework - [Control what gets exported in a module](#control-what-gets-exported-in-a-module) - [Specify when to use a Manifest for a module](#specify-when-to-use-a-manifest-for-a-module) @@ -51,19 +51,17 @@ TODO #### Control what gets exported in a module #### Specify when to use a Manifest for a module - #### Use RequiredAssemblies rather than Add-Type #### Use Add-Type rather than Reflection Avoid using `[System.Reflection]` to load assemblies when possible. Particularly avoid `LoadWithPartialName` (specify the full name instead). #### Use Add-Type for small classes or PInvoke calls -TODO: Is this better than PowerShell Classes, for compatibility? +TODO: Is this better than PowerShell classes, for compatibility? #### Prefer shipping binaries over large compilations -With PowerShell 5, security is tighter, and compiling code in memory will be frowned upon. Now that we have PowerShellGet and the PowerShell Gallery, there are few reasons 's no reason to avoid binaries. - -TODO: Discuss: when does embedding C# code makes sense more sense than just compiling it every time? +With PowerShell 5, security is tighter, and compiling code in memory will be frowned upon. Now that we have PowerShellGet and the PowerShell Gallery, there are few reasons or no reason to avoid binaries. +TODO: Discuss: when does embedding C# code makes more sense than just compiling it every time? ### Performance @@ -72,10 +70,10 @@ Prefer foreach(){} over ForEach-Object Prefer .foreach and .where over cmdlets Prefer functions with process blocks over ForEach-Object -#### Know when to use .Net -Prefer writing wrapper functions to just calling .Net APIs +#### Know when to use .NET +Prefer writing wrapper functions to just calling .NET APIs Discuss: System.IO file loading vs Get-Content (large files) -Discuss: Other examples of .Net API calls that are clearly faster? +Discuss: Other examples of .NET API calls that are clearly faster? Discuss: Casting for creating objects ### Error Handling @@ -85,7 +83,6 @@ Discuss: Avoid depending on `$?` -- why? Discuss: Never use `$Error` in scripts (always use -ErrorVariable) Discuss: Interactively, always copy $Error[0] to $e or something - ### General Design Principles #### Use custom objects @@ -103,26 +100,24 @@ Discuss: During development, always write scripts, which are automatically re-pa This is in the Style Guide too, but we should discuss it in more depth here, and link to it from the Style Guide. Scripts should start life as something like this: ``` -[CmdletBinding()]param() +[CmdletBinding()] +param() process{} end{} ``` You can always ignore one of the blocks, and add parameters and such, but you should never write a script without CmdletBinding, and you should never write one without at least considering making it take pipeline input - ### Include Help TODO: Link to StyleGuide about formatting help -Discuss: Minimum acceptable comment based help: Synopsis, Parameters, and an example for each parameter set (plus pipeline examples if you can contrive one) +Discuss: Minimum acceptable comment-based help: Synopsis, Parameters, and an example for each parameter set (plus pipeline examples if you can contrive one) Discuss: Benefits of MAML help files -#### Always ship a about_ModuleName with modules +#### Always ship an about_ModuleName with modules Discuss: Other reasons to write about_topics - - #### Prefer PipelineByPropertyName parameters. Discuss: This allows the most flexibility: piping objects and using scriptblocks to shape it for parameters. Unless you absolutely need to write a `begin` block and use this parameter in it, you probably should accept it on the pipeline. @@ -137,7 +132,7 @@ You can use aliases to map parameters to property names of objects which might b Particularly when splatting PSBoundParameters to the next function, if that function isn't `[CmdletBinding()]` (it should be!) you must remember to strip the common parameters if they're present. #### Specify positional parameters, but don't use them -When writing at the command line, positional parameters are a blessing, but they can be confusing for future readers. You should always expose your parameters positionally when it makes sense, but you should rarely share a script that pases parameters positionally. +When writing at the command line, positional parameters are a blessing, but they can be confusing for future readers. You should always expose your parameters positionally when it makes sense, but you should rarely share a script that passes parameters positionally. #### Specify short aliases, but don't use them Again, for the sake of typing, it's particularly useful if you can provide two-letter aliases for each of your parameters such that every parameter has a two-letter or less name which is unique. @@ -155,29 +150,26 @@ ValueFromPipelineByPropertyName = $true, HelpText = 'The name of the file to rea [String]$File ``` - #### Use PsCmdlet.ThrowTerminatingError rather than throw #### Use PsCmdlet.WriteError rather than Write-Error Discuss: These need example output to explain why they're better Discuss: a common template (from the Microsoft team) for throwing errors ``` -# Utility to throw an errorrecord -function ThrowError -{ - param - ( - [parameter(Mandatory = $true)] +# Utility to throw an error record +function ThrowError { + param( + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCmdlet] $CallerPSCmdlet, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ExceptionName, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ExceptionMessage, @@ -185,42 +177,42 @@ function ThrowError [System.Object] $ExceptionObject, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorId, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNull()] [System.Management.Automation.ErrorCategory] $ErrorCategory ) - $exception = New-Object $ExceptionName $ExceptionMessage; + $exception = New-Object -TypeName $ExceptionName -ArgumentList $ExceptionMessage $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $ErrorId, $ErrorCategory, $ExceptionObject $CallerPSCmdlet.ThrowTerminatingError($errorRecord) } ``` - #### Use SupportsShouldProcess when appropriate -Discuss: when is this critical (-whatif) and optional (-confirm_ -Discuss: when should you call PSCmdlet.ShouldProcess vs PSCmdlet.ShouldContinue (-Force) +Discuss: when is this critical (-WhatIf) and optional (-Confirm) +Discuss: when should you call $PSCmdlet.ShouldProcess vs $PSCmdlet.ShouldContinue (-Force) ``` [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")] param([Switch]$Force) -$RejectAll = $false; -$ConfirmAll = $false; +$RejectAll = $false +$ConfirmAll = $false -foreach($file in ls) { +foreach ($file in ls) { - if($PSCmdlet.ShouldProcess( "Removed the file '$($file.Name)'", + if($PSCmdlet.ShouldProcess("Removed the file '$($file.Name)'", "Remove the file '$($file.Name)'?", - "Removing Files" )) { + "Removing Files") + ) { - if($Force -Or $PSCmdlet.ShouldContinue("Are you REALLY sure you want to remove '$($file.Name)'?", "Removing '$($file.Name)'", [ref]$ConfirmAll, [ref]$RejectAll)) { + if ($Force -Or $PSCmdlet.ShouldContinue("Are you REALLY sure you want to remove '$($file.Name)'?", "Removing '$($file.Name)'", [ref]$ConfirmAll, [ref]$RejectAll)) { "Removing $File" @@ -242,7 +234,6 @@ The problems with this have gone away with autoloading, and this is the only way #### Persisting Configuration My choice: Configuration module. Otherwise, use clixml (or XAML) to persist to AppData (TEST: you shouldn't store configuration in your module folder, as it may not survive upgrades (in PowerShell 3 & 4 there was no side-by-side loading)) - #### Provide aliases in your modules You should feel free to create and use aliases within your modules. In some cases, you can even improve readability by using an alias without the verb, or shortening command names. @@ -250,28 +241,23 @@ For exported aliases, follow the guidance of Microsoft ("ip" for import, "s" for Use `New-Alias ... -ErrorAction SilentlyContinue` to avoid overwriting existing aliases. - - - ### GOTCHAS -#### Beware string concatenation with + -You should always wrap this with parentheses, because otherwise it can break (e.g. when passing a string as a parameter. +#### Beware of string concatenation with + +You should always wrap this with parentheses, because otherwise it can break (e.g., when passing a string as a parameter). -#### Beware -match and -like +#### Beware of -match and -like They quietly cast objects to strings (or arrays of strings) -#### Beware -contains and -in -They work on ARRAYS not strings - +#### Beware of -contains and -in +They work on ARRAYS, not strings ### Use Language Features -When writing scripts (as opposed to at the command line), you should almost always choose language features over cmdlets. This includes using if instead of where-object, foreach instead of ForEach-Object, etc. - -The language features are always faster, and almost always more readable. Of course, there are always exceptions, and one exception to this rule is when using foreach will force you to collect a lot of items into an array instead of iterating them as they stream through a pipleine. +When writing scripts (as opposed to at the command line), you should almost always choose language features over cmdlets. This includes using `if` instead of `Where-Object`, `foreach` instead of `ForEach-Object`, etc. +The language features are always faster, and almost always more readable. Of course, there are always exceptions, and one exception to this rule is when using `foreach` will force you to collect a lot of items into an array instead of iterating them as they stream through a pipeline. -### You should understand the .Net underpinnings +### You should understand the .NET underpinnings #### AVOID appending to string in a loop ##### INSTEAD assign output from the loop using $OFS @@ -283,19 +269,17 @@ The language features are always faster, and almost always more readable. Of cou * Joining an array of strings is fast * Easier to read and understand +### Strongly typed parameters +Although PowerShell is a dynamic language, we can specify types, and in parameters, it's particularly useful because it hints to users what they can pass to your command. -### Strongly type parameters -Although PowerShell is a dynamic language, we have can specify types, and in Parameters, it's particularly useful because it hints to users what they can pass to your command. - -Strong types on parameters is also crucial because it's a user-input point. Strong types can help you avoid script injection and various other problems with user inputs, and will allow failures to happen as early as possible (even before your command is called). +Strong types on parameters is also crucial because it's a user-input point. Strong types can help you avoid script injection and various other problems with user inputs, and will allow failures to happen as early as possible (even before your command is called). Additionally, avoid using `[string]` with ParameterSets because anything can be cast to it, so PowerShell can't distinguish one parameter set from the other. -When passing on parameters to another command, you should be _at least_ as strongly typed as the other command, to avoid casting exceptions within your script. +When passing on parameters to another command, they should be _at least_ as strongly typed as the other command, to avoid casting exceptions within your script. One notable exception is when you could accept more than one type. In PowerShell you can specify parameter set overloads, but you can't change the type of a parameter. - ### Don't reinvent the wheel -### Let's talk about Logging -### Let's talk about code signing \ No newline at end of file +### Let's talk about logging +### Let's talk about code signing diff --git a/Best-Practices/Writing-Parameter-Blocks.md b/Best-Practices/Writing-Parameter-Blocks.md new file mode 100644 index 0000000..4718f6f --- /dev/null +++ b/Best-Practices/Writing-Parameter-Blocks.md @@ -0,0 +1,127 @@ +# Writing Parameter Blocks + +## Always write Help + +For every script and function you should have a comment-based help block (we recommend using a block comment). The best place for these is _inside_ the `function` above the `param` block, but they can also be placed _above_ the function, or at the bottom just before closing. + +In order for it to register as help, you must provide a `.SYNOPSIS` and/or `.DESCRIPTION` + +### Always Write Examples + +You should also always provide at least one example (even if your function doesn't take parameters), where you show the output (or the need to capture it), and explain what happens when the command is run. Note that examples should have the _code_ first, and the _documentation_ of what it does after an empty line or two. + + +```PowerShell +function Test-Help { + <# + .SYNOPSIS + An example function to display how help should be written. + + .EXAMPLE + Test-Help -Name Test-Help + + This tests the help for the Test-Help function. + #> + [CmdletBinding()] + param ( + # This parameter doesn't do anything, but you must provide a value + # Aliases: MP + [Parameter(Mandatory = $true)] + [Alias("MP")] + [String]$MandatoryParameter + ) + + <# code here ... #> +} +``` +### Always Document Every Parameter + +You should always provide at least a brief explanation of each parameter, what its expected or allowed values are, etc. + +The best place for this is a simple comment directly above the parameter (inside the param block) so you don't forget to update it if you remove, rename, or otherwise change the parameter, but you can also place them in the comment help block by using `.PARAMETER ParameterName` and writing the help on the next line. + +## You should specify `[CmdletBinding()]` + +CmdletBinding makes functions and scripts behave like the built-in commands, adding support for the "common" output parameters like `-Verbose` and `-ErrorAction` and supporting `-?` for help. If you don't support it, you risk someone _accidentally_ running your code when they were just trying to `Get-Help` on it. + +There are a few specific advanced cases where you might want to write an old-fashioned script that doesn't use CmdletBinding() -- they are very rare, and are all exceptions to the best practices, so we won't go into them further. + +## You should pick a default ParameterSet + +If you have more than one ParameterSetName on your parameters, you should specify one of them as the `DefaultParameterSetName` in the CmdletBinding. + +## You should support -WhatIf + +If you write a command that changes state, you should probably add `SupportsShouldProcess` to your CmdletBinding. This allows users to specify `-WhatIf` and `-Confirm` when calling your command, so you'll need to actually support those by using `$PSCmdlet.ShouldProcess(...)` or `$PSCmdlet.ShouldContinue(...)` or by passing the preference variable on to other commands you're calling (e.g., `-Whatif:$WhatIfPreference`). + +Here's an example of what that might look like: + +```PowerShell +# NOTE: ConfirmImpact defaults to Medium +# But I recommend setting ConfirmImpact explicitly as a reminder :) +[CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Medium")] +param([switch]$Force) + +# You need to pre-define these (because they're passed by [ref]) +$RejectAll = $false +$ConfirmAll = $false + +# Note: please don't actually do this with services, restarting them in non-dependency order would be a nightmare... +foreach ($service in Get-Service | Where Status -eq "Running") { + # This will normally automatically be TRUE. It will only query if the user: + # 1. Has their $ConfirmPreference (default High) set LOWER than or equal to the ConfirmImpact in the cmdlet binding (default Medium) + # 2. Passes -Confirm, which sets the $ConfirmPreference in the function's scope to Low + if ($PSCmdlet.ShouldProcess( "Restarted the service '$($service.Name)'", + "Restart the '$($service.DisplayName)' service ($($service.Name))?", + "Restarting Services" )) { + + # If you use ShouldContinue, have a -Force parameter that bypasses it + # And if you know there may be multiple prompts, you should use this overload that supports the Confirm/Reject "All" option + # In this example, we're only prompting when there are dependent services, and otherwise restart without additional prompting + if ($Force -Or $service.DependentServices.Count -eq 0 -or $PSCmdlet.ShouldContinue( + "$($service.Name) has $($service.DependentServices.Count) dependent services. Are you sure?", + "Restarting the '$($service.DisplayName)' service", + [ref]$ConfirmAll, + [ref]$RejectAll)) { + "(Not actually) restarting $($service.DisplayName)" + } + } +} +``` + +## You should strongly type parameters + +Although PowerShell is a dynamic language, we can specify types, and in parameters, it's particularly useful. + +First, because it hints to users what sort of values they can pass to your command. Is it numeric? Text? An object? + +Second, because using types on parameters helps validate the input, which is crucial because parameters are where you get your user input. Strong types can help you avoid code injection and other problems with user inputs, and will allow failures to happen as early as possible (even before your command is called). + +Additionally, when passing on parameters to another command, they should be _at least_ as strongly typed as the other command, to avoid casting exceptions within your script. + +### Be careful with `[string]` or `[object]` (and `[PSObject]`) + +Obviously [string] is one of the most common parameter types, and [object] is the default type. However, because anything can be cast to these types, you should avoid combining these types with parameters that are designed to differentiate parameter sets, or that accept `ValueFromPipeline` because PowerShell will coerce _everything_ to that. + +Obviously if you want to accept more than one type of object on the same parameter, you have to use `[object]` or `[PSObject]` as the universal base types. You have to be very careful when doing this, and normally should use a `ValidateScript` to ensure the objects are one of your _actual_ supported types. + +### Using [pscredential] + +When you need to accept credentials, you almost always want to name the parameter `$Credential` and accept a [System.Management.Automation.PSCredential](https://docs.microsoft.com/en-us/dotnet/api/System.Management.Automation.PSCredential), which has special support in PowerShell for automatically coercing user names to credentials and more. + +In old versions of PowerShell, you needed to manually decorate these PSCredential parameters with `[System.Management.Automation.CmdletAttribute()]` in order to automatically coerce user names to credentials -- that is, to support the use case where someone writes `-Credential Jaykul` and is automatically prompted for the password using the secure credential prompt. In current versions, this is automatically added when you use the PSCredential type. + +### Using [switch] + +Parameters of type `[switch]` support passing as switches (without a value), and by default cannot be passed by position. + +- Switch parameters should not be given default values. They should always default to false. +- Switch parameters should be designed so that setting them moves a command from its default functionality to a less common or more complicated mode. +- Switch parameters should be treated as boolean values in your scripts. Corrolary: you should not write logic that depends on whether or not the user explicitly passed a value to a switch -- do not attempt to treat a switch as having three states! +- When you need to pass the value of a switch on to another command, you can either splat it, or specify it using the colon syntax for parameters, as in `-TheirSwitch:$MySwitch` + +## Be generous with accept ValueFromPipelineByPropertyName + +For the most flexibility, whenever it's practical, you should write your commands to accept their parameters from the pipeline _by property name_. To enhance your ability to match objects, you can add aliases for the parameter name using the `[Alias()]` attribute. + +Don't forget that values set from the pipeline are only available in the `process` block. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4751222..084a63b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ ## Contributing -Are you interested in helping create the style guide, or do you just want to change a specific rule? You may use the issues system or Github pull requests freely to suggests corrections and simple changes to rules. However, if you want to add a completely new rule, or totally change a rule, please open an issue and let us discuss it! +Are you interested in helping create the style guide, or do you just want to change a specific rule? You may use the issues system or Github pull requests freely to suggest corrections and simple changes to rules. However, if you want to add a completely new rule, or totally change a rule, please open an issue and let us discuss it! ### Tone @@ -33,7 +33,7 @@ To repeat from the ReadMe, the guidelines are divided into these sections: * [Error Handling](Best-Practices/Error-Handling.md) * [Performance](Best-Practices/Performance.md) * [Security](Best-Practices/Security.md) - * [Language, Interop and .Net](Best-Practices/Language-Interop-and-.Net.md) + * [Language, Interop, and .NET](Best-Practices/Language-Interop-and-.NET.md) * [Metadata, Versioning, and Packaging](Best-Practices/Metadata-Versioning-and-Packaging.md) Markdown documents on GitHub support linking within a document, but only to headers, so when editing, in addition to keeping practices and guidelines in the documents where they make sense, please use headlines for each guideline, and lower level headlines for rationale, examples, counter examples, and exceptions. diff --git a/LICENSE.md b/LICENSE.md index 6c3ab47..5b474fd 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -13,7 +13,7 @@ The authors encourage you to redistribute this content as widely as possible, bu #### Credits -_The Community Book of PowerShell Practices_ was originally compiled and edited by Don Jones and Matt Penny with input from the Windows PowerShell community on PowerShell.org +_The Community Book of PowerShell Practices_ was originally compiled and edited by Don Jones and Matt Penny with input from the Windows PowerShell community on PowerShell.org. Portions copyright (c) Don Jones, Matt Penny, 2014-2015 @@ -27,7 +27,7 @@ Portions copyright (c) Joel Bennett, 2015 #### NOTE -The *PowerShell Best Practices* are always evolving, and continue to be edited and updated as the language and tools (and our community understanding of them) evolve. We encourage you to check back for new editions at least twice a year, by visiting [https://github.com/PoshCode/PowerShellPracticeAndStyle](https://github.com/PoshCode/PowerShellPracticeAndStyle) +The *PowerShell Best Practices* are always evolving, and continue to be edited and updated as the language and tools (and our community understanding of them) evolve. We encourage you to check back for new editions at least twice a year, by visiting [https://github.com/PoshCode/PowerShellPracticeAndStyle](https://github.com/PoshCode/PowerShellPracticeAndStyle). The *PowerShell Style Guide* in particular is in PREVIEW, and we are still actively working out our disagreements about the rules in the guide through the GitHub issues system. Please don't be surprised if over then next few weeks we change rules to contradict what they say at this current moment. diff --git a/README.md b/README.md index 56bf9fd..e9019bb 100644 --- a/README.md +++ b/README.md @@ -43,14 +43,14 @@ The guidelines are divided into these sections: * [Error Handling](Best-Practices/Error-Handling.md) * [Performance](Best-Practices/Performance.md) * [Security](Best-Practices/Security.md) - * [Language, Interop and .Net](Best-Practices/Language-Interop-and-.Net.md) + * [Language, Interop and .NET](Best-Practices/Language-Interop-and-.NET.md) * [Metadata, Versioning, and Packaging](Best-Practices/Metadata-Versioning-and-Packaging.md) ### Current State: -Remember [what we mean by _Best Practices_](#what-are-best-practices) +Remember [what we mean by _Best Practices_](#what-are-best-practices). -The *PowerShell Best Practices* are always evolving, and continue to be edited and updated as the language and tools (and our community understanding of them) evolve. We encourage you to check back for new editions at least twice a year, by visiting [https://github.com/PoshCode/PowerShellPracticeAndStyle](https://github.com/PoshCode/PowerShellPracticeAndStyle) +The *PowerShell Best Practices* are always evolving, and continue to be edited and updated as the language and tools (and our community understanding of them) evolve. We encourage you to check back for new editions at least twice a year, by visiting [https://github.com/PoshCode/PowerShellPracticeAndStyle](https://github.com/PoshCode/PowerShellPracticeAndStyle). The *PowerShell Style Guide* in particular is in PREVIEW, and we are still actively working out our disagreements about the rules in the guide through the GitHub issues system. @@ -58,11 +58,11 @@ The *PowerShell Style Guide* in particular is in PREVIEW, and we are still activ Please use the issues system or GitHub pull requests to make corrections, contributions, and other changes to the text - we welcome your contributions! -For more information, see [CONTRIBUTING](CONTRIBUTING.md) +For more information, see [CONTRIBUTING](CONTRIBUTING.md). #### Credits -_The Community Book of PowerShell Practices_ was originally compiled and edited by Don Jones and Matt Penny with input from the Windows PowerShell community on PowerShell.org +_The Community Book of PowerShell Practices_ was originally compiled and edited by Don Jones and Matt Penny with input from the Windows PowerShell community on PowerShell.org. Portions copyright (c) Don Jones, Matt Penny, 2014-2015 diff --git a/Style-Guide/Code-Layout-and-Formatting.md b/Style-Guide/Code-Layout-and-Formatting.md index 0394875..84e5598 100644 --- a/Style-Guide/Code-Layout-and-Formatting.md +++ b/Style-Guide/Code-Layout-and-Formatting.md @@ -21,7 +21,7 @@ PowerShell is **not** case sensitive, but we follow capitalization conventions t * PascalCase - capitalize the first letter of each word * camelCase - capitalize the first letter of each word _except_ the first. -PowerShell uses PascalCase for _all_ public identifiers: module names, function or cmdlet names, class, enum, and attribute names, public fields or properties, global variables and constants, etc. In fact, since the _parameters_ to PowerShell commands are actually _properties_ of .Net classes, even parameters use PascalCase rather than camelCase. +PowerShell uses PascalCase for _all_ public identifiers: module names, function or cmdlet names, class, enum, and attribute names, public fields or properties, global variables and constants, etc. In fact, since the _parameters_ to PowerShell commands are actually _properties_ of .NET classes, even parameters use PascalCase rather than camelCase. PowerShell language keywords are written in lower case (yes, even `foreach` and `dynamicparam`), as well as operators such as `-eq` and `-match`. The keywords in comment-based help are written in UPPERCASE to make it easy to spot them among the dense prose of documentation. @@ -29,31 +29,26 @@ PowerShell language keywords are written in lower case (yes, even `foreach` and function Write-Host { <# .SYNOPSIS - Writes customized output to a host. + Writes customized output to a host. .DESCRIPTION - The Write-Host cmdlet customizes output. You can specify the color of text by using - the ForegroundColor parameter, and you can specify the background color by using the - BackgroundColor parameter. The Separator parameter lets you specify a string to use to - separate displayed objects. The particular result depends on the program that is - hosting Windows PowerShell. + The Write-Host cmdlet customizes output. You can specify the color of text by using + the ForegroundColor parameter, and you can specify the background color by using the + BackgroundColor parameter. The Separator parameter lets you specify a string to use to + separate displayed objects. The particular result depends on the program that is + hosting Windows PowerShell. #> [CmdletBinding()] param( [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromRemainingArguments = $true)] - [PSObject] - $Object, + [psobject]$Object, - [Switch] - $NoNewline, + [switch]$NoNewline, - [PSObject] - $Separator, + [psobject]$Separator, - [System.ConsoleColor] - $ForegroundColor, + [System.ConsoleColor]$ForegroundColor, - [System.ConsoleColor] - $BackgroundColor + [System.ConsoleColor]$BackgroundColor ) begin { ... @@ -81,7 +76,7 @@ enum Color { function Test-Code { [CmdletBinding()] - param( + param ( [int]$ParameterOne ) end { @@ -96,9 +91,10 @@ function Test-Code { # An Exception case: Get-ChildItem | Where-Object { $_.Length -gt 10mb } ``` + The primary reason for this recommendation is practical: there are no exceptions necessary when following this rule, and when code is written following this style, _new lines_ of code can be inserted between any two lines with no risk of accidentally breaking the code by separating braces from their statement blocks. Thus, it's easier to follow, and makes errors less likely. -Because this choice was somewhat contentious in the community (about 1/3 of voters opposed), it's worth adding some addition reasonning here: First: in some historical consoles, it was necessary to write this way, so much of the early PowerShell code follows this style anyway. Second: PowerShell functions which accept scriptblocks (such as `ForEach-Object` and `Where-Object`) are common, and an _inherent_ part of the syntax of important PowerShell-based domain-specific languages such as DSC. Since it's **required** to place the opening brace on the end of the line in those cases, the only _consistent_ option is to follow OTBS. +Because this choice was somewhat contentious in the community (about 1/3 of voters opposed), it's worth adding some additional reasoning here. First, in some historical consoles, it was necessary to write this way, so much of the early PowerShell code follows this style anyway. Second, PowerShell functions which accept scriptblocks (such as `ForEach-Object` and `Where-Object`) are common, and an _inherent_ part of the syntax of important PowerShell-based domain-specific languages such as DSC. Since it's **required** to place the opening brace on the end of the line in those cases, the only _consistent_ option is to follow OTBS. #### Always Start With CmdletBinding @@ -106,16 +102,16 @@ All of your scripts or functions should start life as something like this snippe ```powershell [CmdletBinding()] -param() +param () process { } end { } ``` -You can always delete or ignore one of the blocks (or add the `begin` block), add parameters and necessary valiation and so on, but you should **avoid** writing scripts or functions without `[CmdletBinding()]`, and you should always at least _consider_ making it take pipeline input. +You can always delete or ignore one of the blocks (or add the `begin` block), add parameters and necessary validation and so on, but you should **avoid** writing scripts or functions without `[CmdletBinding()]`, and you should always at least _consider_ making it take pipeline input. -#### Prefer: param(), begin, process, end +#### Prefer: param (), begin, process, end Having a script written in the order of execution makes the intent clearer. Since there is no functional reason to have these blocks out of order (they _will_ still be executed in the normal order), writing them out of order can be confusing, and makes code more difficult to maintain and debug. @@ -155,7 +151,7 @@ Keeping lines to a small width allows scripts to be read in _one_ direction (top In this guide we use two particular sources for the maximum line width: -The PowerShell console is, by default, 120 characters wide, but it allows only 119 characters on output lines, and when entering multi-line text, PowerShell uses a line continuation prompt: `>>> ` and thus limits your line length to 116 anyway. +The PowerShell console is, by default, 120 characters wide, but it allows only 119 characters on output lines, and when entering multi-line text, PowerShell uses a line continuation prompt: `>>>` and thus limits your line length to 116 anyway. Github's current maximum line width varies between 121 and 126 depending on your browser and OS (and thus, font). However, the 115 line length suggested by PowerShell would be enough to even allow side-by-side diffs to be displayed without scrolling or wrapping on the current "standard" 1080p monitor. @@ -164,9 +160,11 @@ Again, this is a particularly flexible rule, and you should always follow the gu The preferred way to avoid long lines is to use splatting (see [Get-Help about_Splatting](https://technet.microsoft.com/en-us/library/jj672955.aspx)) and PowerShell's implied line continuation inside parentheses, brackets, and braces -- these should **always** be used in preference to the backtick for line continuation when applicable, even for strings: ```powershell -Write-Host ("This is an incredibly important, and extremely long message. " + - "We cannot afford to leave any part of it out, nor do we want line-breaks in the output. " + - "Using string concatenation lets us use short lines here, and still get a long line in the output") +Write-Host -Object ("This is an incredibly important, and extremely long message. " + + "We cannot afford to leave any part of it out, " + + "nor do we want line-breaks in the output. " + + "Using string concatenation lets us use short lines here, " + + "and still get a long line in the output") ``` #### Blank Lines and Whitespace @@ -193,7 +191,7 @@ One notable exception is when using colons to pass values to switch parameters: ```PowerShell # Do not write: -$variable=Get-Content $FilePath -Wai:($ReadCount-gt0) -First($ReadCount*5) +$variable=Get-Content $FilePath -Wait:($ReadCount-gt0) -First($ReadCount*5) # Instead write: $variable = Get-Content -Path $FilePath -Wait:($ReadCount -gt 0) -TotalCount ($ReadCount * 5) @@ -214,7 +212,7 @@ $yesterdaysDate = (Get-Date).AddDays(-1) $i = 0 $i++ -# Same principle should be applied when using a variable +# Same principle should be applied when using a variable. $yesterdaysDate = (Get-Date).AddDays(-$i) ``` diff --git a/Style-Guide/Documentation-and-Comments.md b/Style-Guide/Documentation-and-Comments.md index 51fb87b..af79fe0 100644 --- a/Style-Guide/Documentation-and-Comments.md +++ b/Style-Guide/Documentation-and-Comments.md @@ -1,8 +1,8 @@ ### Documenting and Comments -Comments that contradict the code are worse than no comments. Always make a priority of keeping the comments up-to-date when the code changes! +Comments that contradict the code are worse than no comments. Always make a priority of keeping the comments up-to-date when the code changes! -Comments should be in English, and should be complete sentences. If the comment is short, the period at the end can be omitted. +Comments should be in English, and should be complete sentences. If the comment is short, the period at the end can be omitted. Remember that comments should serve to your reasoning and decision-making, not attempt to explain what a command does. With the exception of regular expressions, well-written PowerShell can be pretty self-explanatory. @@ -14,28 +14,27 @@ $Margin = $Margin + 2 # Maybe write: # The rendering box obscures a couple of pixels. $Margin = $Margin + 2 - ``` #### Block comments Don't go overboard with comments. Unless your code is particularly obscure, don't precede each line with a comment -- doing so breaks up the code and makes it harder to read. Instead, write a single block comment. -Block comments generally apply to some or all of the code which follows them, and are indented to the same level as that code. Each line should start with a # and a single space. +Block comments generally apply to some or all of the code which follows them, and are indented to the same level as that code. Each line should start with a # and a single space. If the block is particularly long (as in the case of documentation text) it is recommended to use the `<# ... #>` block comment syntax, but you should place the comment characters on their own lines, and indent the comment: - + ```PowerShell - # Requiring a space makes things legible and prevents confusion. - # Writing comments one-per line makes them stand out more in the console. +# Requiring a space makes things legible and prevents confusion. +# Writing comments one-per line makes them stand out more in the console. - <# - .SYNOPSIS - Really long comment blocks are tedious to keep commented in single-line mode - .DESCRIPTION +<# + .SYNOPSIS + Really long comment blocks are tedious to keep commented in single-line mode. + .DESCRIPTION Particularly when the comment must be frequently edited, - as with the help and documentation for a function or script - #> + as with the help and documentation for a function or script. +#> ``` #### Inline comments @@ -47,16 +46,16 @@ They should be separated from the code statement by at least two spaces, and ide ```PowerShell $Options = @{ Margin = 2 # The rendering box obscures a couple of pixels. - Padding = 2 # We need space between the border and the text - FontSize = 24 # Keep this above 16 so it's readable in presentations + Padding = 2 # We need space between the border and the text. + FontSize = 24 # Keep this above 16 so it's readable in presentations. } ``` #### Documentation comments -Comment-based help should be written in simple language. +Comment-based help should be written in simple language. -You're not writing a thesis for your college Technical Writing class - you're writing something that describes how a function works. Avoid unecessarily large words, and keep your explanations short. You're not trying to impress anyone, and the only people who will ever read this are just trying to figure out how to use the function. +You're not writing a thesis for your college Technical Writing class - you're writing something that describes how a function works. Avoid unnecessarily large words, and keep your explanations short. You're not trying to impress anyone, and the only people who will ever read this are just trying to figure out how to use the function. If you're writing in what is, for you, a foreign language, simpler words and simpler sentence structures are better, and more likely to make sense to a native reader. @@ -74,21 +73,21 @@ If you want to provide detailed explanations about how your tool works, use the ##### Describe The Function -Every script function command should have at least a short statement describing it's function. That is the `Synopsis`. +Every script function command should have at least a short statement describing its function. That is the `Synopsis`. ##### Document Each Parameter -Each parameter should be documented. To make it easier to keep the comments synchronized with changes to the parameters, the preferred location for parameter documentation comments is _within_ the `param` block, directly above each parameter. +Each parameter should be documented. To make it easier to keep the comments synchronized with changes to the parameters, the preferred location for parameter documentation comments is _within_ the `param` block, directly above each parameter. Examples can be found in the ISE snippets: ```powershell -Param( +param ( # Param1 help description [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] $Param1, - +     # Param2 help description [int] $Param2 @@ -99,21 +98,21 @@ It is also possible to write `.PARAMETER` statements with the rest of the docume ##### Provide Usage Examples -Your help should always provide an example for each major use case. A 'usage example' is just an example of what you would type in to Powershell to run the script - you can even cut and paste one from the command line while you're testing your function. - +Your help should always provide an example for each major use case. A 'usage example' is just an example of what you would type in to PowerShell to run the script - you can even cut and paste one from the command line while you're testing your function. ```PowerShell function Test-Help { <# .SYNOPSIS - An example function to display how help should be written + An example function to display how help should be written. + .EXAMPLE Get-Help -Name Test-Help - This shows the help for the example function + This shows the help for the example function. #> [CmdletBinding()] - param( + param ( # This parameter doesn't do anything. # Aliases: MP [Parameter(Mandatory = $true)] @@ -125,9 +124,6 @@ function Test-Help { } ``` - - - ### DOC-01 Write comment-based help You should always write comment-based help in your scripts and functions. @@ -135,7 +131,7 @@ You should always write comment-based help in your scripts and functions. Comment-based help is formatted as follows: ```PowerShell -function get-example { +function Get-Example { <# .SYNOPSIS A brief description of the function or script. @@ -144,35 +140,35 @@ function get-example { A longer description. .PARAMETER FirstParameter - Description of each of the parameters + 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, +        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 + Description of each of the parameters. .INPUTS - Description of objects that can be piped to the script + Description of objects that can be piped to the script. .OUTPUTS - Description of objects that are output by the script + Description of objects that are output by the script. .EXAMPLE - Example of how to run the script + Example of how to run the script. .LINK - Links to further documentation + Links to further documentation. .NOTES - Detail on what the script does, if this is needed + Detail on what the script does, if this is needed. #> ``` -Comment-based help is displayed when the user types `help get-example` or `get-example -?`, etc. +Comment-based help is displayed when the user types `help Get-Example` or `Get-Example -?`, etc. Your help should be helpful. That is, if you've written a tool called `Get-LOBAppUser`, don't write help that merely says, "Gets LOB App Users." Duh. -**Further information:** You can get more on the use of comment-based help by typing `help about_Comment_Based_Help` within Powershell. +**Further information:** You can get more on the use of comment-based help by typing `help about_Comment_Based_Help` within PowerShell. diff --git a/Style-Guide/Function-Structure.md b/Style-Guide/Function-Structure.md index dcc3fd9..046412e 100644 --- a/Style-Guide/Function-Structure.md +++ b/Style-Guide/Function-Structure.md @@ -3,7 +3,7 @@ Avoid using the `return` keyword in your functions. Just place the object variable on its own. When declaring simple functions leave a space between the function name and the parameters. - + ```PowerShell function MyFunction ($param1, $param2) { ... @@ -12,14 +12,14 @@ function MyFunction ($param1, $param2) { ### Advanced Functions -For Advanced Functions and scripts use the format of **** for +For Advanced Functions and scripts use the format of **\-\** for naming. For a list of approved verbs the cmdlet `Get-Verb` will list them. On the noun side it can be composed of more than one joined word using Pascal Case and only singular nouns. In Advanced Functions do not use the keyword `return` to return an object. -In Advanced Functions you return objects inside the `Process {}` block +In Advanced Functions you return objects inside the `Process {}` block and not in `Begin {}` or `End {}` since it defeats the advantage of the pipeline. ```PowerShell @@ -80,9 +80,9 @@ function Get-USCitizenCapability { #### Always use CmdletBinding attribute. -#### Always have at least a `process {}` code block if any parameters takes values from the Pipeline. +#### Always have at least a `process {}` code block if any parameters takes values from the pipeline. -#### Specify an OutputType attribute if the advanced function returns an object or collection of objects. +#### Specify an OutputType attribute if the advanced function returns an object or collection of objects. If the function returns different object types depending on the parameter set provide one per parameter set. @@ -98,7 +98,7 @@ function Get-User { [CmdletBinding(DefaultParameterSetName = "ID")] [OutputType("System.Int32", ParameterSetName = "ID")] [OutputType([String], ParameterSetName = "Name")] - param ( + param ( [parameter(Mandatory = $true, ParameterSetName = "ID")] [Int[]] $UserID, @@ -106,14 +106,14 @@ function Get-User { [parameter(Mandatory = $true, ParameterSetName = "Name")] [String[]] $UserName - ) + ) <# function body #> } ``` -#### 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. +#### 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. - * **AllowNull** Validation Attribute +* **AllowNull** Validation Attribute The AllowNull attribute allows the value of a mandatory parameter to be null ($null). @@ -123,10 +123,10 @@ function Get-User { [AllowNull()] [String] $ComputerName - ) + ) ``` - * **AllowEmptyString** Validation Attribute +* **AllowEmptyString** Validation Attribute The AllowEmptyString attribute allows the value of a mandatory parameter to be an empty string (""). @@ -136,10 +136,10 @@ function Get-User { [AllowEmptyString()] [String] $ComputerName - ) + ) ``` - * **AllowEmptyCollection** Validation Attribute +* **AllowEmptyCollection** Validation Attribute The AllowEmptyCollection attribute allows the value of a mandatory parameter to be an empty collection (@()). @@ -149,15 +149,15 @@ function Get-User { [AllowEmptyCollection()] [String[]] $ComputerName - ) + ) ``` - * **ValidateCount** Validation Attribute +* **ValidateCount** Validation Attribute The ValidateCount attribute specifies the minimum and maximum number - of parameter values that a parameter accepts. Windows PowerShell + of parameter values that a parameter accepts. PowerShell generates an error if the number of parameter values in the command that - calls the function is outside that range. + calls the function is outside that range. ```PowerShell param ( @@ -165,13 +165,13 @@ function Get-User { [ValidateCount(1,5)] [String[]] $ComputerName - ) + ) ``` - * **ValidateLength** Validation Attribute +* **ValidateLength** Validation Attribute - The ValidateLength attribute specifies the minimum and maximum number - of characters in a parameter or variable value. Windows PowerShell generates an + The ValidateLength attribute specifies the minimum and maximum number + of characters in a parameter or variable value. PowerShell generates an error if the length of a value specified for a parameter or a variable is outside of the range. @@ -181,42 +181,44 @@ function Get-User { [ValidateLength(1,10)] [String[]] $ComputerName - ) + ) ``` - * **ValidatePattern** Validation Attribute +* **ValidatePattern** Validation Attribute The ValidatePattern attribute specifies a regular expression that - is compared to the parameter or variable value. Windows PowerShell generates - an error if the value does not match the regular expression - pattern. + is compared to the parameter or variable value. PowerShell generates + an error if the value does not match the regular expression + pattern. + ```PowerShell param ( [Parameter(Mandatory = $true)] [ValidatePattern("[0-9][0-9][0-9][0-9]")] [String[]] $ComputerName - ) + ) ``` - * **ValidateRange** Validation Attribute +* **ValidateRange** Validation Attribute The ValidateRange attribute specifies a numeric range for each - parameter or variable value. Windows PowerShell generates an error - if any value is outside that range. + parameter or variable value. PowerShell generates an error + if any value is outside that range. + ```PowerShell param ( [Parameter(Mandatory = $true)] [ValidateRange(0,10)] [Int] $Attempts - ) + ) ``` - * **ValidateScript** Validation Attribute +* **ValidateScript** Validation Attribute The ValidateScript attribute specifies a script that is used - to validate a parameter or variable value. Windows PowerShell + to validate a parameter or variable value. PowerShell pipes the value to the script, and generates an error if the script returns "false" or if the script throws an exception. @@ -230,13 +232,13 @@ function Get-User { [ValidateScript({$_ -ge (get-date)})] [DateTime] $EventDate - ) + ) ``` - * **ValidateSet** Attribute +* **ValidateSet** Attribute - The ValidateSet attribute specifies a set of valid values for a - parameter or variable. Windows PowerShell generates an error if a + The ValidateSet attribute specifies a set of valid values for a + parameter or variable. PowerShell generates an error if a parameter or variable value does not match a value in the set. In the following example, the value of the Detail parameter can only be "Low," "Average," or "High." @@ -247,36 +249,38 @@ function Get-User { [ValidateSet("Low", "Average", "High")] [String[]] $Detail - ) + ) ``` - * **ValidateNotNull** Validation Attribute +* **ValidateNotNull** Validation Attribute The ValidateNotNull attribute specifies that the parameter - value cannot be null ($null). Windows PowerShell generates an - error if the parameter value is null. + value cannot be null ($null). PowerShell generates an + error if the parameter value is null. The ValidateNotNull attribute is designed to be used when the type of the parameter value is not specified or when the specified - type will accept a value of Null. (If you specify a type that will + type will accept a value of null. (If you specify a type that will not accept a null value, such as a string, the null value will be rejected without the ValidateNotNull attribute, because it does not - match the specified type.) + match the specified type.) + ```PowerShell param ( [Parameter(Mandatory = $true)] [ValidateNotNull()] $ID - ) + ) ``` - * **ValidateNotNullOrEmpty** Validation Attribute +* **ValidateNotNullOrEmpty** Validation Attribute - The ValidateNotNullOrEmpty attribute specifies that the parameter + The ValidateNotNullOrEmpty attribute specifies that the parameter value cannot be null ($null) and cannot be an empty string (""). - Windows PowerShell generates an error if the parameter is used in + PowerShell generates an error if the parameter is used in a function call, but its value is null, an empty string, or an empty - array. + array. + ```PowerShell param ( [Parameter(Mandatory = $true)] @@ -285,4 +289,3 @@ function Get-User { $UserName ) ``` - diff --git a/Style-Guide/Introduction.md b/Style-Guide/Introduction.md index 6cd1366..2839065 100644 --- a/Style-Guide/Introduction.md +++ b/Style-Guide/Introduction.md @@ -4,7 +4,7 @@ In the Python community, developers have a great programming style reference provided as part of the language enhancement process specifications ([PEP-8](https://www.python.org/dev/peps/pep-0008/)), but in the PowerShell world there has been no official documentation of community preferences. -This document is an attempt to come to an agreement on a style-guide because we know that the more people follow the same set of code-style habits, the more readable the community's code will be. In other words, although the recommendations of this guide are _just recomendations_, if you follow them, you will write PowerShell code that is more easily read, understood, and maintained. +This document is an attempt to come to an agreement on a style-guide because we know that the more people follow the same set of code-style habits, the more readable the community's code will be. In other words, although the recommendations of this guide are _just recommendations_, if you follow them, you will write PowerShell code that is more easily read, understood, and maintained. ## Table of Contents @@ -13,4 +13,3 @@ This document is an attempt to come to an agreement on a style-guide because we - [Documentation and Comments](Documentation-and-Comments.md) - [Readability](Readability.md) - [Naming Conventions](Naming-Conventions.md) - diff --git a/Style-Guide/Naming-Conventions.md b/Style-Guide/Naming-Conventions.md index a0fc205..0035315 100644 --- a/Style-Guide/Naming-Conventions.md +++ b/Style-Guide/Naming-Conventions.md @@ -1,6 +1,6 @@ ### Naming Conventions -In general, prefer the use of full explicit names for commands and parameters rather than aliases or short forms. There are tools [Expand-Alias](https://github.com/PoshCode/ModuleBuilder/blob/master/ResolveAlias.psm1) for fixing many, but not all of these issues. +In general, prefer the use of full explicit names for commands and parameters rather than aliases or short forms. There are tools like [PSScriptAnalyzer](https://github.com/PowerShell/PSScriptAnalyzer)'s `Invoke-Formatter` and scripts like [Expand-Alias](https://github.com/PoshCode/ModuleBuilder/blob/master/PotentialContribution/ResolveAlias.psm1) for fixing many, but not all of these issues. #### Use the full name of each command. @@ -14,7 +14,7 @@ gps -Name Explorer Get-Process -Name Explorer ``` -#### Use full parameter names. +#### Use full parameter names. Because there are so many commands in PowerShell, it's impossible for every scripter to know every command. Therefore it's useful to be explicit about your parameter names for the sake of readers who may be unfamiliar with the command you're using. This will also help you avoid bugs if a future change to the command alters the parameter sets. @@ -28,7 +28,9 @@ Get-Process -Name Explorer #### Use full, explicit paths when possible. -When writing scripts, it's really only safe to use `..` or `.` in a path if you have previously explicitly set the location (within the script), and even then you should beware of using relative paths when calling .Net methods or legacy/native applications, because they will use the `[Environment]::CurrentDirectory` rather than PowerShell's present working directory (`$PWD`). Because checking for these types of errors is tedious (and because they are easy to over-look) it's best to avoid using relative paths altogether, and instead, base your paths off of $PSScriptRoot (the folder your script is in) when necessary. +When writing scripts, it is only safe to use `..` or `.` in a path if you have previously set the location explicitly (within the current function or script). Even if you _have_ explictly set the path, you must beware of using relative paths when calling .NET methods or legacy/native applications, because they will use `[Environment]::CurrentDirectory` which is not automatically updated to PowerShell's present working directory (`$PWD`). + +Because troubleshooting these types of errors is tedious (and they are easy to over-look), it's best to avoid using relative paths altogether, and instead, base your paths off of $PSScriptRoot (the folder your script is in) when necessary. ```PowerShell # Do not write: @@ -37,27 +39,42 @@ 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: +# Although you can write: +Push-Location $PSScriptRoot +Get-Content README.md + +# It would be better to write: +Get-Content -Path (Join-Path -Path $PSScriptRoot -ChildPath README.md) +# Or to use string concatenation: +Get-Content "$PSScriptRoot\README.md" + +# For calling .NET methods, pass full paths: [System.IO.File]::ReadAllText("$PSScriptRoot\README.md") + +# Optionally by calling Convert-Path +Push-Location $PSScriptRoot +[System.IO.File]::ReadAllText((Convert-Path README.md)) + ``` -##### Avoid the use of `~` to represent the home folder. +##### Avoid the use of `~` to represent the home folder. -The meaning of ~ is unfortunately dependent on the "current" provider at the time of execution. This isn't really a style issue, but it's an important rule for code you intend to share anyway. Instead, use `${Env:UserProfile}` or `(Get-PSProvider FileSystem).Home` ... +The meaning of ~ is unfortunately dependent on the "current" provider at the time of execution. This isn't really a style issue, but it's an important rule for code you intend to share anyway. Instead, use `${Env:UserProfile}` or `(Get-PSProvider -PSProvider FileSystem).Home`. ```PowerShell PS C:\Windows\system32> cd ~ PS C:\Users\Name> 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'". +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 - ``` - + + +#### See also the Capitalization Conventions + +In the Code Layout and Formatting chapter, there is a section on [capitalization conventions](Code-Layout-and-Formatting.md#Capitalization-Conventions). diff --git a/Style-Guide/Readability.md b/Style-Guide/Readability.md index 4179485..16605f2 100644 --- a/Style-Guide/Readability.md +++ b/Style-Guide/Readability.md @@ -34,7 +34,6 @@ foreach ($computer in $computers) { You will probably be reviled if you don't format carefully. - # READ-02 Avoid backticks Consider this: diff --git a/TableOfContents.md b/TableOfContents.md index 2798faa..2d89708 100644 --- a/TableOfContents.md +++ b/TableOfContents.md @@ -23,5 +23,5 @@ PowerShell Practice and Style Guide * [Error Handling](Best-Practices/Error-Handling.md) * [Performance](Best-Practices/Performance.md) * [Security](Best-Practices/Security.md) -* [Language, Interop and .Net](Best-Practices/Language-Interop-and-.Net.md) +* [Language, Interop and .NET](Best-Practices/Language-Interop-and-.NET.md) * [Metadata, Versioning, and Packaging](Best-Practices/Metadata-Versioning-and-Packaging.md)