├── 01. template.ps1
├── 02. demo.ps1
├── 03. Import-Configuration.ps1
├── 04. ArgumentTransformation.ps1
├── 05. Import-Configuration.ps1
├── ReadMe.md
├── export
├── images
│ ├── bg.png
│ ├── cc-by-nc-sa.png
│ ├── header.png
│ └── prompt-exception.png
├── index.html
├── libs
│ ├── highlight.js
│ │ └── 9.12.0
│ │ │ ├── darcula.css
│ │ │ ├── darkula.css
│ │ │ ├── highlight.js
│ │ │ └── reveal-code-focus-1.0.0-mod.js
│ └── reveal.js
│ │ ├── 3.7.0
│ │ ├── css
│ │ │ ├── print
│ │ │ │ └── paper.css
│ │ │ ├── reveal.css
│ │ │ └── theme
│ │ │ │ └── white.css
│ │ ├── js
│ │ │ └── reveal.orig.js
│ │ ├── lib
│ │ │ ├── css
│ │ │ │ └── zenburn.css
│ │ │ ├── font
│ │ │ │ └── source-sans-pro
│ │ │ │ │ ├── source-sans-pro-italic.woff
│ │ │ │ │ ├── source-sans-pro-regular.woff
│ │ │ │ │ ├── source-sans-pro-semibold.woff
│ │ │ │ │ └── source-sans-pro.css
│ │ │ └── js
│ │ │ │ └── head.min.js
│ │ └── plugin
│ │ │ ├── chalkboard
│ │ │ ├── chalkboard.js
│ │ │ └── img
│ │ │ │ ├── blackboard.png
│ │ │ │ ├── boardmarker.png
│ │ │ │ ├── chalk.png
│ │ │ │ └── sponge.png
│ │ │ ├── chart
│ │ │ ├── Chart.min.js
│ │ │ └── csv2chart.js
│ │ │ ├── embed-tweet
│ │ │ └── embed-tweet.js
│ │ │ ├── markdown
│ │ │ ├── markdown.js
│ │ │ └── marked.js
│ │ │ ├── math
│ │ │ ├── MathJax.js
│ │ │ ├── config
│ │ │ │ └── TeX-AMS_HTML-full.js
│ │ │ ├── jax
│ │ │ │ └── output
│ │ │ │ │ └── HTML-CSS
│ │ │ │ │ └── fonts
│ │ │ │ │ └── STIX
│ │ │ │ │ ├── General
│ │ │ │ │ └── Italic
│ │ │ │ │ │ ├── GreekItalic.js
│ │ │ │ │ │ └── MathItalic.js
│ │ │ │ │ └── fontdata.js
│ │ │ └── math.js
│ │ │ ├── menu
│ │ │ ├── menu.css
│ │ │ └── menu.js
│ │ │ ├── notes
│ │ │ └── notes.js
│ │ │ ├── search
│ │ │ └── search.js
│ │ │ ├── title-footer
│ │ │ ├── title-footer-mod.css
│ │ │ ├── title-footer.css
│ │ │ └── title-footer.js
│ │ │ └── zoom-js
│ │ │ └── zoom.js
│ │ └── font-awesome-4.7.0
│ │ ├── css
│ │ └── font-awesome.min.css
│ │ └── fonts
│ │ └── fontawesome-webfont.woff
├── markdown.md
└── web.config
├── images
├── bg.png
├── cc-by-nc-sa.png
├── header.png
└── prompt-exception.png
└── should-have-been-a-book
├── 00. abstract.md
├── 01. Help You Must Write.md
├── 02. Naming Conventions.md
├── 03. Inputs and Outputs -- Types Of Objects.md
├── 03.5 History of classes.md
├── 04. UsingTypesForParameters.md
├── 05. ValueFromPipelineByPropertyName.md
├── 06. Starting from Process.md
└── 07. Outputting errors that are unrecoverable.md
/01. template.ps1:
--------------------------------------------------------------------------------
1 | function Import-Configuration {
2 | <#
3 | .SYNOPSIS
4 | TODO
5 | .EXAMPLE
6 | TODO
7 | #>
8 | [CmdletBinding()]
9 | param(
10 | # TODO
11 | [Parameter(ValueFromPipelineByPropertyName)]
12 | $TODO
13 | )
14 | process {
15 | try {
16 | # TODO
17 | } catch {
18 | throw $_
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/02. demo.ps1:
--------------------------------------------------------------------------------
1 | # A basic prompt
2 | function prompt { "$($MyInvocation.HistoryId)|$pwd> " }
3 |
4 |
5 | # When there are errors in your prompt
6 | function prompt {
7 | Write-Error "Typo";
8 | "$($MyInvocation.HistoryId)|$pwd> " }
9 |
10 | # When there's an exception in your prompt
11 | function prompt {
12 | Write-Error "Typo"
13 | "$($MyInvocation.HistoryId)|$pwd> "
14 | throw "grenade" }
15 |
16 | # You should know to check ...
17 | $Error[0..2]
18 |
19 | # Let's look at how PowerLine handles it:
20 | Set-PowerLinePrompt
21 |
22 | # Your prompt is an array of scriptblocks
23 | $prompt
24 |
25 | # We can add an error to it
26 | Add-PowerLineBlock { Write-Error "Typo"}
27 |
28 | # We can hide that warning output:
29 | Set-PowerLinePrompt -HideErrors
30 |
31 | # Even if it's an exception!
32 | Add-PowerLineBlock { throw "grenades" }
33 |
34 | # Hiding it is probably a bad idea
35 | Set-PowerLinePrompt -HideErrors:$False
36 |
37 | # So let's look at the errors
38 | # We can see which block caused them
39 | $PromptErrors
40 |
41 | # And look more closely at the exception
42 | $PromptErrors[1] | fl * -fo
43 |
44 | # Then we can put our prompt back
45 | Remove-PowerLineBlock { Write-Error "Typo"}
46 | Remove-PowerLineBlock { throw "grenades" }
--------------------------------------------------------------------------------
/03. Import-Configuration.ps1:
--------------------------------------------------------------------------------
1 | function Import-Configuration {
2 | <#
3 | .SYNOPSIS
4 | A command to load configuration for a module
5 | .EXAMPLE
6 | $Config = Import-Configuration
7 |
8 | Load THIS module's configuration from a command
9 | .EXAMPLE
10 | $Config = Import-Configuration
11 | $Config.AuthToken = $ShaToken
12 | $Config | Export-Configuration
13 |
14 | Update a single setting in the configuration
15 | .EXAMPLE
16 | $Config = Get-Module PowerLine | Import-Configuration
17 | $Config.PowerLineConfig.DefaultAddIndex = 2
18 | Get-Module PowerLine | Export-Configuration $Config
19 |
20 | Update a single setting in the configuration
21 | #>
22 | [CmdletBinding()]
23 | param(
24 | # The module to import configuration from
25 | [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline, Mandatory)]
26 | [System.Management.Automation.PSModuleInfo]$Module
27 | )
28 | process {
29 | try {
30 | # TODO
31 | } catch {
32 | throw $_
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/04. ArgumentTransformation.ps1:
--------------------------------------------------------------------------------
1 | class ModuleInfoAttribute : System.Management.Automation.ArgumentTransformationAttribute {
2 | [object] Transform([System.Management.Automation.EngineIntrinsics]$engineIntrinsics, [object] $inputData) {
3 | $ModuleInfo = $null
4 | if ($inputData -is [string] -and -not [string]::IsNullOrWhiteSpace($inputData)) {
5 | $ModuleInfo = Get-Module $inputData -ErrorAction SilentlyContinue
6 | if (-not $ModuleInfo) {
7 | $ModuleInfo = @(Get-Module $inputData -ErrorAction SilentlyContinue -ListAvailable)[0]
8 | }
9 | }
10 | if (-not $ModuleInfo) {
11 | throw ([System.ArgumentException]"$inputData module could not be found, please try passing the output of 'Get-Module $InputData' instead")
12 | }
13 | return $ModuleInfo
14 | }
15 | }
--------------------------------------------------------------------------------
/05. Import-Configuration.ps1:
--------------------------------------------------------------------------------
1 | using namespace System.Management.Automation
2 |
3 | class ModuleInfoAttribute : ArgumentTransformationAttribute {
4 | [object] Transform([EngineIntrinsics]$engineIntrinsics, [object] $inputData) {
5 | $ModuleInfo = $null
6 | if ($inputData -is [string] -and -not [string]::IsNullOrWhiteSpace($inputData)) {
7 | $ModuleInfo = Get-Module $inputData -ErrorAction SilentlyContinue
8 | if (-not $ModuleInfo) {
9 | $ModuleInfo = @(Get-Module $inputData -ErrorAction SilentlyContinue -ListAvailable)[0]
10 | }
11 | }
12 | if (-not $ModuleInfo) {
13 | throw ([System.ArgumentException]"$inputData module could not be found, please try passing the output of 'Get-Module $InputData' instead")
14 | }
15 | return $ModuleInfo
16 | }
17 | }
18 |
19 | function Import-Configuration {
20 | <#
21 | .SYNOPSIS
22 | A command to load configuration for a module
23 | .EXAMPLE
24 | $Config = Import-Configuration
25 |
26 | Load THIS module's configuration from a command
27 | .EXAMPLE
28 | $Config = Import-Configuration
29 | $Config.AuthToken = $ShaToken
30 | $Config | Export-Configuration
31 |
32 | Update a single setting in the configuration
33 | .EXAMPLE
34 | $Config = Get-Module PowerLine | Import-Configuration
35 | $Config.PowerLineConfig.DefaultAddIndex = 2
36 | Get-Module PowerLine | Export-Configuration $Config
37 |
38 | Update a single setting in the configuration
39 | .EXAMPLE
40 | $Config = Import-Configuration -Name Powerline -CompanyName HuddledMasses.org
41 |
42 | Load the specififed module's configuration by hand
43 | #>
44 | [CmdletBinding()]
45 | param(
46 | # The module to import configuration from
47 | [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline, Mandatory)]
48 | [ModuleInfo()]
49 | [PSModuleInfo]$Module
50 | )
51 | process {
52 | try {
53 |
54 | $Path = Join-Path $Env:APPDATA (
55 | Join-Path $Module.CompanyName $Module.Name
56 | )
57 |
58 | Import-LocalizedData -BaseDirectory $Path -FileName Configuration.psd1
59 |
60 | } catch {
61 | throw $_
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/ReadMe.md:
--------------------------------------------------------------------------------
1 | ---
2 | theme: "white"
3 | transition: "slide"
4 | highlightTheme: "vs"
5 | slideNumber: true
6 | defaultTiming: 100
7 | ---
8 |
9 |
10 |
11 | # Bullet-Proofing
12 | ## Patterns & Practices
13 | #### Survivable Advanced Functions and Scripts
14 | https://github.com/Jaykul/DevOps2019
15 |
16 | Joel "Jaykul" Bennett
17 |
18 | Battle Faction
19 |
20 |
21 |
22 | note:
23 |
24 | Welcome everyone to "Bullet-proofing: Patterns and practices for survivable advanced functions and scripts" ... a presentation for the PowerShell + DevOps Global Summit, 2019. I am, of course, Joel Bennett
25 |
26 | You may know me as "Jaykul" on Twitter or on the PowerShell Slack or Discord or IRC. I've been running that online chat community for about 12 years, and I am a ten-time PowerShell MVP.
27 |
28 | Before we get started today I want to tell you something about myself: Occupationally I'm a programmer. My business cards (if I had any) would say "Senior DevOps Engineer" or sometimes just "Senior Software Engineer" or "Software Architect" but at the end of the day, I'm a Battle faction hacker. It's important that you understand that as we get into our subject for today, because we're going to be talking a lot about design, but I want you to keep in mind that the only reason we're putting this much thought into our design is to make sure that our modules are usable by _other people_, and that they will survive first contact with a newbie.
29 |
30 |
31 | ---
32 |
33 |
34 |
35 | # Disclaimer
36 |
37 | My code may not actually be bullet-proof
38 |
39 | note:
40 |
41 | I want to be clear about one thing: I can promise you, up front, not all of my own code is bullet-proof. As a battle faction hacker, the truth is that in my public code, I only rarely write things that really **need** to be bullet-proof, and as a result, if you look me up on github, you're not going to find all of these patterns followed, and you may find a lot of commands that are missing exception handling. That tends to be a side-effect of the types of modules I write, where errors are unlikely, and not critical. ;)
42 |
43 | At work, it's quite different -- we write infrastructure automation and software deployment scripts. There, I have to be much more vigilant, and we prioritize logging and error handling, and so on.
44 |
45 | ---
46 |
47 |
48 |
49 | # Survivable Code
50 |
51 | - Errors are handled appropriately
52 | - Commands make sense & work together
53 |
54 | note:
55 |
56 | To me, survivable code means two things: it's about design and about error handling. Of the two, error handling is the easiest, so we're going to talk about that right up front and get it out of the way.
57 |
58 |
59 | ---
60 |
61 |
62 |
63 | # Error Handling
64 | ## What's Appropriate?
65 | - Sometimes that means not handling
66 | - Usually that means catch and release
67 | - Normal use shouldn't produce errors
68 | - Wrap **everything** in try/catch
69 |
70 | note:
71 |
72 | Obviously in PowerShell it's relatively acceptable to let errors flow from your output, but you should always do so by catching and re-throwing, not by just ignoring. There are a lot of explanations I could get into about why, and how sometimes exception only show up when there's a try/catch, but this is not an error-handling talk, it's a patterns and practices talk so I can just say:
73 |
74 | Follow this template, and add custom handling inside it when you want to suppress errors, turn them into warnings, or convert terminating exceptions into non-terminating errors or vice-versa.
75 |
76 | --
77 |
78 |
79 |
80 | ## Code Template
81 |
82 | ```PowerShell
83 | function Test-Function {
84 | <# help here #>
85 | [CmdletBinding()]param()
86 | process {
87 | try {
88 | <# code here #>
89 | } catch {
90 | throw $_
91 | }
92 | }
93 | }
94 |
95 | ```
96 | note:
97 |
98 | There could be more to this template (and there will be, later), but for the moment, the point is to start with a try/catch wrapped around the inside of your process block (and your begin and end blocks too, if you need them)
99 |
100 | At a bare minimum, you're going to be rethrowing, to make sure that you don't get surprised by exceptions if someone wraps your code. I actually encourage you to test your code with `-ErrorAction Stop`, to help you identify potential problems.
101 |
102 | Remember that you can add additional try/catch statements inside, to wrap specific lines or handle particular errors. You can _of course_ handle particular errors even here, if you just need to customize the error message, or whatever, but this is meant to be the last stand, so you can't really do much to _recover_ here.
103 |
104 | Ok, let's look a real-world example: What happens if something in your `prompt` function has an error or throws an exception?
105 |
106 | --
107 |
108 |
109 |
110 | ## Demo 1
111 | #### Not handling errors appropriately
112 |
113 | ```PowerShell
114 | function prompt {
115 | Write-Error "Typo"
116 | "$pwd> "
117 | }
118 |
119 | function prompt {
120 | Write-Error "Typo"
121 | "$pwd> "
122 | throw "grenade"
123 | }
124 | ```
125 |
126 | What happens if something in your `prompt` function has an error or throws an exception?
127 |
128 | note:
129 |
130 | If we run these ...
131 | 1. we can see that _errors are ignored_,
132 | 2. but when the prompt throws an exception, PowerShell tosses any output it's _already gotten_ and gives you the minimalist prompt.
133 | 3. You're expected to know that this prompt means you should look in `$Error` and figure out what happened (oh, someone threw a grenade, classic).
134 |
135 | --
136 |
137 |
138 |
139 | ## Demo 2
140 | #### Handling errors appropriately
141 |
142 |
143 | ```PowerShell
144 | Set-PowerLinePrompt
145 | $prompt
146 | $prompt += { Write-Error "Typo"}
147 | $prompt += { throw "grenades" }
148 | $PromptErrors
149 | $PromptErrors[1] | Select *
150 | $prompt = $prompt[0,1]
151 | ```
152 |
153 |
154 | note:
155 |
156 | Let me show you what PowerLine does in that situation. PowerLine is my prompt module, and in it, your prompt is `$prompt`, a collection of script blocks. Here's an equivalent PowerLinePrompt, and let's see what happens when you add an exception ...
157 |
158 | You can see I actually still got my prompt! But I also got a warning and we can see that it's telling me how to _hide_ the error if I really want to do that...
159 |
160 | Of course, I don't really want to hide the errors.
161 |
162 | If I throw an exception, it gets logged right along with the error, and we can look at both of them in `$PromptErrors`. Notice that it tells us which block cause each problem, and of course, in this case, we can just remove those blocks.
163 |
164 | --
165 |
166 |
167 |
168 | ## Friends log everything
169 |
170 | ```PowerShell
171 | try {
172 | Write-Information "Enter Process Import-Configuration" -Tag Trace
173 | <# code here #>
174 | } catch {
175 | Write-Information $_ -Tag Exception
176 | throw $_
177 | }
178 | ```
179 |
180 | Invoke it with `-Iv drip`
181 |
182 | ```PowerShell
183 | $drip |
184 | Where Tag -Contains Exception |
185 | Export-CliXml exception.logx
186 | ```
187 |
188 | note:
189 |
190 | I want to encourage you to _log_ everything. When we're trying to track down a problem, it's extremely helpful if there are logable statements for each logic block -- you know what I mean, right? Within each branch of an `if`, or each statement of a `switch`, etc.
191 |
192 | There are a lot of **better** ways to log than what I'm showing you here, but if you don't have a logging solution, you could do a lot worse than writing it to the Information stream.
193 |
194 | The information stream is timestamped and sourced, and it's full of objects, so you can capture it with the `-InformationVariable` parameter and use Export-CliXml to dump it to a file. It's pretty straight-forward, and can even be used across remoting.
195 |
196 | --
197 |
198 |
199 |
200 | ## In summary
201 |
202 | - Always try/catch
203 | - Rethrow by default
204 | - Only handle specific exceptions
205 | - Always log
206 | - Especially exceptions
207 | - Even Verbose output counts
208 |
209 | note:
210 |
211 | OK, before we go back to design, I want to just summarize this a little: the point here is that you should always try/catch, even if you're just rethrowing.
212 |
213 | And (especially if you're supressing exceptions), you should log the path of execution, so when something unexpected happens, you have the ability to say: look, this is what happened...
214 |
215 | OK, Now, let's improve the design...
216 |
217 | ---
218 |
219 |
220 |
221 |
222 | # Usable Commands
223 |
224 | - Intuitive and discoverable
225 | - Play well with others
226 | - **It's about good interfaces**
227 |
228 | Let's talking about design,
229 | this is my favorite part.
230 |
231 | note:
232 |
233 | So. I told you that survivable code was about writing commands that make sense, and work together.
234 |
235 | What that means is that it's about designing good interfaces
236 |
237 | - commands people can use even without reading the help, and
238 | - commands which work well _with other commands_,
239 |
240 | Let's talk about the process.
241 |
242 | I know I said I was Battle Faction ... but the truth is I'm really never quite happy with a module until the commands can pipe into each other, and the number of nouns has been reduced as far as is comfortable. I don't worry too much about total newbies, but I want to write commands that people with some PowerShell experience can pick up and use intuitively.
243 |
244 | To design commands correctly, we have to think about how they'll be used
245 |
246 | --
247 |
248 |
249 |
250 | ## How will it be used?
251 |
252 | - How do you want to call it
253 | - What parameters do you want to pass
254 | - Where will you get those values
255 | - What are you doing with the output
256 |
257 | note:
258 |
259 | You're going to brainstorm, in a sense: How do you want to use it, or how do you think other people will use it. What commands exist which people might want to use it _with_. Where are you getting the values for your parameters? What are you doing with the output? Are you passing it to another command, formatting it for display?
260 |
261 | Now, our goal is to design the command to make these scenarios that you come up with easier.
262 |
263 | It's a good practice to start by writing down concrete examples of your answers to these questions, in pseudo code. It will help you get a feel for how you expect the command to work. When you do that, write them like this ...
264 |
265 |
266 | --
267 |
268 |
269 |
270 | ### Write down your examples ...
271 |
272 | ```PowerShell
273 | function Import-Configuration {
274 | <# .SYNOPSIS
275 | A command to load configuration for a module
276 | .EXAMPLE
277 | $Config = Import-Configuration
278 | Load THIS module's configuration from a command
279 | .EXAMPLE
280 | $Config = Import-Configuration
281 | $Config.AuthToken = $ShaToken
282 | $Config | Export-Configuration
283 |
284 | Update a single setting in the configuration
285 | #>
286 | ```
287 |
288 | note:
289 |
290 | When you start writing out the concrete examples, write them like this ...
291 |
292 | Hopefully, you recognize this as comment-based help for the command -- and I'm very serious. The first thing you should do when you start writing a command, is write the help.
293 |
294 | ---
295 |
296 |
297 |
298 | # First, write help
299 |
300 | We really require three things in the help:
301 |
302 | 1. A Synopsis and/or a short description
303 | 2. Examples -- for every parameter set
304 | 3. Documentation for each parameter
305 |
306 | note:
307 |
308 | I'm not suggesting you can write all of the help before you write the command, but ...
309 |
310 | When you start writing down your ideas about how you're going to use the command, it can help you to visualize what you're going to be doing with the command, and that helps you think about the necessary parameters, what the output needs to be, etc.
311 |
312 | I like to talk about the help you can't not write. That's three things:
313 |
314 | 1. A Synopsis
315 |
316 | First we need a synopsis or short description of the command. That's all it takes for the help system to engage, but describing it in a sentence can also help you to start thinking about the command: what it's job is, and what it's job is not.
317 |
318 | I encourage you to also write a full description, but for now, just write a synopsis (you'd probably get the description wrong anyway at this point). The synopsis is enough to get started.
319 |
320 | 2. An example -- for each parameter set
321 |
322 | Then we can write down our examples. At this stage, it's important that your examples aren't contrived. They should be the result of your brainstorming for how you want to use it. Each example should have an explanation of the purpose of using the command this way.
323 |
324 | In the simplest case, you can provide a single example (with no parameters), and a sentence explaining that this runs it with the default values (and explain what those are), and then explain what happens in that case.
325 |
326 | You don't need an example of every parameter, but you do need an example showing all of the _mandatory_ parameters for each parameter set.
327 |
328 | Now, maybe you don't know what those are yet, but these examples are long-lived, and you can update these and add more as you progress.
329 |
330 | It's might be worth saying that if you can't think of a real example for a parameter set -- you probably don't need that parameter set 😉.
331 |
332 | Long term, more examples are better, but only if they have significantly different _outcomes_. Examples showing parameters which just set properties on the output aren't necessary, because we're also going to write...
333 |
334 | 3. Parameter Documentation
335 |
336 | Documentation for each parameter. You can write this as you add parameters, by simply putting a comment above each one. In fact, I strongly recommend you do it that way (rather than using the `.PARAMETER` marker) because it's harder to forget to write and update!
337 |
338 | The next thing we're going to do is ...
339 |
340 | ---
341 |
342 |
343 |
344 | # Then write tests
345 |
346 | ## Remember this is design
347 | - Write tests as documentation
348 | - Document your intent and design
349 | - Prove your implementation works
350 |
351 | note:
352 |
353 | We're going to mostly skip over testing, because that's an entirely different talk (or two or three), but let me say this:
354 |
355 | You should approach tests as documentation. Think of them as documenting your intent, your design, and your examples, and ensuring that you don't break one of your own use cases at some point in the future.
356 |
357 | Listen: If you're not writing tests, start. Grab Pester. Write some _acceptance tests_, and read a little about behavior-driven development. Have a look at Gherkin syntax.
358 |
359 | But the bottom line is: make sure you have tests for each of the examples that we wrote above.
360 |
361 | ---
362 |
363 |
364 |
365 | # Pick good names
366 |
367 | Once you have some help and some tests in place, stop and think _again_ about naming things.
368 |
369 | This really is the most crucial part of your design.
370 |
371 | Parameter names define your user interface, but also your programming interface, affecting pipeline binding as well as discoverability.
372 |
373 | note:
374 |
375 | I know most of you spend some time thinking about what to name your commands right? What to name your functions or scripts. It's inevitable, because there are rules in PowerShell about naming.
376 |
377 | But you _should_ be spending even more time thinking about the names of your parameters, because parameter names are not just about users discovering how to use your command, they're also the interface by which commands interact with each other.
378 |
379 | --
380 |
381 |
382 |
383 | ## Remember our example
384 |
385 | - So far we have one parameter
386 | - What should I call it?
387 | - Module
388 | - ModuleInfo
389 | - PSModuleInfo
390 | - Maybe `ArgumentTransformation` for strings
391 | - What about Get-Command & Get-Module
392 |
393 | note:
394 |
395 | Show the Import-Configuration code
396 |
397 | So far we have one parameter. What should it's name be?
398 |
399 | Personally, I'm leaning toward ModuleInfo, because I think the "PS" looks like a module prefix that I should not use, and ModuleInfo makes it clear that I'm not just looking for a module _name_.
400 |
401 | However, I'm considering three things:
402 |
403 | 1. Perhaps I could write a TypeAdapter for ModuleInfo to call get-module if you pass a string name. That would mean "Module" would be a good name anyway.
404 | 2. What sorts of objects exist in PowerShell that might have a ModuleInfo as a property? CommandInfo! It turns out that the output of Get-Command has a `Module` property which would work for this -- so even if I name it "ModuleInfo", I'll need to alias it as "Module" for that to work.
405 | 3. The command that returns `PSModuleInfo` is `Get-Module` and most people probably don't know the type of object it returns.
406 |
407 | --
408 |
409 |
410 |
411 | ## Good parameter names
412 |
413 | - Recognizable and specific
414 | - Implicitly typed
415 | - Distinct
416 | - Consistent
417 |
418 | note:
419 |
420 | So what makes a good parameter name?
421 |
422 | Obviously, it's a good name if users can tell what you want! Specifically, if a user can tell what information they need to pass to each parameter --and what form the data should take-- without needing to read the help.
423 |
424 | So here are some guidelines for picking parameter names. Sometimes, these are going to cause conflicts in terms of not being able to meet all of them, but they are in priority order, and also -- you can use aliases to meet some of these goals.
425 |
426 | Parameters should be:
427 |
428 | --
429 |
430 |
431 |
432 | ### Recognizable and Specific
433 |
434 | | Good | Better |
435 | | ---- | ------ |
436 | | `$Path` | `$FilePath` or `$DirectoryPath`
437 | | `$Name` | `$FirstName` or `$FullName`
438 |
439 |
440 | Users should know which value you actually want
441 |
442 | note:
443 |
444 | Users should be able to guess what you actually want. I put some examples here -- the idea is that more specific parameter names help people know what to pass in.
445 |
446 | --
447 |
448 |
449 |
450 | ### Implicitly Typed
451 |
452 | | Good | Better |
453 | | ---- | ------ |
454 | | `$File` | `$FilePath` |
455 | | `$TimeOut` | `$TimeOutSeconds` |
456 | | `$Color` | `$ColorName` |
457 |
458 | Users should know what types they can pass
459 |
460 | note:
461 |
462 | Users should be able to guess about what type of object is needed, or what the unit of measurement is, and what format the data should take (that is, you know "Red" not the css hex value #FF0000)
463 |
464 | --
465 |
466 |
467 |
468 | ### Distinct
469 |
470 | - Save typing by reducing common prefixes
471 | - Avoid uncommon terms
472 | - Avoid similarity
473 | - Avoid duplication
474 |
475 | | Good | Better |
476 | | ---- | ------ |
477 | | `$AllowClobber`, `$AllowPreRelease` | `$IgnoreCommandName`, `$AllowPrerelease` |
478 |
479 |
480 | note:
481 |
482 | Consider what happens if I use PSReadLine's `Ctrl+Space` to list parameters (look at Install-Package as a bad example!)
483 |
484 | Multiple parameters that accept similar information in different ways might seem desireable for flexibility, but it will confuse users -- even if you put them in different parameter sets.
485 |
486 | Ideally, each parameter would start with a different letter, and be a unique way to pass a specific piece of information. Less typing is better.
487 |
488 | Here's another example: if you need a username and password, don't ask for `$UserName` and `$Password` -- ask for a `$Credential`. Don't offer both options either (that is: Credential _and_ UserName/Password). More is not better, it's just more.
489 |
490 | It's ok to limit the ways a user can invoke your command (even if it means forcing them to create a credential), if it results in a dramatically clearer interface where there's only one representation of each piece of information, and it's more obvious.
491 |
492 | --
493 |
494 |
495 |
496 | ### Consistent
497 |
498 | - Reuse parameter names ...
499 | - Match properties on output objects
500 | - Match properties on pipeline input
501 |
502 | note:
503 |
504 | Being consistent with parameter names across your module, or even parameter names on common PowerShell commands, will make it easier for users to learn and to guess based on their previous experience.
505 |
506 | Also, when we're using parameter values as output properties, try to make the names match. Your users may be already familiar with the output object, but even if they're not, they'll learn your conventions faster if the name repeats consistently.
507 |
508 | Finally, the same consideration applies to the names of properties which you want to use as input. Not only is consistecy important, it allows pipelining.
509 |
510 | Don't forget that while you _can_ use aliases to resolve pipeline inputs and even handle user expectations, but when there are too many aliases, it can lead to confusion too -- it's a lot easier for users to follow if the names match up exactly...
511 |
512 | ---
513 |
514 |
515 |
516 | # process first
517 | #### Improve performance by reducing calls
518 |
519 | - Most commands could participate in a pipeline
520 | - Use `ValueFromPipelineByPropertyName`
521 | - Or `ValueFromPipeline` (one parameter per set)
522 |
523 | This improves performance! The overhead of initializing a command is substantial.
524 |
525 | note:
526 |
527 | Once you've written your help and tests, and put some thought into parameter names, it's time to start implementing.
528 |
529 | You should start with the process block.
530 |
531 | The reality is that initializing a command is expensive (commands are objects), so it's faster to pipe multiple things to a command than to call the command multiple times.
532 |
533 | Obviously getting that improvement depends on your users calling your command that way, but you want to be able to do that.
534 |
535 | I believe most commands should be able to participate in a pipeline -- and in order for you to write commands that can, you need to put some or most of the work in the process block, and make sure that any parameters you need to use there have the `ValueFromPipelineByPropertyName` (or `ValueFromPipeline`) in their attributes.
536 |
537 | Basically, my position is that you should start by putting everything in the process block, and decorate all your parameters with `ValueFromPipelineByPropertyName`, and then remove logic from the process block as a performance optimization.
538 |
539 |
540 | --
541 |
542 |
543 |
544 | # Optimize process
545 |
546 | What can we remove from process?
547 |
548 | - Don't pre-optimize
549 | - Begin and End blocks only run once
550 | - Code there can't use pipeline parameters
551 | - Setup and teardown code
552 | - Test and validation code
553 |
554 | note:
555 |
556 | It's tempting to just leave everything in the process block, because that pretty much guarantees that the command will work the same way regardless of how it's called (with parameters or on the pipeline).
557 |
558 | However, you should always look over your code before you're ready to share it and consider whether you can move code to the `Begin` or `End` block -- anything you can do once instead of every time will improve the performance of your command when it's in the pipeline!
559 |
560 | Some obvious examples include setup and teardown code which doesn't need to be re-run each time, and which doesn't use values from your pipeline parameters can obviously be moved, but in general: re-examine your use cases! Look for parameters which you anticipate passing only as parameters, and never as pipeline values (for example, consider `-Destination` on a `Move` command), and see if you're doing anything with _just those parameters_ that could be moved to the `begin` or `end` blocks.
561 |
562 | Remember: you can't _safely_ refer to any parameter that's set as `ValueFromPipelineByPropertyName` or `ValueFromPipeline` in the `begin` block -- but you _can_ collect those values for use in the `end` block.
563 |
564 | ---
565 |
566 |
567 |
568 | # Customizing Types
569 |
570 | Consider writing Classes or setting the `PSTypeName` on your outputs.
571 |
572 | - Parameters bind to properties by name _and type_
573 | - Formatting is customized by type
574 | - Piping objects can communicate _a lot_ of data
575 |
576 |
577 | note:
578 |
579 | I want to leave you with some thoughts on custom objects.
580 |
581 | In PowerShell, everything is an object, and the [Type] of a object is fundamental to the formatting of objects on screen. I don't have time to get into the intricacies of format files and so on, but I'll make the time to say:
582 |
583 | When you're designing a set of commands that work together, you need to think beyond the function itself and think about your output objects as well. Consider what properties you need on the output, and which ones you really need to be visible by default. Consider what information you have available within each command that you might want to pass to other commands.
584 |
585 | --
586 |
587 |
588 |
589 | ## What Type of Object?
590 |
591 | - Built-in, Dynamic, Custom
592 | - Write PowerShell Classes
593 | - Write PowerShell Enums
594 | - Constrain with `[PSTypeName(...)]`
595 |
596 | note:
597 |
598 | In PowerShell we deal in three general categories of objects: the built-in objects which are part of the .NET framework, such as the FileInfo, dynamic objects (i.e. "PSCustomObject") such as those created by PowerShell when you use `Select-Object`, and custom objects defined by the functions and
599 |
600 | However, there are lots of very good reasons that you should define your own object types.
601 |
602 | 1. When you want to customize formatting, your output will need a type name
603 | 2. When you need to pass a lot of data between commands, you'll want a name for a parameter type
604 | 3. When you want interactive objects, you'll want a custom type
605 |
606 | A lof of the time, you can get away with just specifying a custom `PSTypeName` -- it's enough to let you format and even contrain inputs. However, it doesn't help users who are trying to tab-complete properties of your output objects, nor is it easy for users to create the objects to pass them as input.
607 |
608 | Why do we care about types?
609 |
610 | Probably the best interaction between functions is to take the output of one command as input to another -- but the best user experience is not necessarily an `InputObject` parameter of the specific type, sometimes it's better to accept the properties of the object as parameters. For one thing, it means that a `PSObject` will give you enough structure for pipelining. For another, it allows users to just pass values for each parameter. one much easier for users who do _not_ have the object to call your function, while still preserving the ease of use
611 |
612 | --
613 |
614 |
615 |
616 | ## Getting parameter values from the pipeline
617 |
618 | - ValueFromPipeline
619 | - Input from specific other commands
620 | - Easy custom objects
621 | - ValueFromPipelineByPropertyName
622 | - Properties from other commands
623 | - Speculatively allowed in-line
624 |
625 | note:
626 |
627 | Hopefully, you've already encountered the `[Parameter()]` attribute, and it's many switches. Two of them allow you to collect the value of the parameter from pipeline input:
628 |
629 | - ValueFromPipeline allows you to create an `$InputObject` sort of parameter to collect each object. It's a good fit for when you only want to accept the output from one of your other functions, or when your objects are easy to construct (e.g have default constructors so you can easily build them from hashtables).
630 |
631 | - ValueFromPipelineByPropertyName allows you to collect the value of a single property from each object. Of course, you can set up multiple parameters like this to collect multiple properties. This is a good fit when you don't have a specific object in mind, or when you only need the key identifier from it (e.g. `PSPath` for files).
632 |
633 | ---
634 |
635 |
636 |
637 | # Thank You
638 | Please use the event app to submit a session rating
639 |
640 |
641 |
642 | https://github.com/Jaykul/DevOps2019
643 |
644 | If you have good things to say,
645 | I'm Joel Bennett
646 |
647 | Otherwise, I'm Kirk Munro 😉
648 |
649 |
650 |
651 |
652 |
--------------------------------------------------------------------------------
/export/images/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jaykul/DevOps2019/b371706c12c6fcc589385ed60d0647a90a889a49/export/images/bg.png
--------------------------------------------------------------------------------
/export/images/cc-by-nc-sa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jaykul/DevOps2019/b371706c12c6fcc589385ed60d0647a90a889a49/export/images/cc-by-nc-sa.png
--------------------------------------------------------------------------------
/export/images/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jaykul/DevOps2019/b371706c12c6fcc589385ed60d0647a90a889a49/export/images/header.png
--------------------------------------------------------------------------------
/export/images/prompt-exception.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jaykul/DevOps2019/b371706c12c6fcc589385ed60d0647a90a889a49/export/images/prompt-exception.png
--------------------------------------------------------------------------------
/export/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |