From f19883de2abc4413bd4449b372ff566066d5eda3 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 03:26:28 +0100 Subject: [PATCH 01/28] Make .NET written consistently the same throughout --- Style-Guide/Code-Layout-and-Formatting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Style-Guide/Code-Layout-and-Formatting.md b/Style-Guide/Code-Layout-and-Formatting.md index a39790d..b66b2b2 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. From 8555e2121934dcd1da435f2f6245c5cc17bde1bd Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 22:26:05 +0100 Subject: [PATCH 02/28] Change 'addition' to 'additional'. --- Style-Guide/Code-Layout-and-Formatting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Style-Guide/Code-Layout-and-Formatting.md b/Style-Guide/Code-Layout-and-Formatting.md index b66b2b2..76aaa41 100644 --- a/Style-Guide/Code-Layout-and-Formatting.md +++ b/Style-Guide/Code-Layout-and-Formatting.md @@ -94,7 +94,7 @@ 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 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. +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 From b9dd96e37ba655634bd31ae989ec99fb457475c5 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 22:28:07 +0100 Subject: [PATCH 03/28] Change some inconsistent punctuation marks. --- Style-Guide/Code-Layout-and-Formatting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Style-Guide/Code-Layout-and-Formatting.md b/Style-Guide/Code-Layout-and-Formatting.md index 76aaa41..84e5598 100644 --- a/Style-Guide/Code-Layout-and-Formatting.md +++ b/Style-Guide/Code-Layout-and-Formatting.md @@ -94,7 +94,7 @@ 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 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. +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 From 410532aac3b10b6a7eb315e649c42b1a745fe7c0 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 22:50:45 +0100 Subject: [PATCH 04/28] Change 'Windows PowerShell' to 'PowerShell'. As Windows is not the only OS PowerShell can run on, having 'Windows' in the name is not very practical anymore. That's why, starting with version 6, Microsoft decided to change 'Windows PowerShell' to 'PowerShell'. The text now follows this. --- Style-Guide/Function-Structure.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Style-Guide/Function-Structure.md b/Style-Guide/Function-Structure.md index 9af1e8b..ecef736 100644 --- a/Style-Guide/Function-Structure.md +++ b/Style-Guide/Function-Structure.md @@ -155,7 +155,7 @@ function Get-User { * **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. @@ -171,7 +171,7 @@ function Get-User { * **ValidateLength** Validation Attribute The ValidateLength attribute specifies the minimum and maximum number - of characters in a parameter or variable value. Windows PowerShell generates an + 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. @@ -187,7 +187,7 @@ function Get-User { * **ValidatePattern** Validation Attribute The ValidatePattern attribute specifies a regular expression that - is compared to the parameter or variable value. Windows PowerShell generates + is compared to the parameter or variable value. PowerShell generates an error if the value does not match the regular expression pattern. @@ -203,7 +203,7 @@ function Get-User { * **ValidateRange** Validation Attribute The ValidateRange attribute specifies a numeric range for each - parameter or variable value. Windows PowerShell generates an error + parameter or variable value. PowerShell generates an error if any value is outside that range. ```PowerShell @@ -218,7 +218,7 @@ function Get-User { * **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. @@ -238,7 +238,7 @@ function Get-User { * **ValidateSet** Attribute The ValidateSet attribute specifies a set of valid values for a - parameter or variable. Windows PowerShell generates an error if 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." @@ -255,7 +255,7 @@ function Get-User { * **ValidateNotNull** Validation Attribute The ValidateNotNull attribute specifies that the parameter - value cannot be null ($null). Windows PowerShell generates an + 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 @@ -277,7 +277,7 @@ function Get-User { 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. From 10e35e6a2ecd4ee5ae77236aae3b6af7b69efec0 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 22:53:26 +0100 Subject: [PATCH 05/28] Change 'Null' to 'null' to keep it consistent. --- Style-Guide/Function-Structure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Style-Guide/Function-Structure.md b/Style-Guide/Function-Structure.md index ecef736..87d9a3b 100644 --- a/Style-Guide/Function-Structure.md +++ b/Style-Guide/Function-Structure.md @@ -260,7 +260,7 @@ function Get-User { 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.) From 5f7cf65a211af3d25cb13c1e253c821bf19dd0d3 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 22:59:20 +0100 Subject: [PATCH 06/28] Change "it's" to "its" as it is used possessively. --- Style-Guide/Documentation-and-Comments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Style-Guide/Documentation-and-Comments.md b/Style-Guide/Documentation-and-Comments.md index d0ad3e2..70342b6 100644 --- a/Style-Guide/Documentation-and-Comments.md +++ b/Style-Guide/Documentation-and-Comments.md @@ -73,7 +73,7 @@ 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 From 59b660dfd5fc8fc002b0ee2b9c3c710632c81c58 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 23:03:50 +0100 Subject: [PATCH 07/28] Change 'Powershell' to 'PowerShell'. Two times 'Powershell' was written in this text instead of 'PowerShell', which makes them inconsistent with the rest of the text where it was properly written as 'PowerShell'. --- Style-Guide/Documentation-and-Comments.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Style-Guide/Documentation-and-Comments.md b/Style-Guide/Documentation-and-Comments.md index 70342b6..af79fe0 100644 --- a/Style-Guide/Documentation-and-Comments.md +++ b/Style-Guide/Documentation-and-Comments.md @@ -98,7 +98,7 @@ 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 { @@ -171,4 +171,4 @@ Comment-based help is displayed when the user types `help Get-Example` or `Get-E 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. From 7539a35b0d111966907d50fe82ccb1d7318445dd Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 23:11:57 +0100 Subject: [PATCH 08/28] Change '.Net' to '.NET'. The proper name of '.NET' is '.NET', all capitals, not '.Net'. --- Style-Guide/Naming-Conventions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Style-Guide/Naming-Conventions.md b/Style-Guide/Naming-Conventions.md index b65a14f..05cf924 100644 --- a/Style-Guide/Naming-Conventions.md +++ b/Style-Guide/Naming-Conventions.md @@ -28,7 +28,7 @@ Get-Process -Name Explorer #### Use full, explicit paths when possible. -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`). +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. From ecd84a1c8359d671bd8065e8b9ac645959de08f6 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 23:12:53 +0100 Subject: [PATCH 09/28] Add missing comma to make the sentence correct. --- Style-Guide/Naming-Conventions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Style-Guide/Naming-Conventions.md b/Style-Guide/Naming-Conventions.md index 05cf924..d6314b8 100644 --- a/Style-Guide/Naming-Conventions.md +++ b/Style-Guide/Naming-Conventions.md @@ -30,7 +30,7 @@ Get-Process -Name Explorer 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. +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: From c7defc81fffbe1ec72b2b6528dc13b1cc81493a6 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 23:13:58 +0100 Subject: [PATCH 10/28] Change '.net' to '.NET'. --- Style-Guide/Naming-Conventions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Style-Guide/Naming-Conventions.md b/Style-Guide/Naming-Conventions.md index d6314b8..5a52224 100644 --- a/Style-Guide/Naming-Conventions.md +++ b/Style-Guide/Naming-Conventions.md @@ -49,7 +49,7 @@ 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: +# For calling .NET methods, pass full paths: [System.IO.File]::ReadAllText("$PSScriptRoot\README.md") # Optionally by calling Convert-Path From 04bcaf2e676ffcb7caf83979308a9944bec56ed9 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 23:15:25 +0100 Subject: [PATCH 11/28] Change '...' to '.'. --- Style-Guide/Naming-Conventions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Style-Guide/Naming-Conventions.md b/Style-Guide/Naming-Conventions.md index 5a52224..0035315 100644 --- a/Style-Guide/Naming-Conventions.md +++ b/Style-Guide/Naming-Conventions.md @@ -60,7 +60,7 @@ Push-Location $PSScriptRoot ##### 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 -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 ~ From 454dfe464ecfb2d43f6e5a3d3e741c018ad89e35 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 23:32:06 +0100 Subject: [PATCH 12/28] Change 'when' to 'which'. --- Best-Practices/Building-Reusable-Tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Best-Practices/Building-Reusable-Tools.md b/Best-Practices/Building-Reusable-Tools.md index 964fe7f..9f0774c 100644 --- a/Best-Practices/Building-Reusable-Tools.md +++ b/Best-Practices/Building-Reusable-Tools.md @@ -2,7 +2,7 @@ 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. +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 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 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. From 59ac64665085a354e32bd3f47d5c15c79e61f7d9 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 23:38:28 +0100 Subject: [PATCH 13/28] Change non-existent words. Change 're-use' to 'reuse' and 're-used' to 'reused', as these words are written as such in both Merriam-Webster and the Cambridge Dictionary. Furthermore, in another location in the text the word was already written as 'reuse' and 'reusable', so it was also inconsistent. --- Best-Practices/Building-Reusable-Tools.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Best-Practices/Building-Reusable-Tools.md b/Best-Practices/Building-Reusable-Tools.md index 9f0774c..7729cba 100644 --- a/Best-Practices/Building-Reusable-Tools.md +++ b/Best-Practices/Building-Reusable-Tools.md @@ -2,7 +2,7 @@ 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, 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 re-use. +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 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. @@ -14,7 +14,7 @@ Controllers, on the other hand, often produce output directly to the screen (whe 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 From fbb9f5543c3bd7323a15d9785d49b1a5ee2ef08e Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 23:39:06 +0100 Subject: [PATCH 14/28] Add dot to the end of a sentence. --- Best-Practices/Building-Reusable-Tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Best-Practices/Building-Reusable-Tools.md b/Best-Practices/Building-Reusable-Tools.md index 7729cba..a07ae38 100644 --- a/Best-Practices/Building-Reusable-Tools.md +++ b/Best-Practices/Building-Reusable-Tools.md @@ -3,7 +3,7 @@ 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, 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 script is not intended to be reusable; it is intended to make use of reuse by leveraging functions and other commands +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. 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. From f0dc492567df645799dab822e42337c9ebe188f1 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 23:45:27 +0100 Subject: [PATCH 15/28] Add 'controller'. In the explanation about controller scripts, it read 'A script is not intended to be reusable', which made no sense to me, so I changed it to 'A controller script is not intended to be reusable'. --- Best-Practices/Building-Reusable-Tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Best-Practices/Building-Reusable-Tools.md b/Best-Practices/Building-Reusable-Tools.md index a07ae38..833a77a 100644 --- a/Best-Practices/Building-Reusable-Tools.md +++ b/Best-Practices/Building-Reusable-Tools.md @@ -3,7 +3,7 @@ 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, 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 script is not intended to be reusable; it is intended to make use of reuse by leveraging functions and other commands. +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. From 58e9ab6fe75033f3b5ca1d84b7a3f42702181374 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 23:52:54 +0100 Subject: [PATCH 16/28] Delete 'on the other hand'. In the example paragraph under TOOL-01, it is clear a controller script is being explained. In the paragraph following that, a further remark is made about controller scripts, but it read 'Controllers, on the other hand, (...)', which seems to imply the paragraph before that was talking about a tool script, which wasn't the case (it was talking about a controller script helped by several tool scripts, so tool scripts were also mentioned, but the paragraph ends with two sentences about the controller script, so 'on the other hand' in the next paragraph is incorrect and confusing IMHO). --- Best-Practices/Building-Reusable-Tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Best-Practices/Building-Reusable-Tools.md b/Best-Practices/Building-Reusable-Tools.md index 833a77a..963d667 100644 --- a/Best-Practices/Building-Reusable-Tools.md +++ b/Best-Practices/Building-Reusable-Tools.md @@ -7,7 +7,7 @@ For this discussion, it's important to have some agreed-upon terminology. While 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 From 2e1d9360778747ddae0c8dcd113fbe1f23094798 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 23:54:55 +0100 Subject: [PATCH 17/28] Delete inconsistent line spacing. Some paragraphs were separated by one empty line, some by two. The second empty line at several places has been deleted. --- Best-Practices/Building-Reusable-Tools.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Best-Practices/Building-Reusable-Tools.md b/Best-Practices/Building-Reusable-Tools.md index 963d667..5642d8f 100644 --- a/Best-Practices/Building-Reusable-Tools.md +++ b/Best-Practices/Building-Reusable-Tools.md @@ -9,14 +9,12 @@ For example, you might write a "New-CorpUser" script, which provisions new users 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 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 Tools should accept input from parameters and should (in most cases) produce any output to the pipeline; this approach helps maximize reusability. @@ -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,15 +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 [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. @@ -103,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. - From a28f24f50e26e18c7d226710ae8856451601c466 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sat, 28 Jan 2023 23:57:00 +0100 Subject: [PATCH 18/28] Add dot to sentences. At two locations, a dot was missing at the end of the sentence. This has been corrected. --- Best-Practices/Building-Reusable-Tools.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Best-Practices/Building-Reusable-Tools.md b/Best-Practices/Building-Reusable-Tools.md index 5642d8f..227791e 100644 --- a/Best-Practices/Building-Reusable-Tools.md +++ b/Best-Practices/Building-Reusable-Tools.md @@ -13,7 +13,7 @@ Controllers often produce output directly to the screen (when designed for inter 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 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 +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,7 +29,7 @@ You can get a list of the verbs by typing 'get-verb' at the command line. Tools should be consistent with PowerShell native cmdlets in regards 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 From fa28ceaed07a027eb5301b45d14b1ecde52b7ebd Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sun, 29 Jan 2023 00:00:27 +0100 Subject: [PATCH 19/28] Correct 'in regards'. In the text it was written '(..) in regards parameter naming'. This is incorrect, according to both Merriam-Webster and the Cambridge Dictionary: it is 'in regard to'. This has been corrected. --- Best-Practices/Building-Reusable-Tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Best-Practices/Building-Reusable-Tools.md b/Best-Practices/Building-Reusable-Tools.md index 227791e..47c5fa9 100644 --- a/Best-Practices/Building-Reusable-Tools.md +++ b/Best-Practices/Building-Reusable-Tools.md @@ -27,7 +27,7 @@ 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. From dfb06f4f55b21f1d133ce15987368bb005306361 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sun, 29 Jan 2023 03:09:41 +0100 Subject: [PATCH 20/28] Write 'pipeline' consistently throughout. At one location 'Pipeline' was written and everywhere else 'pipeline', which is inconsistent, so 'Pipeline' has been changed to 'pipeline'. --- Style-Guide/Function-Structure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Style-Guide/Function-Structure.md b/Style-Guide/Function-Structure.md index 87d9a3b..046412e 100644 --- a/Style-Guide/Function-Structure.md +++ b/Style-Guide/Function-Structure.md @@ -80,7 +80,7 @@ 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. From 845f55ef87dca1c8d57de0b2f9ef0f5ec402f9cd Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sun, 16 Apr 2023 10:45:14 +0200 Subject: [PATCH 21/28] Change few typos Changed some typos like 'a interactions' and the use of 'i.e.' (misspelled as 'ie'), which means 'that is', while 'e.g.' seemed to be the correct abbreviation in that context, which means 'for example'. --- Best-Practices/Output-and-Formatting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Best-Practices/Output-and-Formatting.md b/Best-Practices/Output-and-Formatting.md index 702fa5a..27a84a6 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. From 4ef6c3158439e9b48858ab16b8493573ff7ffee5 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sun, 16 Apr 2023 10:46:36 +0200 Subject: [PATCH 22/28] Update Output-and-Formatting.md --- Best-Practices/Output-and-Formatting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Best-Practices/Output-and-Formatting.md b/Best-Practices/Output-and-Formatting.md index 27a84a6..10798cf 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 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 (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. +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. From d82198f3a0dc37264a7576cd4446467c2d1d5730 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sun, 16 Apr 2023 10:48:15 +0200 Subject: [PATCH 23/28] Update Output-and-Formatting.md --- Best-Practices/Output-and-Formatting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Best-Practices/Output-and-Formatting.md b/Best-Practices/Output-and-Formatting.md index 10798cf..d1a7fc0 100644 --- a/Best-Practices/Output-and-Formatting.md +++ b/Best-Practices/Output-and-Formatting.md @@ -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 From 670a534612fa46b10e25978cd1cd85bf366cdbf5 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sun, 16 Apr 2023 11:08:16 +0200 Subject: [PATCH 24/28] Update Performance.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add space between 'foreach' and the following parenthesis. - Change the faulty 'a la' to the correct 'à la'. - Write '.NET' and '.NET Framework' consistently the same throughout. --- Best-Practices/Performance.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Best-Practices/Performance.md b/Best-Practices/Performance.md index 8657cab..cc2ae91 100644 --- a/Best-Practices/Performance.md +++ b/Best-Practices/Performance.md @@ -7,7 +7,7 @@ If you're aware of multiple techniques to accomplish something, and you're writi For example: ```PowerShell -foreach($result in Do-Something) { $result.PropertyOne + $result.PropertyTwo } +foreach ($result in Do-Something) { $result.PropertyOne + $result.PropertyTwo } Do-Something | ForEach-Object { $_.PropertyOne + $_.PropertyTwo } ``` @@ -40,7 +40,7 @@ ForEach-Object -Process { } ``` -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: @@ -73,8 +73,8 @@ The moral here is that both aesthetic and performance are important consideratio 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 +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**. From 947784215347ed7dd33ddca45a0f6e86b342d067 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sun, 16 Apr 2023 11:09:10 +0200 Subject: [PATCH 25/28] Update Performance.md Delete extra line. --- Best-Practices/Performance.md | 1 - 1 file changed, 1 deletion(-) diff --git a/Best-Practices/Performance.md b/Best-Practices/Performance.md index cc2ae91..0b0a50e 100644 --- a/Best-Practices/Performance.md +++ b/Best-Practices/Performance.md @@ -78,4 +78,3 @@ This is just a rough guideline, but as a general rule: 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**. - From e06936a06313304d2c4bd78b54f67af57d1edae1 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sun, 16 Apr 2023 11:19:07 +0200 Subject: [PATCH 26/28] Update Security.md - Change '.Net' to '.NET' to keep it consistent. - Change incorrect usage of spaces within example code (space after opening parenthesis and space before closing parenthesis). - Change several inconsistencies in example code: - Change '[System.Runtime.InteropServices.marshal]' to '[System.Runtime.InteropServices.Marshal]'. - Remove semicolon at the end of the lines. - Remove an unnecessary 'return' keyword. - Remove extra line at the end. --- Best-Practices/Security.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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 ``` - From 6fbe21fe14b8d7faf60a61e20e2c1b8cb8cd013e Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sun, 16 Apr 2023 12:23:25 +0200 Subject: [PATCH 27/28] Update TODO.md - Change '.Net' to '.NET' to keep it consistent. - Remove several spurious blank lines. - Change 'PowerShell Classes' to 'PowerShell classes'. - Change "'s" to "or". - Change 'makes sense more sense' to 'makes more sense'. - Move 'param()' in '[CmdletBinding()]param()' to the next line. - Change 'comment based help' to 'comment-based help'. - Change 'a about_ModuleName' to 'an about_ModuleName'. - Change 'that pases parameters positionally' to 'that passes parameters positionally'. - Change 'PsCmdlet.ThrowTerminatingError' to '$PSCmdlet.ThrowTerminatingError'. - Change 'PsCmdlet.WriteError' to '$PSCmdlet.WriteError'. - Change inconsistencies with the styling guide/conventions in the example code for function ThrowError: - Change typo 'errorrecord' to 'error record'. - Move brace after 'function ThrowError' to the same line. - Move parenthesis after 'param' to the same line. - Change 'parameter' to 'Parameter'. - Change '$exception = New-Object $ExceptionName $ExceptionMessage;' to '$exception = New-Object -TypeName $ExceptionName -ArgumentList $ExceptionMessage' (so using named parameters instead of positional parameters and removing the unnecessary semicolon at the end). - Change 'Discuss: when is this critical (-whatif) and optional (-confirm_' to 'Discuss: when is this critical (-WhatIf) and optional (-Confirm)'. - Change 'Discuss: when should you call PSCmdlet.ShouldProcess vs PSCmdlet.ShouldContinue (-Force)' to 'Discuss: when should you call $PSCmdlet.ShouldProcess vs $PSCmdlet.ShouldContinue (-Force)'. - Change more inconsistencies in example code: - Remove unnecessary semicolons at the end of the line. - Add space after 'foreach'. - Remove inconsistent spacing. - Add space after 'if'. - Change more small typos. P.S.: Make '$errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $ErrorId, $ErrorCategory, $ExceptionObject' code that actually works! This code will generate the error **New-Object: Cannot bind argument to parameter 'TypeName' because it is null.**! --- Best-Practices/TODO.md | 104 +++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 60 deletions(-) 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 From 3a00c9018364a9c8502ec94c26b1a7c424516710 Mon Sep 17 00:00:00 2001 From: Richard Vrijhof Date: Sun, 16 Apr 2023 12:47:29 +0200 Subject: [PATCH 28/28] Update Writing-Parameter-Blocks.md - Change "what it's expected or allowed values are" to "what its expected or allowed values are" as clearly "it's" cannot mean 'it is' or 'it has' in this context and is thus incorrect. - Change 'You should support --whatif' to 'You should support -WhatIf' as that's the correct casing. Also, the double dash has been changed to a single dash because this is PowerShell, not sh, bash, zsh or something like that. - Remove the unnecessary semicolon at the end of the line in the example code. - Change some small typos and remove more than one blank line in several places. P.S. Consider using single quotes for strings with constant values instead of double quotes! Using PSScriptAnalyzer will catch these! By the way, consider recommending using PSScriptAnalyzer, because using that will generally improve one's code significantly! --- Best-Practices/Writing-Parameter-Blocks.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Best-Practices/Writing-Parameter-Blocks.md b/Best-Practices/Writing-Parameter-Blocks.md index 63f3133..4718f6f 100644 --- a/Best-Practices/Writing-Parameter-Blocks.md +++ b/Best-Practices/Writing-Parameter-Blocks.md @@ -36,7 +36,7 @@ function Test-Help { ``` ### Always Document Every Parameter -You should always provide at least a brief explanation of each parameter, what it's expected or allowed values are, etc. +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. @@ -50,9 +50,9 @@ There are a few specific advanced cases where you might want to write an old-fas 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 +## 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`). +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: @@ -63,13 +63,13 @@ Here's an example of what that might look like: param([switch]$Force) # You need to pre-define these (because they're passed by [ref]) -$RejectAll = $false; -$ConfirmAll = $false; +$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 or equal to the ConfirmImpact in the cmdlet binding (default Medium) + # 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))?", @@ -93,13 +93,11 @@ foreach ($service in Get-Service | Where Status -eq "Running") { 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, you should be _at least_ as strongly typed as the other command, to avoid casting exceptions within your script. - +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]`) @@ -122,9 +120,8 @@ Parameters of type `[switch]` support passing as switches (without a value), and - 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. \ No newline at end of file +Don't forget that values set from the pipeline are only available in the `process` block.