-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfeed.xml
1253 lines (934 loc) · 167 KB
/
feed.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.8.5">Jekyll</generator><link href="https://chrislgardner.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://chrislgardner.dev/" rel="alternate" type="text/html" /><updated>2019-07-18T02:40:53-05:00</updated><id>/feed.xml</id><title type="html">Get-RandomProblems</title><subtitle>A selection of the various problems I've encountered while working in PowerShell on various projects and the solutions I've developed to resolve them.
</subtitle><entry><title type="html">Git bisect and PowerShell</title><link href="https://chrislgardner.dev/powershell/2019/07/17/git-bisect-and-powershell.html" rel="alternate" type="text/html" title="Git bisect and PowerShell" /><published>2019-07-16T16:00:00-05:00</published><updated>2019-07-16T16:00:00-05:00</updated><id>/powershell/2019/07/16/git-bisect-and-powershell</id><content type="html" xml:base="/powershell/2019/07/16/git-bisect-and-powershell.html"><p>Inevitably when writing code there will be bugs, it’s just part of being human and writing code and especially true when the code gets complex. So when it occurs we need to track it down and fix it, which can be easy, but we often want to track down where the bug was introduced so we can figure out what caused it (especially for the more difficult to pinpoint bugs). As we’re all using source control this becomes easier with git and it’s extremely powerful bisect tool.</p>
<!--more-->
<h2 id="what-is-git-bisect">What is git bisect?</h2>
<p>To quote the <a href="https://git-scm.com/docs/git-bisect">official documentation</a> “git-bisect - Use binary search to find the commit that introduced a bug”, which to the average person doesn’t mean a lot. The simple description is it takes two points you specify and splits them down the middle, picks a side and splits it again, doing this until it finds where the bad commit is. The way it figures out which side to pick can be done in two ways, either by you manually telling it “good” or “bad” or it can automatically figure that out for you if you give it something to run. It’s this last part we’ll be looking at in more detail here but we’ll look at the manual approach first.</p>
<h2 id="git-bisect-in-action">git bisect in action</h2>
<p>First we’ll need a git repository to work with, <a href="https://github.com/ChrisLGardner/gitbisect">here’s one I prepared earlier</a> that includes a function that has been changed over the course of a number of commits. This repository has a bit of history to it, looking at the log with <code class="highlighter-rouge">git log --oneline</code> we can see that it started out quite simple and extra functionality has been added over time, we’ve also got some less than useful commit messages that we should probably speak to the dev about and try to get them writing better messages in future but that’s a different problem. The function in example.ps1 is pretty simple and just does some simple data gathering, either from a local computer or a remote one, nothing too special and it’s been working for a while.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="k">function </span>Get-Data <span class="o">{</span>
<span class="k">param</span> <span class="o">(</span>
<span class="nv">$ServerName</span> <span class="o">=</span> <span class="nv">$Env</span>:COMPUTERNAME,
<span class="nv">$Username</span> <span class="o">=</span> <span class="s1">'test'</span>
<span class="o">)</span>
<span class="k">if</span> <span class="o">(</span><span class="nv">$ServerName</span> -eq <span class="nv">$Env</span>:COMPUTERNAME<span class="o">)</span> <span class="o">{</span>
<span class="nv">$ComputerInfo</span> <span class="o">=</span> <span class="nb">Get-ComputerInfo</span>
<span class="o">[</span>PSCustomObject]@<span class="o">{</span>
Name <span class="o">=</span> <span class="nv">$Env</span>:COMPUTERNAME
OS <span class="o">=</span> <span class="nv">$ComputerInfo</span>.OSName
IPAddress <span class="o">=</span> <span class="o">(</span><span class="nb">Get-NetIPAddress</span> -AddressFamily IPv4 -InterfaceAlias <span class="s1">'Ethernet'</span><span class="o">)</span>.IPAddress
Domain <span class="o">=</span> <span class="nv">$ComputerInfo</span>.CsDomain
Model <span class="o">=</span> <span class="nv">$ComputerInfo</span>.Model
Manufacturer <span class="o">=</span> <span class="nv">$ComputerInfo</span>.Manufacturer
<span class="o">}</span>
<span class="o">}</span>
<span class="k">else</span> <span class="o">{</span>
Invoke-Command -ComputerName <span class="nv">$ComputerName</span> -ScriptBlock <span class="o">{</span>
<span class="nv">$ComputerInfo</span> <span class="o">=</span> <span class="nb">Get-ComputerInfo</span>
<span class="o">[</span>PSCustomObject]@<span class="o">{</span>
Name <span class="o">=</span> <span class="nv">$Env</span>:COMPUTERNAME
OS <span class="o">=</span> <span class="nv">$ComputerInfo</span>.OSName
IPAddress <span class="o">=</span> <span class="o">(</span><span class="nb">Get-NetIPAddress</span> -AddressFamily IPv4 -InterfaceAlias <span class="s1">'Ethernet'</span><span class="o">)</span>.IPAddress
Domain <span class="o">=</span> <span class="nv">$ComputerInfo</span>.CsDomain
Model <span class="o">=</span> <span class="nv">$ComputerInfo</span>.Model
Manufacturer <span class="o">=</span> <span class="nv">$ComputerInfo</span>.Manufacturer
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span></code></pre></figure>
<p>We’ve been informed by a colleague that it’s stopped querying remote machines at some point. Looking at the current version of the script it’s pretty obvious what the problem is, the parameter being passed in is $ServerName but the Invoke-Command is trying to connect to $ComputerName, that’ll not work out well for us but it’s an easy fix to revert everything to ComptuerName since that’s the convention in PowerShell (we can add an alias for ServerName if necessary). But now we need to know when this bug was introduced, in our case it’s an easy fix but in many it won’t be so obvious and it’s useful to know what else might be impacted by this change if it was made at the same time as others. This is where <code class="highlighter-rouge">git bisect</code> comes in.</p>
<p>Let’s look at the manual way first, we’ll need to start a bisect session:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nb">PS</span> <span class="o">&gt;</span> git bisect <span class="nb">start</span></code></pre></figure>
<p>Then we need to tell it which commit is bad and which is good, we’ll start with bad because we know what that is:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nb">PS</span> <span class="o">&gt;</span> git bisect bad</code></pre></figure>
<p>In this case we can also use <code class="highlighter-rouge">git bisect bad HEAD</code> to point at the current commit or give it part of the sha for a specific commit if we’ve already fixed the issue on this branch.</p>
<p>Next up we need to tell git which commit we know is good, this can be either a specific commit sha or it can be relative to the current HEAD position:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nb">PS</span> <span class="o">&gt;</span> git bisect good 6b98
Bisecting: 2 revisions left to <span class="nb">test </span>after this <span class="o">(</span>roughly 1 step<span class="o">)</span>
<span class="o">[</span>e3eb782ce10f59c9bf10626ecf7c00f3b89e5344] fix other thing</code></pre></figure>
<p>In our case we know that the first commit that added remote computer support was good so we’ll use the first few characters of the sha for that. You can provide as many characters as you want from the sha of that commit but I find 4 is usually enough on most code bases, if you’ve got one with a lot of history then you might need to use 5 or more.</p>
<p>As we can see from the output git has already picked a commit somewhere close to the middle of the distance between the commits we’ve specified. From here we can test the code and see if we’re still seeing the error. As we know that we are then we can tell git that this is a bad commit and to look for another commit to try somewhere between here and the known good commit.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nb">PS</span> <span class="o">&gt;</span> git bisect bad
Bisecting: 0 revisions left to <span class="nb">test </span>after this <span class="o">(</span>roughly 0 steps<span class="o">)</span>
<span class="o">[</span>e4886e8a59c1a921d149b7948eca0954d658b050] fix parameter name</code></pre></figure>
<p>Now git has picked another commit but looking at the function we can see it’s also bad in this one. Git tries to predict how many more commits it’s got to check before it finds the issue, and given the short amount of history we’ve got available it predicts it’s got 0 steps left to take.</p>
<p>So we’ll tell git that this commit is also bad:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nb">PS</span> <span class="o">&gt;</span> git bisect bad
e4886e8a59c1a921d149b7948eca0954d658b050 is the first bad commit
commit e4886e8a59c1a921d149b7948eca0954d658b050
Author: Chris Gardner &lt;[email protected]&gt;
Date: Wed Jul 17 20:16:40 2019 +0100
fix parameter name
example.ps1 | 4 ++--
1 file changed, 2 insertions<span class="o">(</span>+<span class="o">)</span>, 2 deletions<span class="o">(</span>-<span class="o">)</span></code></pre></figure>
<p>Based on the the fact that the next commit it could check is our known good commit git has decided that this must be the first bad commit and where our problem first occurred. So we can see what changed and who did it, in this case some person called Chris Gardner is going to get a nice bug to fix since he introduced it.</p>
<p>When we’re done with this bisect we simple run <code class="highlighter-rouge">git bisect reset</code> to get out of it and back to the branch we started on.</p>
<p>In this case it was a pretty short history to look through and the code base wasn’t very complex, but if we imagine scaling this up to an older code base with 10s or 100s of commits (or 1000s) and a lot of files then it can become quite time consuming to try to narrow down where the bad commit is. Luckily the folks who wrote bisect thought of this ahead of time and gave us a way to automate this.</p>
<h2 id="git-bisect-run">git bisect run</h2>
<p>From the <a href="https://git-scm.com/docs/git-bisect#_bisect_run">documentation</a> we can see that <code class="highlighter-rouge">git bisect run</code> expects a command to run and its arguments, and if it returns an exit code between 1 and 127 (but not 125) then it’ll assume the result was equivalent to <code class="highlighter-rouge">git bisect bad</code> and try moving on to the next commit. So how do we leverage this with PowerShell?</p>
<p>We’ll start with the same example repository and try running our simple test file against it. This test file can be as simple or as complex as we need it to be, as long as it fails in at least one of the ways that we see with the bug, ideally it’ll become part of our testing suite for this code (or start it if we don’t have any yet) so that we can prevent it occurring again (or catch it before it gets to production).</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell">Describe <span class="s2">"Testing Get-Data function"</span> <span class="o">{</span>
BeforeAll <span class="o">{</span>
Mock -CommandName Invoke-Command -MockWith <span class="o">{}</span>
Mock -CommandName <span class="nb">Get-ComputerInfo</span> -MockWith <span class="o">{}</span>
<span class="o">}</span>
It <span class="s2">"Should call Invoke-Command with the computer name"</span> <span class="o">{</span>
.\example.ps1 <span class="s2">"NotARealComputer"</span>
Assert-MockCalled -CommandName Invoke-Command -ParameterFilter <span class="o">{</span>
<span class="nv">$ComputerName</span> -eq <span class="s1">'NotARealComputer'</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span></code></pre></figure>
<p>So with our example.tests.ps1 file we can start the git bisect again. <strong>Note</strong> before you run this you’ll want to do <code class="highlighter-rouge">git reset --soft HEAD~1</code> to ensure the test files aren’t part of the history of the repository and therefore disappear as soon as git bisect checks out an earlier commit.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nb">PS</span> <span class="o">&gt;</span> git bisect <span class="nb">start </span>HEAD 6b98
Bisecting: 2 revisions left to <span class="nb">test </span>after this <span class="o">(</span>roughly 1 step<span class="o">)</span>
<span class="o">[</span>e3eb782ce10f59c9bf10626ecf7c00f3b89e5344] fix other thing
<span class="nb">PS</span> <span class="o">&gt;</span> git bisect run .\example.tests.ps1
running .\example.tests.ps1
C:/Program Files/Git/mingw64/libexec/git-core\git-bisect: line 247: .\example.tests.ps1: No such file or directory</code></pre></figure>
<p>Now we see a problem here, while we’d like to just run our test file and let Pester do what it should we can see that it’s not actually going to work that way. Because git isn’t a PowerShell tool it doesn’t know how to handle .ps1 files even if we run it from within a PowerShell prompt. So the workaround is to of course just run PowerShell.exe and pass it the file we want to run.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nb">PS</span> <span class="o">&gt;</span> git bisect run powershell -file .\example.tests.ps1
running powershell.exe -file .\example.tests.ps1
Describing Testing Get-Data <span class="k">function</span>
<span class="o">[</span>-] Should call Invoke-Command with the computer name 99ms
Expected Invoke-Command to be called at least 1 <span class="nb">times </span>but was called 0 <span class="nb">times
</span>10: Assert-MockCalled -CommandName Invoke-Command -ParameterFilter <span class="o">{</span>
at &lt;ScriptBlock&gt;, C:\code\gitbisect\example.tests.ps1: line 10
Bisecting: 0 revisions left to <span class="nb">test </span>after this <span class="o">(</span>roughly 0 steps<span class="o">)</span>
<span class="o">[</span>d77bd49d42ab8012a5593dd222fb52003e26d2f2] make <span class="nb">local </span>the same as remote
Describing Testing Get-Data <span class="k">function</span>
<span class="o">[</span>-] Should call Invoke-Command with the computer name 94ms
Expected Invoke-Command to be called at least 1 <span class="nb">times </span>but was called 0 <span class="nb">times
</span>10: Assert-MockCalled -CommandName Invoke-Command -ParameterFilter <span class="o">{</span>
at &lt;ScriptBlock&gt;, C:\code\gitbisect\example.tests.ps1: line 10
a9192eae044133433e24c25d29e4a350862d1d02 is the first bad commit
commit a9192eae044133433e24c25d29e4a350862d1d02
Author: Chris Gardner &lt;[email protected]&gt;
Date: Wed Jul 17 20:31:48 2019 +0100
Add support <span class="k">for </span>usernames
example.ps1 | 3 ++-
1 file changed, 2 insertions<span class="o">(</span>+<span class="o">)</span>, 1 deletion<span class="o">(</span>-<span class="o">)</span>
bisect run success</code></pre></figure>
<p>So now we can get our code running and the tests correctly failing, but the commit it’s flagging as the bad commit is actually in the wrong direction. What’s causing this? It’s due to the fact we’re running the test file directly and it’s not giving back an exit code,so therefore git assumes it’s a 0 and the commit it’s checked is good.</p>
<p>The solution to this problem is one of two things, we can write a quick one line script to call <code class="highlighter-rouge">Invoke-Pester -Script .\Example.tests.ps1 -EnableExit</code> (as seen in runtests.ps1) or we can just use <code class="highlighter-rouge">PowerShell.exe -command Invoke-Pester -Script .\Example.tests.ps1 -EnableExit</code>, either will work just as well. EnableExit on Invoke-Pester will cause it to return an exit code equal to the number of failing tests, hopefully this’ll be a low number (less than 127) but if it doesn’t you can instead use the script approach and capture the output of Invoke-Pester (using <code class="highlighter-rouge">-PassThru</code>) and return your own error code if it fails. This approach is very useful when you’ve got a large test suite that you need to run against the code as part of a bisect to ensure no other bugs have appeared (that you don’t already know about), for most cases though you want to craft a small number of tests to check just this bug and use those rather than whatever full suite you have.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nb">PS</span> <span class="o">&gt;</span> git bisect run PowerShell.exe -command Invoke-Pester -Script .\Example.tests.ps1 -EnableExit
running PowerShell.exe -command Invoke-Pester -Script .\Example.tests.ps1 -EnableExit
Executing all tests <span class="k">in</span> <span class="s1">'.\Example.tests.ps1'</span>
Executing script .\Example.tests.ps1
Describing Testing Get-Data <span class="k">function</span>
<span class="o">[</span>-] Should call Invoke-Command with the computer name 88ms
Expected Invoke-Command to be called at least 1 <span class="nb">times </span>but was called 0 <span class="nb">times
</span>10: Assert-MockCalled -CommandName Invoke-Command -ParameterFilter <span class="o">{</span>
at &lt;ScriptBlock&gt;, C:\code\gitbisect\example.tests.ps1: line 10
Tests completed <span class="k">in </span>1s
Tests Passed: 0, Failed: 1, Skipped: 0, Pending: 0, Inconclusive: 0
Bisecting: 0 revisions left to <span class="nb">test </span>after this <span class="o">(</span>roughly 0 steps<span class="o">)</span>
<span class="o">[</span>e4886e8a59c1a921d149b7948eca0954d658b050] fix parameter name
running PowerShell.exe -command Invoke-Pester -Script .\Example.tests.ps1 -EnableExit
Executing all tests <span class="k">in</span> <span class="s1">'.\Example.tests.ps1'</span>
Executing script .\Example.tests.ps1
Describing Testing Get-Data <span class="k">function</span>
<span class="o">[</span>-] Should call Invoke-Command with the computer name 91ms
Expected Invoke-Command to be called at least 1 <span class="nb">times </span>but was called 0 <span class="nb">times
</span>10: Assert-MockCalled -CommandName Invoke-Command -ParameterFilter <span class="o">{</span>
at &lt;ScriptBlock&gt;, C:\code\gitbisect\example.tests.ps1: line 10
Tests completed <span class="k">in </span>940ms
Tests Passed: 0, Failed: 1, Skipped: 0, Pending: 0, Inconclusive: 0
e4886e8a59c1a921d149b7948eca0954d658b050 is the first bad commit
commit e4886e8a59c1a921d149b7948eca0954d658b050
Author: Chris Gardner &lt;[email protected]&gt;
Date: Wed Jul 17 20:16:40 2019 +0100
fix parameter name
example.ps1 | 4 ++--
1 file changed, 2 insertions<span class="o">(</span>+<span class="o">)</span>, 2 deletions<span class="o">(</span>-<span class="o">)</span>
bisect run success</code></pre></figure>
<p>And now we can see it actually finds the correct commit that introduced the issue. In this case it took about as long either way, but that was due to the simplicity of the code and the small number of commits, on larger codebases or more complex code the automated way will almost always be better (and helps your overall test coverage).</p>
<h2 id="conclusion">Conclusion</h2>
<p>So we’ve seen here how to use <code class="highlighter-rouge">git bisect</code> to find the exact commit a bug appeared in, using our knowledge of Pester if necessary to automate it, and from there we can take whatever action we need to beyond just fixing the bug. If this was a larger commit with a few files changed we could see if the same or similar issues had been introduced in those but not detected in production use yet. The documentation for <code class="highlighter-rouge">git bisect</code> is very good (as is most of the git documentation) and includes a few other things you can do with it. Hopefully this will help with any future debugging you need to do, narrowing down the root cause of a bug can help to fix it where just studying the code in its current form might not be proving as useful, especially if there has been refactoring going on at the same time.</p></content><author><name></name></author><summary type="html">Inevitably when writing code there will be bugs, it’s just part of being human and writing code and especially true when the code gets complex. So when it occurs we need to track it down and fix it, which can be easy, but we often want to track down where the bug was introduced so we can figure out what caused it (especially for the more difficult to pinpoint bugs). As we’re all using source control this becomes easier with git and it’s extremely powerful bisect tool.</summary></entry><entry><title type="html">Enforcing Code Style using Pester</title><link href="https://chrislgardner.dev/powershell/2019/03/26/enforcing-style-with-pester.html" rel="alternate" type="text/html" title="Enforcing Code Style using Pester" /><published>2019-03-26T16:00:00-05:00</published><updated>2019-03-26T16:00:00-05:00</updated><id>/powershell/2019/03/26/enforcing-style-with-pester</id><content type="html" xml:base="/powershell/2019/03/26/enforcing-style-with-pester.html"><p>As part of an ongoing effort to improve code quality and consistency across the company we decided to apply the same principles to PowerShell code as we would apply to our C# and other code, since code is code no matter what language it is written in or who maintains it. With this in mind a few of us sat down many months ago and figured out what our style should be using <a href="https://github.com/PoshCode/PowerShellPracticeAndStyle">the community style guide</a> as a baseline and picking the things we’d like to apply. With these basic guidelines decided it was up to me to enforce these in some way, and Pester as my tool of choice.</p>
<!--more-->
<h2 id="why-pester">Why Pester</h2>
<p>When looking at the guidelines we’d set out to follow there were a few choices that could be made to handle this:</p>
<ul>
<li>Pester</li>
<li>ScriptAnalyzer</li>
<li>Custom script</li>
</ul>
<p>I discarded the idea of a custom script from the start, I didn’t really want to reinvent existing tooling especially when this will be running across some unknown number of files.</p>
<p>ScriptAnalyzer is pretty much built for doing this kind of work however to achieve it would be to write some custom rules, which at the time was something I didn’t have the time or skills for. If I were to revisit this (and I probably will soon) then that’s the route I’ll take as I’m much more comfortable with the AST now.</p>
<p>Pester therefore is the last option left, it sort of bridges the gap between the two other options where I just have to write some simple code to check for each guideline I want to test and then if I find it I just throw an error and Pester handles the reporting for me. And because this is going to be run as part of a CI process then I can take those reports and publish them as test results and fail my builds if I want to.</p>
<h2 id="how-do-we-actually-test-the-guidelines">How do we actually test the guidelines?</h2>
<p>This turns out to be both a really simple problem and a really difficult one at the same time, depending on which guideline we want to test. My basic process is quite simple and best illustrated with some psuedo code:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Find all the ps1 and psm1 files in the repository
For each file found:
Check a guideline
Log out a warning if it fails with the specific line number it's failing on and why
Check next guideline, repeat for each guideline
Optionally, if any tests have failed then fail the build
</code></pre></div></div>
<p>Quite a simple approach but quite extensible too as we decide on new guidelines we want to apply as it’s just a case of writing a new Pester test and adding it to the script that runs this. We converted this to a private Azure Pipelines task so we can easily add it to all the builds we have which include any PowerShell and have the guidelines controlled from a single central point.</p>
<h2 id="what-does-a-guideline-look-like">What does a guideline look like?</h2>
<p>So we know the Why and the How but what does some of this code actually look like, here is an example we have for ensuring that opening braces aren’t on a line on their own as we follow Stroustrup for various reasons.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell">It <span class="s1">'Should have opening braces on the same line as the statement'</span> <span class="o">{</span>
<span class="nv">$OpeningBracesExist</span> <span class="o">=</span> <span class="nv">$Contents</span> | <span class="nb">Where</span>-Object <span class="o">{</span> <span class="nv">$_</span>.Trim<span class="o">()</span> -eq <span class="s1">'{'</span><span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="nv">$OpeningBracesExist</span><span class="o">)</span> <span class="o">{</span>
<span class="nb">Write-Warning</span> <span class="s2">"Found the following opening braces on their own line:"</span>
<span class="k">foreach</span> <span class="o">(</span><span class="nv">$OpeningBrace</span> <span class="k">in</span> <span class="nv">$OpeningBracesExist</span><span class="o">)</span> <span class="o">{</span>
<span class="nb">Write-Warning</span> <span class="s2">"Opening Brace on it's own line - </span><span class="nv">$OpeningBrace</span><span class="s2">"</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="nv">$OpeningBracesExist</span> | Should -BeNullOrEmpty
<span class="o">}</span></code></pre></figure>
<p>Again quite a simple example, we’re taking $Contents (which is the result of <code class="highlighter-rouge">Get-Content -Path File.ps1</code>) and checking if any lines contain only <code class="highlighter-rouge">{</code>. If we find any instances of <code class="highlighter-rouge">{</code> then we’ll write some warnings and then fail the test. For most of our projects this isn’t necessary as we’ve got VS Code settings in place to enforce the Stroustrup style, but these settings aren’t in all of our projects yet and some people still like to use full Visual Studio or the PowerShell ISE.</p>
<p>Now lets look at a more complex example, in this case we’re looking for instances where a developer has made us of one of the Format-* commands to pretty up their output before passing it back to the user. As a general rule a function should always return objects and let the user decide if they want to format them in a pretty way or output to csv or do whatever with them, formatting before the user gets them takes that choice away from them and doesn’t actually return normal objects that they can make use of outside of the console.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell">It <span class="s1">'Should not have Format-* within a function'</span> <span class="o">{</span>
<span class="nv">$InputScript</span> <span class="o">=</span> <span class="o">[</span>Scriptblock]::Create<span class="o">((</span><span class="nb">Get-Content</span> -LiteralPath <span class="nv">$File</span>.Fullname -Raw<span class="o">))</span>
<span class="nv">$FunctionPredicate</span> <span class="o">=</span> <span class="o">{</span>
<span class="k">param</span> <span class="o">(</span><span class="nv">$Ast</span><span class="o">)</span>
<span class="nv">$Ast</span> -Is <span class="o">[</span>System.Management.Automation.Language.FunctionDefinitionAst]
<span class="o">}</span>
<span class="nv">$Functions</span> <span class="o">=</span> <span class="nv">$InputScript</span>.Ast.FindAll<span class="o">(</span><span class="nv">$FunctionPredicate</span>,<span class="nv">$true</span><span class="o">)</span>
<span class="k">Foreach</span> <span class="o">(</span><span class="nv">$Function</span> <span class="k">in</span> <span class="nv">$Functions</span><span class="o">)</span> <span class="o">{</span>
<span class="nv">$CommandPredicate</span> <span class="o">=</span> <span class="o">{</span>
<span class="k">param</span> <span class="o">(</span><span class="nv">$ast</span><span class="o">)</span>
<span class="nv">$Ast</span> -Is <span class="o">[</span>System.Management.Automation.Language.CommandAst] -and
<span class="nv">$ast</span>.GetCommandName<span class="o">()</span> -like <span class="s1">'Format-*'</span>
<span class="o">}</span>
<span class="nv">$Results</span> <span class="o">=</span> <span class="nv">$Function</span>.FindAll<span class="o">(</span><span class="nv">$CommandPredicate</span>,<span class="nv">$True</span><span class="o">)</span>
<span class="o">}</span>
<span class="k">Foreach</span><span class="o">(</span><span class="nv">$result</span> <span class="k">in</span> <span class="nv">$Results</span><span class="o">)</span> <span class="o">{</span>
<span class="nb">Write-Warning</span> -Message <span class="s2">"Found a line containing a Format cmdlet: </span><span class="k">$(</span><span class="nv">$Result</span>.Parent<span class="k">)</span><span class="s2">"</span>
<span class="o">}</span>
<span class="nv">$Results</span>.Count | Should Be 0
<span class="o">}</span></code></pre></figure>
<p>In this case we’re making use of the AST to find all the fuctions defined in a specified script, then for each of those fuction we’re finding all the commands which are named like <code class="highlighter-rouge">Format-*</code>. This is a more recenty addition to the guidelines after a colleague created a few functions outputting pretty data that was completely unusable outside of the console.</p>
<h2 id="whats-next">What’s next?</h2>
<p>So now we’ve got a bunch of guidelines that we apply to every build containing PowerShell and using Pester to test them, we take the output of those test runs and publish them back to Azure DevOps. This way we can see on a build how many failed and what they were, and when we’re reasonably confident that the majority of our scripts are following these rules we’ll start enforcing build failures when even a single test fails.</p>
<p>The next step beyond this will be converting many of these to custom ScriptAnalyzer rules. We’ve started making more use of it and are now outputting the results from analysis into SonarQube, so incorporating our custom rules into this would be very useful for an extra level of reporting. Some of them convert reasonably easy, like the check for Format-*, since they already make use of the AST, others will prove to be more difficult such as the check for the opening brace not being on its own line. When I make some progress on that then I’ll almost certianly blog about the process.</p></content><author><name></name></author><summary type="html">As part of an ongoing effort to improve code quality and consistency across the company we decided to apply the same principles to PowerShell code as we would apply to our C# and other code, since code is code no matter what language it is written in or who maintains it. With this in mind a few of us sat down many months ago and figured out what our style should be using the community style guide as a baseline and picking the things we’d like to apply. With these basic guidelines decided it was up to me to enforce these in some way, and Pester as my tool of choice.</summary></entry><entry><title type="html">[Scriptblock] and ConvertTo-Json: a match made in recursive hell</title><link href="https://chrislgardner.dev/powershell/2019/02/17/convertto-json-scripblock.html" rel="alternate" type="text/html" title="[Scriptblock] and ConvertTo-Json: a match made in recursive hell" /><published>2019-02-17T07:00:00-06:00</published><updated>2019-02-17T07:00:00-06:00</updated><id>/powershell/2019/02/17/convertto-json-scripblock</id><content type="html" xml:base="/powershell/2019/02/17/convertto-json-scripblock.html"><p>I’ve been working on <a href="https://github.com/ChrisLGardner/JeaDsc">JeaDsc</a> off and on for a few months to improve on the original project and make it available in the PowerShell Gallery. The biggest bug it’s currently got is that it doesn’t compare an existing configuration against a new one very well, especially for complex configurations. For a DSC resource this is a huge problem and something I’ve been wanting to fix got a little while. As part of fixing it I came across this wonderful problem with ConvertTo-Json.</p>
<!--more-->
<p>Before I get into the details of the problem and how I worked around it let’s look at JEA a little.</p>
<h2 id="what-is-jea">What is JEA?</h2>
<p>JEA stands for Just Enough Administration and is a PowerShell feature that allows you to restrict what a user can do when they make use of PowerShell Remoting into another machine. There’s some documentation on <a href="https://docs.microsoft.com/en-us/powershell/jea/overview">docs.com</a> about it.</p>
<p>With the ever growing use of PowerShell to manage systems, especially at scale, it’s important to consider what permissions a user needs to perform their tasks and only grant them that much access. Level 1 Helpdesk users don’t need Domain Admin permissions if their main work is resetting passwords and basic troubleshooting, but depending on your AD structure you might not be able to easily grant them the permissions they need, this is where JEA comes in. You can define a Role Capabilities file that states a user can only run <code class="highlighter-rouge">Set-ADAccountPassword</code> and that the identity they can specify must be a certain format, that which matches your normal staff users. We then take this file and add it to a Session Configuration file, which specifies who can connect to a PowerShell remoting session and if they do then what permissions they get and a bunch of other useful things like transaction logging etc.</p>
<p>With this we can securely control who can access which servers and what they can do on those servers, and all of this can be controlled through PowerShell so we can deploy this using DSC. The big benefit of using DSC to handle this is that we can version control these configuration files to see what permissions were granted and when and by who. We can then feed this through a release pipeline to allow us to deploy these changes automatically to our pull server, first to a test environment and then to production. I’ll have another blog post or two on this process in future as I’m currently working on a pipeline for this.</p>
<h2 id="where-does-convertto-json-come-into-this">Where does ConvertTo-Json come into this?</h2>
<p>As part of JeaDsc we need to test the existing configuration to see if we need to apply the new configuration to it. As both Role Capabilities and Session Configurations are stored as hashtables it’s a little complex to compare them, we can’t just do a simple <code class="highlighter-rouge">$ExistingConfiguration -eq $NewConfiguration</code> as they are different objects even if the keys and values are the same. So my first implementation for Role Capabilities was to make use of a existing function to <a href="https://github.com/stuartleeks/PesterMatchHashtable">compare hashtables for pester</a> but this runs into a bit of problem when the hashtables can be as complex as these, including arrays and hashtables within the values of some of the keys. This wasn’t initially a problem until you start making use of the FunctionDefinitions feature and have to compare scriptblocks, as raised <a href="https://github.com/ChrisLGardner/JeaDsc/issues/19">here by Raimund Andree</a>.</p>
<p>So I looked at how Session Configurations were handling this as that had existing before I started working on this and I found this method:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"> hidden <span class="o">[</span><span class="kt">bool</span><span class="o">]</span> ComplexObjectsEqual<span class="o">(</span><span class="nv">$object1</span>, <span class="nv">$object2</span><span class="o">)</span> <span class="o">{</span>
<span class="nv">$object1ordered</span> <span class="o">=</span> <span class="o">[</span>System.Collections.Specialized.OrderedDictionary]@<span class="o">{}</span>
<span class="nv">$object1</span>.Keys | <span class="nb">Sort-Object</span> -Descending | <span class="k">ForEach</span>-Object <span class="o">{</span><span class="nv">$object1ordered</span>.Insert<span class="o">(</span>0, <span class="nv">$_</span>, <span class="nv">$object1</span><span class="o">[</span><span class="s2">"</span><span class="nv">$_</span><span class="s2">"</span><span class="o">])}</span>
<span class="nv">$object2ordered</span> <span class="o">=</span> <span class="o">[</span>System.Collections.Specialized.OrderedDictionary]@<span class="o">{}</span>
<span class="nv">$object2</span>.Keys | <span class="nb">Sort-Object</span> -Descending | <span class="k">ForEach</span>-Object <span class="o">{</span><span class="nv">$object2ordered</span>.Insert<span class="o">(</span>0, <span class="nv">$_</span>, <span class="nv">$object2</span><span class="o">[</span><span class="s2">"</span><span class="nv">$_</span><span class="s2">"</span><span class="o">])}</span>
<span class="nv">$json1</span> <span class="o">=</span> <span class="nb">ConvertTo-Json</span> -InputObject <span class="nv">$object1ordered</span> -Depth 100
<span class="nv">$json2</span> <span class="o">=</span> <span class="nb">ConvertTo-Json</span> -InputObject <span class="nv">$object2ordered</span> -Depth 100
<span class="k">if</span> <span class="o">(</span><span class="nv">$json1</span> -ne <span class="nv">$json2</span><span class="o">)</span> <span class="o">{</span>
<span class="nb">Write-Verbose</span> <span class="s2">"object1: </span><span class="nv">$json1</span><span class="s2">"</span>
<span class="nb">Write-Verbose</span> <span class="s2">"object2: </span><span class="nv">$json2</span><span class="s2">"</span>
<span class="o">}</span>
<span class="k">return</span> <span class="o">(</span><span class="nv">$json1</span> -eq <span class="nv">$json2</span><span class="o">)</span>
<span class="o">}</span></code></pre></figure>
<p>Ignoring the badly named variables, this is a pretty simple but elegant solution. Converting the objects to ordered dictionaries and then to JSON should ensure that the comparison is accurate. This method works very well for Session Configuration files but how well will it work with Role Capabilities? I think we can guess the answer to this by the fact I’m writing a blog post about it.</p>
<h2 id="scriptblocks-and-their-properties">Scriptblocks and their properties</h2>
<p>Within a JEA Role Capability file ScriptBlocks are used for the FunctionDefinitions and allow administrators to define custom functions that are available within a JEA session to further control what a user can or can’t do. The problem from my perspective comes when I try to convert those to JSON as the default output to the console isn’t the only property of the object, much like many other object types in PowerShell.</p>
<p>Let’s take a look at an example:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nb">PS</span><span class="o">&gt;</span> <span class="nv">$ScriptBlock</span> <span class="o">=</span> <span class="o">{</span> Get-Command <span class="o">}</span>
<span class="nb">PS</span><span class="o">&gt;</span> <span class="nv">$scriptblock</span>
Get-Command
<span class="nb">PS</span><span class="o">&gt;</span> <span class="nv">$scriptblock</span>.ToString<span class="o">()</span>
Get-Command
<span class="nb">PS</span><span class="o">&gt;</span> <span class="nv">$ScriptBlock</span> | <span class="nb">Get-Member</span> -MemberType Properties
TypeName: System.Management.Automation.ScriptBlock
Name MemberType Definition
---- ---------- ----------
Ast Property System.Management.Automation.Language.Ast Ast <span class="o">{</span>get;<span class="o">}</span>
Attributes Property System.Collections.Generic.List[System.Attribute] Attributes <span class="o">{</span>get;<span class="o">}</span>
DebuggerHidden Property <span class="kt">bool </span>DebuggerHidden <span class="o">{</span>get;set;<span class="o">}</span>
File Property <span class="kt">string </span>File <span class="o">{</span>get;<span class="o">}</span>
Id Property guid Id <span class="o">{</span>get;<span class="o">}</span>
IsConfiguration Property <span class="kt">bool </span>IsConfiguration <span class="o">{</span>get;set;<span class="o">}</span>
IsFilter Property <span class="kt">bool </span>IsFilter <span class="o">{</span>get;set;<span class="o">}</span>
Module Property psmoduleinfo Module <span class="o">{</span>get;<span class="o">}</span>
StartPosition Property System.Management.Automation.PSToken StartPosition <span class="o">{</span>get;<span class="o">}</span></code></pre></figure>
<p>So we can see here that the default output for a <code class="highlighter-rouge">[ScriptBlock]</code> type is just calling the ToString() method on it but it has a few other properties on it that <code class="highlighter-rouge">ConvertTo-Json</code> will try to use instead, since it doesn’t care about methods, and many of these properties also have properties of their own. This normally wouldn’t be a problem as <code class="highlighter-rouge">ConvertTo-Json</code> has a limit of how far down the tree it’ll go, by specifying the <code class="highlighter-rouge">-Depth</code> parameter we can control this and it defaults to 2. Other than not really returning what we need from this I also discovered that after a certain depth there are some references to further up the chain and we get into a bit of a recursive lookup problem before PowerShell either crashes or throws a Stackoverflow exception. <a href="https://github.com/PowerShell/PowerShell/issues/7091">Someone else had found this too</a> and helpfully logged it on GitHub but with no solution in sight and if there was it would only be available in PowerShell 6+ I needed a workaround.</p>
<h2 id="the-solutionworkaround">The Solution/Workaround</h2>
<p>Because I don’t want all of those properties that come with a ScriptBlock and only really need the ToString() output I decided the simplest soluiton was to just replace any ScriptBlocks with their ToString() output and ended up with this:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="k">if</span> <span class="o">(</span><span class="nv">$ReferenceObjectordered</span>.FunctionDefinitions<span class="o">)</span> <span class="o">{</span>
<span class="k">foreach</span> <span class="o">(</span><span class="nv">$FunctionDefinition</span> <span class="k">in</span> <span class="nv">$ReferenceObjectordered</span>.FunctionDefinitions<span class="o">)</span> <span class="o">{</span>
<span class="nv">$FunctionDefinition</span>.ScriptBlock <span class="o">=</span> <span class="nv">$FunctionDefinition</span>.ScriptBlock.Ast.ToString<span class="o">()</span>.Replace<span class="o">(</span><span class="s1">' '</span>, <span class="s1">''</span><span class="o">)</span>
<span class="o">}</span>
<span class="o">}</span></code></pre></figure>
<p>I’m calling the AST version of ToString() to ensure I get the braces in as well, this will make it a bit easier for people debugging if their new configuration doesn’t match the existing one when they’d expect it to, since it’ll match exactly what is in the file. I’m also removing all the whitespace to handle any odd spacing that might come in to play as we don’t care about the code actually running and it won’t have any impact on the Set method.</p>
<p>The <a href="https://github.com/ChrisLGardner/JeaDsc/blob/master/JeaDsc.psm1#L105">full function</a> has been renamed to something a bit more focused and I’ve updated the variable names to be more meaningful. This will be rolled into the Session Configuration resource as well so that I only have to maintain a single version of the code. I also wrote a <a href="https://github.com/ChrisLGardner/JeaDsc/blob/master/Tests/Unit/Compare-JeaConfiguration.Tests.ps1">whole bunch of tests</a> for the different scenarios I could think of where I’d want to use this function and this way I can ensure it still works whenever I make changes to it, which should hopefully be very rare now.</p>
<p>Arguably this function should really be called <code class="highlighter-rouge">Test-JeaConfiguration</code> since one output is a bool and it should output <code class="highlighter-rouge">$true</code> when they match but as I’m actually doing a comparison it made more sense to call it <code class="highlighter-rouge">Compare-JeaConfiguration</code>. I should probably fix the output to be <code class="highlighter-rouge">$true</code> when they do match just to remain consistent with the <code class="highlighter-rouge">$false</code> output but other <code class="highlighter-rouge">Compare-*</code> functions don’t necessarily output anything when they match.</p></content><author><name></name></author><summary type="html">I’ve been working on JeaDsc off and on for a few months to improve on the original project and make it available in the PowerShell Gallery. The biggest bug it’s currently got is that it doesn’t compare an existing configuration against a new one very well, especially for complex configurations. For a DSC resource this is a huge problem and something I’ve been wanting to fix got a little while. As part of fixing it I came across this wonderful problem with ConvertTo-Json.</summary></entry><entry><title type="html">Finding Default Parameter Values with the AST</title><link href="https://chrislgardner.dev/powershell/2018/08/22/finding-default-parameter-values.html" rel="alternate" type="text/html" title="Finding Default Parameter Values with the AST" /><published>2018-08-22T13:00:00-05:00</published><updated>2018-08-22T13:00:00-05:00</updated><id>/powershell/2018/08/22/finding-default-parameter-values</id><content type="html" xml:base="/powershell/2018/08/22/finding-default-parameter-values.html"><p>As part of a big refactor on an internal module I’ve decided to add a big pile of Pester tests. The module was lacking them previously due to various reasons and this seemed like the perfect oppurtunity to add them.</p>
<p>With my Pester test suites one of the things I like to do is have a bunch of tests for parameters and their various attributes that I want to ensure are correctly set. Previously I’d focused on the easy things like aliases, mandatory-ness, pipeline input etc. but this time I wanted to check for default values since we make use of them in a few places.</p>
<!--more-->
<h2 id="possible-soltuions">Possible soltuions</h2>
<p>When approaching this problem there were a few options that occured to me, I could use some regular expressions (regex) to handle it or I could delve into the PowerShell Abstract Syntax Tree (AST). Here’s the sample function we’ll be working with for this:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="k">Function </span>Get-SomeData <span class="o">{</span>
<span class="o">[</span><span class="na">cmdletbinding</span><span class="o">()]</span>
<span class="k">param</span> <span class="o">(</span>
<span class="o">[</span>Parameter<span class="o">(</span>Mandatory<span class="o">)]</span>
<span class="o">[</span><span class="kt">string</span><span class="o">]</span><span class="nv">$ParameterName</span> <span class="o">=</span> <span class="s1">'DefaultValue'</span>,
<span class="o">[</span><span class="kt">int</span><span class="o">]</span><span class="nv">$HowMany</span>
<span class="o">)</span>
<span class="nv">$ParameterName</span>
<span class="o">}</span></code></pre></figure>
<h3 id="regex">Regex</h3>
<p>There’s a running joke among developers that when you choose to use regex to solve a problem then you now have two problems. However in this case it seemend like quite a reasonable solution to implement as it was a very regular pattern that would never be repeated in a file.</p>
<p>So here’s the regex I came up with, and a handy comment above it with what it all means (perhaps a little too verbose a description):</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="c1"># \[ - Matches a [ character
</span>
<span class="c1"># \w+ - Matches any word character 1 or more times
</span>
<span class="c1"># \] - Matches a ] character
</span>
<span class="c1"># \$ - Matches a $ character
</span>
<span class="c1"># ParameterName - Matches the string ParamterName
</span>
<span class="c1"># * - Matches 0 or more spaces
</span>
<span class="c1"># = - Matches a = character
</span>
<span class="c1"># ("DefaultValue"|'DefaultValue') - Matches the DefaultValue string in either quotes and captures in a group
</span>
<span class="c1"># ,$ - Matches a comma at the end of a line
</span>
<span class="nv">$regex</span> <span class="o">=</span> <span class="s2">"\[\w+\]\</span><span class="se">`$</span><span class="s2">ParameterName *= *(</span><span class="se">`"</span><span class="s2">DefaultValue</span><span class="se">`"</span><span class="s2">|'DefaultValue'),$"</span></code></pre></figure>
<p>This works pretty well and will happily find me any instances of that parameter in a file, there should only be 1 so the rest of my It block would look like this:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell">It <span class="s2">"Should have a default value of 'DefaultValue' for ParameterName parameter"</span> <span class="o">{</span>
<span class="nv">$regex</span> <span class="o">=</span> <span class="s2">"^\[\w+\]\</span><span class="se">`$</span><span class="s2">ParameterName *= *(</span><span class="se">`"</span><span class="s2">DefaultValue</span><span class="se">`"</span><span class="s2">|'DefaultValue'),$"</span>
<span class="nv">$MatchStrings</span> <span class="o">=</span> <span class="nv">$Sut</span> | <span class="nb">Select-String</span> <span class="nv">$Regex</span>
<span class="nv">$MatchStrings</span>.Count | Should -Be 1
<span class="o">}</span></code></pre></figure>
<p>The major problem I have with this approach is that I have to run it against the functions source file rather than the compiled psm1 file, like all my other tests. It’s unlikely there has been any change in the short time between my script combining all the ps1 files into a single psm1 and the tests running but I still prefer to always test the compiled module.</p>
<p>It would still be possible to do this test against the compiled psm1 but I’d have to account for how many functions use that parameter with a default value, which either means updating all my effected tests each time I add a new function or maintaining a config file that I use to control the tests. The later is certainly an option but doesn’t feel quite right for this particular testing problem.</p>
<h3 id="ast">AST</h3>
<p>The alternative approach to this is using the Abstract Syntax Tree and the various methods built into it. There are a number of great posts and books out there describing the AST much better than I can, including quite a good example on <a href="https://blogs.technet.microsoft.com/heyscriptingguy/2012/09/26/learn-how-it-pros-can-use-the-powershell-ast/">Hey, Scripting Guy</a>.</p>
<p>So let’s dive into my solution to this problem. First we have to get our function and its AST representation, which <code class="highlighter-rouge">Get-Command</code> is able to provide quite easily. Let’s see what properties we can retrieve using the ever useful <code class="highlighter-rouge">Get-Member</code>:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell">Get-Command -Name Get-SomeData | <span class="nb">Get-Member
</span>TypeName: System.Management.Automation.FunctionInfo
Name MemberType Definition
---- ---------- ----------
Equals Method <span class="kt">bool </span>Equals<span class="o">(</span>System.Object obj<span class="o">)</span>
GetHashCode Method <span class="kt">int </span>GetHashCode<span class="o">()</span>
GetType Method <span class="nb">type </span>GetType<span class="o">()</span>
ResolveParameter Method System.Management.Automation.ParameterMetadata ResolveParameter<span class="o">(</span><span class="kt">string </span>name<span class="o">)</span>
ToString Method <span class="kt">string </span>ToString<span class="o">()</span>
<span class="na">CmdletBinding </span>Property <span class="kt">bool </span><span class="na">CmdletBinding</span> <span class="o">{</span>get;<span class="o">}</span>
CommandType Property System.Management.Automation.CommandTypes CommandType <span class="o">{</span>get;<span class="o">}</span>
DefaultParameterSet Property <span class="kt">string </span>DefaultParameterSet <span class="o">{</span>get;<span class="o">}</span>
Definition Property <span class="kt">string </span>Definition <span class="o">{</span>get;<span class="o">}</span>
Description Property <span class="kt">string </span>Description <span class="o">{</span>get;set;<span class="o">}</span>
HelpFile Property <span class="kt">string </span>HelpFile <span class="o">{</span>get;<span class="o">}</span>
Module Property psmoduleinfo Module <span class="o">{</span>get;<span class="o">}</span>
ModuleName Property <span class="kt">string </span>ModuleName <span class="o">{</span>get;<span class="o">}</span>
Name Property <span class="kt">string </span>Name <span class="o">{</span>get;<span class="o">}</span>
Noun Property <span class="kt">string </span>Noun <span class="o">{</span>get;<span class="o">}</span>
Options Property System.Management.Automation.ScopedItemOptions Options <span class="o">{</span>get;set;<span class="o">}</span>
OutputType Property System.Collections.ObjectModel.ReadOnlyCollection[System.Management.Automation.PSTypeName] OutputType <span class="o">{</span>get;<span class="o">}</span>
Parameters Property System.Collections.Generic.Dictionary[string,System.Management.Automation.ParameterMetadata] Parameters <span class="o">{</span>get;<span class="o">}</span>
ParameterSets Property System.Collections.ObjectModel.ReadOnlyCollection[System.Management.Automation.CommandParameterSetInfo] Par...
RemotingCapability Property System.Management.Automation.RemotingCapability RemotingCapability <span class="o">{</span>get;<span class="o">}</span>
ScriptBlock Property scriptblock ScriptBlock <span class="o">{</span>get;<span class="o">}</span>
Source Property <span class="kt">string </span>Source <span class="o">{</span>get;<span class="o">}</span>
Verb Property <span class="kt">string </span>Verb <span class="o">{</span>get;<span class="o">}</span>
Version Property version Version <span class="o">{</span>get;<span class="o">}</span>
Visibility Property System.Management.Automation.SessionStateEntryVisibility Visibility <span class="o">{</span>get;set;<span class="o">}</span>
<span class="na">HelpUri </span>ScriptProperty System.Object <span class="na">HelpUri</span> <span class="o">{</span><span class="nv">get</span><span class="o">=</span><span class="nv">$oldProgressPreference</span> <span class="o">=</span> <span class="nv">$ProgressPreference</span>...</code></pre></figure>
<p>That’s a whole lot of properties but the one we’re interested in is <code class="highlighter-rouge">ScriptBlock</code>. That will, unsurprisingly, return us a ScriptBlock object of the function and part of the scriptblock is the AST representation of it.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="o">(</span>Get-Command -Name Get-SomeData<span class="o">)</span>.ScriptBlock.Ast | <span class="nb">Get-Member
</span>TypeName: System.Management.Automation.Language.FunctionDefinitionAst
Name MemberType Definition
---- ---------- ----------
<span class="nb">Copy </span>Method System.Management.Automation.Language.Ast <span class="nb">Copy</span><span class="o">()</span>
Equals Method <span class="kt">bool </span>Equals<span class="o">(</span>System.Object obj<span class="o">)</span>
Find Method System.Management.Automation.Language.Ast Find<span class="o">(</span>System.Func[System.Management.Automation.Language.Ast,bool] predicate...
FindAll Method System.Collections.Generic.IEnumerable[System.Management.Automation.Language.Ast] FindAll<span class="o">(</span>System.Func[System.Managem...
GetHashCode Method <span class="kt">int </span>GetHashCode<span class="o">()</span>
GetHelpContent Method System.Management.Automation.Language.CommentHelpInfo GetHelpContent<span class="o">()</span>, System.Management.Automation.Language.Commen...
GetType Method <span class="nb">type </span>GetType<span class="o">()</span>
SafeGetValue Method System.Object SafeGetValue<span class="o">()</span>
ToString Method <span class="kt">string </span>ToString<span class="o">()</span>
Visit Method System.Object Visit<span class="o">(</span>System.Management.Automation.Language.ICustomAstVisitor astVisitor<span class="o">)</span>, void Visit<span class="o">(</span>System.Managemen...
Body Property System.Management.Automation.Language.ScriptBlockAst Body <span class="o">{</span>get;<span class="o">}</span>
Extent Property System.Management.Automation.Language.IScriptExtent Extent <span class="o">{</span>get;<span class="o">}</span>
IsFilter Property <span class="kt">bool </span>IsFilter <span class="o">{</span>get;<span class="o">}</span>
IsWorkflow Property <span class="kt">bool </span>IsWorkflow <span class="o">{</span>get;<span class="o">}</span>
Name Property <span class="kt">string </span>Name <span class="o">{</span>get;<span class="o">}</span>
Parameters Property System.Collections.ObjectModel.ReadOnlyCollection[System.Management.Automation.Language.ParameterAst] Parameters <span class="o">{</span>get;<span class="o">}</span>
Parent Property System.Management.Automation.Language.Ast Parent <span class="o">{</span>get;<span class="o">}</span></code></pre></figure>
<p>From here we want to make use of the FindAll method on the AST. This will let us find all AST elements which match a set of criteria we specify. In this case we want to find all of the AST elements which are of type ParameterAST and which have the name ParameterName.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="o">(</span>Get-Command -Name <span class="nv">$Sut</span><span class="o">)</span>.ScriptBlock.Ast.FindAll<span class="o">(</span> <span class="o">{</span>
<span class="nv">$args</span><span class="o">[</span>0] -is <span class="o">[</span>System.Management.Automation.Language.ParameterAst] -and
<span class="nv">$args</span><span class="o">[</span>0].Name.VariablePath.UserPath -eq <span class="s1">'ParameterName'</span>
<span class="o">}</span>, <span class="nv">$true</span><span class="o">)</span></code></pre></figure>
<p>Here we have a scriptblock in a similar format to those used in <code class="highlighter-rouge">Where-Object</code> and we make use of the <code class="highlighter-rouge">$args[0]</code> automatic variable, which is populated with each AST element one at a time. We can also declare a param block within this scriptblock if we wanted to use a more descriptive variable name.</p>
<p>Let’s break down what this comparison is doing and look at it more closely:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nv">$args</span><span class="o">[</span>0] -is <span class="o">[</span>System.Management.Automation.Language.ParameterAst] -and</code></pre></figure>
<p>First we make sure the type of AST we’re looking at is the type we care about, due to how <code class="highlighter-rouge">-and</code> comparisons work if this doesn’t match then it’ll continue on to the next AST element without even checking the other part of the comparison. To find the AST type you want to look at, and to generally explore the AST representation of a scriptblock, you can make use of <code class="highlighter-rouge">Show-AST</code> from the <a href="https://www.powershellgallery.com/packages/ShowPSAst/1.0">ShowPSAst Module</a> or there are a few other AST modules available on the gallery as well.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nv">$args</span><span class="o">[</span>0].Name.VariablePath.UserPath -eq <span class="s1">'ParameterName'</span></code></pre></figure>
<p>Next we compare the name of the parameter to what we want. This is buried a little deeper than just <code class="highlighter-rouge">$args[0].name</code> and took a little digging around in the resulting AST object that came back without this part of the query.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="o">}</span>, <span class="nv">$true</span><span class="o">)</span></code></pre></figure>
<p>This section is a little interesting as FindAll expects two arguements passed to it, a Predicate (the scriptblock) and a boolean. The Predicate is what we’ve just looked and it should just return true or false. The boolean is to tell FindAll if it should recurse through nested scriptblocks. In this case we’ll want to do this so I’ve set it to <code class="highlighter-rouge">$true</code> but there are some cases where you won’t want to do this and can therefore set it to <code class="highlighter-rouge">$false</code>.</p>
<p>So now, hopefully, we’ll have an output from this containing the AST object for the parameter we’re looking for. From here we just want to access the <code class="highlighter-rouge">Value</code> property of the <code class="highlighter-rouge">DefaultValue</code> property (as it’s a nested object with some more details in it).</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="o">(</span>Get-Command -Name <span class="nv">$Sut</span><span class="o">)</span>.ScriptBlock.Ast.FindAll<span class="o">(</span> <span class="o">{</span>
<span class="nv">$args</span><span class="o">[</span>0] -is <span class="o">[</span>System.Management.Automation.Language.ParameterAst] -and
<span class="nv">$args</span><span class="o">[</span>0].Name.VariablePath.UserPath -eq <span class="s1">'ParameterName'</span>
<span class="o">}</span>, <span class="nv">$true</span><span class="o">)</span>.DefaultValue
StringConstantType : SingleQuoted
Value : DefaultValue
StaticType : System.String
Extent : <span class="s1">'DefaultValue'</span>
Parent : <span class="o">[</span>Parameter<span class="o">(</span>Mandatory<span class="o">)]</span>
<span class="o">[</span><span class="kt">string</span><span class="o">]</span><span class="nv">$ParameterName</span> <span class="o">=</span> <span class="s1">'DefaultValue'</span></code></pre></figure>
<p>So our final test when all this is put together looks like the below. We could add another check in here too to ensure the parameter is only present once but that would probably be better suited a separate test.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell">It <span class="s2">"Should have a default value of 'DefaultValue' for ParameterName parameter"</span> <span class="o">{</span>
<span class="o">(</span>Get-Command -Name <span class="nv">$Sut</span><span class="o">)</span>.ScriptBlock.Ast.FindAll<span class="o">(</span> <span class="o">{</span>
<span class="nv">$args</span><span class="o">[</span>0] -is <span class="o">[</span>System.Management.Automation.Language.ParameterAst] -and
<span class="nv">$args</span><span class="o">[</span>0].Name.VariablePath.UserPath -eq <span class="s1">'ParameterName'</span>
<span class="o">}</span>, <span class="nv">$true</span><span class="o">)</span>.DefaultValue.Value | Should -Be <span class="s1">'DefaultValue'</span>
<span class="o">}</span></code></pre></figure>
<p>The major benefit this has over the regex approach is that it’ll run against the psm1 file as it’s pulling the functions from Get-Command after the psm1 has been imported. It’s also a lot more focused and less brittle than the regex approach, if a parameter isn’t strongly typed then the regex will miss it. There are solutions to these problems but it will often feel like investing time and effort into a less flexible solution.</p>
<h2 id="conclusion">Conclusion</h2>
<p>The AST is really powerful but can also be pretty complicated when you’re first looking at it, tools like Show-AST help a lot and there are a lot of really good blog posts out there about working with the AST. There are also a few really helpful people on the <a href="https://j.mp/psslack">PowerShell Slack</a> who know a good deal about the AST. As we can see in this situation it has given us a much more flexible solution to the problem we were trying to solve and it accounts for a lot more possible situations.</p>
<p>Regex is an even more powerful and complicated beast but it can be pretty difficult to get your pattern correct for the various use cases you have. It’s a tool I’ll often employ first but it is not always the best solution, as can be seen here.</p></content><author><name></name></author><summary type="html">As part of a big refactor on an internal module I’ve decided to add a big pile of Pester tests. The module was lacking them previously due to various reasons and this seemed like the perfect oppurtunity to add them. With my Pester test suites one of the things I like to do is have a bunch of tests for parameters and their various attributes that I want to ensure are correctly set. Previously I’d focused on the easy things like aliases, mandatory-ness, pipeline input etc. but this time I wanted to check for default values since we make use of them in a few places.</summary></entry><entry><title type="html">Module Worst Practices</title><link href="https://chrislgardner.dev/powershell/2018/08/03/module-worst-practices.html" rel="alternate" type="text/html" title="Module Worst Practices" /><published>2018-08-03T13:00:00-05:00</published><updated>2018-08-03T13:00:00-05:00</updated><id>/powershell/2018/08/03/module-worst-practices</id><content type="html" xml:base="/powershell/2018/08/03/module-worst-practices.html"><p>It has been a while since my last post so let’s get back on track with some “interesting” things I’ve discovered while working on another project for another blog post (or two).</p>
<p>The project involves taking almost all the modules in the PowerShell Gallery and pulling all their help data into a graph database (Neo4j) and then doing some analytics on it. That’s still in progress but I thought I’d blog about some of the worst practices I’ve seen while doing this work.</p>
<!--more-->
<p>The proces was split into 3 steps:</p>
<ul>
<li>Import the module and add a node to the DB</li>
<li>Get all the public commands in the module and add each as a node in the DB</li>
<li>Get the help for each command and update the command node with some properties</li>
</ul>
<p>Ignoring the various performance improvements I could have made in this process (more on those in the other post), there were a number of problems that jumped out from these steps. We’ll start with the most annoying ones and work from there.</p>
<h2 id="do-not-prompt-for-credentials-on-import">Do not prompt for credentials on import</h2>
<h3 id="the-problem">The Problem</h3>
<p>This covers using Get-Credential, Read-Host, or various other ways and it goes beyond just normal credentials but any sort of values to access things, like API keys or access tokens. This stops any sort of automation dead if you haven’t previously run the import manually and set up these values, assuming they can be persisted to disk securely somewhere.</p>
<h3 id="the-solution">The Solution</h3>
<p>Provide functions for this: a simple <code class="highlighter-rouge">Connect-MyService -Credential</code> cmdlet is often enough as then that credential object creation can be automated. Other options include making it configurable using something like <code class="highlighter-rouge">Set-MyServiceConfiguration -Credential</code>. This is especially useful if you’ve got a number of other configurable settings and modules. <a href="http://psframework.org/">PSFramework</a> can make this a lot easier to work with.</p>
<p>Storing credentials of any sort securely can be another problem to deal with. Data Protection Application Programming Interface (DPAPI) helps a lot in keeping them secured to just the user who created them on the machine they were created. But that’s often going to cause further issues when you run your scripts as dedicated service accounts. There are a few possible solutions to this:</p>
<ul>
<li><a href="https://github.com/Jaykul/BetterCredentials">Better Credentials</a> stores credentials in the Windows Credential Store for easier retrieval</li>
<li><a href="https://github.com/dlwyatt/ProtectedData">ProtectedData</a> lets you encrypt pretty much anything with either a certificate or a password and supports all the way back to PSv2</li>
<li><a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.security/protect-cmsmessage?view=powershell-5.1">Protect-CmsMessage</a> and the other CmsMessage cmdlets perform a similar role to ProtectedData but only work on strings and only work on PSv4+</li>
</ul>
<p>Before implementing any of these (or your own method) always consider your use case and the possible points of failure.</p>
<h2 id="dont-change-the-prompt-or-console-without-warning">Don’t change the prompt or console without warning</h2>
<h3 id="the-problem-1">The Problem</h3>
<p>Suddenly finding your prompt has changed from whatever you had before, even if you didn’t intentionally set it up that way, can be a jarring user experience. Some modules are specifically designed to make changes to the prompt or add extra functionality. They can be very powerful in what they offer beyond the basics offered by the console. The problem comes when they make those changes on import and then don’t offer a way to reverse it on removal.</p>
<p>An equally annoying one is changing the colours of the text or background colour; I’ve also seen at least one instance of a module clearing the screen on import before dumping some module information to it. These were bad user experiences and no matter how good those modules were at what they did I was considerably less likely to be using them.</p>
<h3 id="the-solution-1">The Solution</h3>
<p>At a minimum take a backup of the current settings that you’re about to modify and store that somewhere sensible for the user like $HOME. Preferably in the form of a script they can run to revert the changes. Beyond that provide functions to enable and disable the functionality your module is providing. See <a href="https://github.com/dahlbyk/posh-git">Posh-Git</a> as an example; it has a Write-VcsStatus function that you can place in your custom Prompt function and have it work.</p>
<p>You can add some behaviour to your module that deals with what happens when someone runs <code class="highlighter-rouge">Remove-Module MyModule</code>. The process for doing this is detailed in the Notes section of the help for <a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/remove-module?view=powershell-6#notes">Remove-Module</a>.</p>
<h2 id="dont-set-psdebug--strict-and-leave-it-there">Don’t Set-PSDebug -Strict and leave it there</h2>
<h3 id="the-problem-2">The Problem</h3>
<p><code class="highlighter-rouge">Set-PSDebug -Strict</code> is very helpful for ensuring you haven’t made any obvious mistakes in your code. For example, ensuring variables are declared assigned before use in another operation. But it comes with a problem that many people are apparently not aware of: It applies to the global scope and not just to your module or function.</p>
<h3 id="the-solution-2">The Solution</h3>
<p>The solution is to just use <code class="highlighter-rouge">Set-StrictMode -Version Latest</code> (or another version as appropriate). This will grant the benefits of StrictMode but only within the scope of your module. The downside to StrictMode from a users perspective is that it is not always obvious when it has been enabled. There isn’t a built-in cmdlet to find out if it is enabled and to what level. Thankfully Chris Dent has written <a href="https://gist.github.com/indented-automation/9279592035ca952360ce9e33643ba932">this helpful function</a> to solve this problem.</p>
<p>There are other ways around needing to even enable StrictMode. The best option would be Pester tests for all your functions to help ensure you know that all of your code paths will work and that your function will correctly handle null inputs in places etc. If you’re primarily worried about users not inputting values for some parameters then use the Mandatory parameter attribute, or validation attributes like <code class="highlighter-rouge">[ValidateNotNullOrEmpty()]</code> and others.</p>
<h2 id="dont-throw-an-error-and-fail-importing-if-non-powershell-dependencies-arent-there">Don’t throw an error and fail importing if non-PowerShell dependencies aren’t there</h2>
<h3 id="the-problem-3">The Problem</h3>
<p>Some modules rely on tools outside of PowerShell, we’re writing PowerShell to automate almost anything. That often means we need third party applications installed on the machines the functions will run on. The solution some module authors have chosen is to check for it on import and throw an error when it is not present. Others have chosen to prompt for an optional download of the relevant application. There is some sense in this, you likely can’t make much use of the module without that application. But it doesn’t account for situations such as an application which has restrictions, such as license costs, in place that prevent it being run on a machine that is being used to write the scripts that make use of it.</p>
<h3 id="the-solution-3">The Solution</h3>
<p>The simplest solution is to write a warning on import if you don’t detect the application or other dependency is there. This lets the user know it is needed but doesn’t prevent them from exploring the module, its commands and, importantly, the help. You can extend this further to have it documented in the Readme for the module detailing these dependencies, this should also be present on the page on the PowerShell Gallery, and its Github page if it is open source.</p>
<h2 id="ensure-your-module-manifest-has-the-correct-attributes">Ensure your module manifest has the correct attributes</h2>
<h3 id="the-problem-4">The Problem</h3>
<p>A number of modules I looked at were missing the RootModule attribute, and didn’t make use of NestedModules or the other attributes that also work for this. By missing out these attributes the module wasn’t exporting any commands, which means when I run <code class="highlighter-rouge">Get-Module MyModule -ListAvailable</code> or <code class="highlighter-rouge">Get-Command -Module MyModule</code> I get no output for what commands are available. The other common issue I’ve seen was <code class="highlighter-rouge">FunctionsToExport = @()</code> which also results in no commands being exported, even if you’ve called <code class="highlighter-rouge">Export-ModuleMember -Function</code> at the end of your psm1 file. The main cause of this is when a module developer has been working with the psm1 file and only adds the psd1 file to publish it to the PowerShell Gallery and doesn’t fully understand what all the attributes are for, they’ll keep importing just the psm1 and it works fine for them.</p>
<h3 id="the-solution-4">The Solution</h3>
<p>This is a very simple one to fix, set the RootModule attribute of your psd1 to point at your psm1 or compiled dll. If you’ve got multiple psm1 files then you can make use of the NestedModules attribute, but I’d also set one of them as the RootModule to allow you to Pester test them correctly.</p>
<p>FunctionsToExport is a bit more interesting, it should only list the functions you actually want to present to users. Keeping this up to date as you add new functions can be a bit of a pain especially with multiple people working on the module, the solution is to update it dynamically as part of your process to publish it to the gallery. The <a href="https://github.com/PoshCode/Configuration">Configuration</a> module has a very useful function for handling this called <code class="highlighter-rouge">Update-Metadata</code> and I’d highly recommend making use of it, it’ll also allow you to update the version number as you publish new versions.</p>
<h2 id="conclusion">Conclusion</h2>
<p>These are some of the problems I’ve encountered as part of the larger project I’m working on. I’m sure I’ll find more as I get closer to being finished with it so check back in future for more updates or other blog posts about them.</p>
<p>If you’ve encountered anything you think is a “worst practice”, disagree with any of these, or want alternate ways to solve some problem then feel free to tweet me <a href="https://twitter.com/halbaradkenafin">@halbaradkenafin</a> or find me on the <a href="https://j.mp/psslack">PowerShell Slack</a>.</p></content><author><name></name></author><summary type="html">It has been a while since my last post so let’s get back on track with some “interesting” things I’ve discovered while working on another project for another blog post (or two). The project involves taking almost all the modules in the PowerShell Gallery and pulling all their help data into a graph database (Neo4j) and then doing some analytics on it. That’s still in progress but I thought I’d blog about some of the worst practices I’ve seen while doing this work.</summary></entry><entry><title type="html">Transforming Measure-Object output</title><link href="https://chrislgardner.dev/powershell/2018/02/25/transforming-measure-object-output.html" rel="alternate" type="text/html" title="Transforming Measure-Object output" /><published>2018-02-25T12:00:00-06:00</published><updated>2018-02-25T12:00:00-06:00</updated><id>/powershell/2018/02/25/transforming-measure-object-output</id><content type="html" xml:base="/powershell/2018/02/25/transforming-measure-object-output.html"><p>A user on the PowerShell Slack (<a href="https://powershell.slack.com">available here</a> and <a href="https://slack.poshcode.org">invites here</a>) asked about getting specific information out of Measure-Object into a more usable PowerShell objects Their initial approach was to use Select-Object and calculatd properties to do this but I suggested a nicer way to handle it by using a short function and pipeline input.</p>
<!--more-->
<p>So the best place to start with anything like this is to get some example data and what they want the output to look like. The outputted data from Measure-Object looked like below.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell">Average Sum Maximum Minimum Property
------- --- ------- ------- --------
123.45 Interest
123456.78 Total</code></pre></figure>
<p>The way they wanted the data to look at the end was something like this:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"> Total Interest
----- --------
123456.78 123.45</code></pre></figure>
<p>To transform this data and flatten it out into a single object was quite easy. The initial attempt at doing this made use of a hashtable to build up the object and then cast it to a PsCustomObject.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nv">$hash</span> <span class="o">=</span> @<span class="o">{}</span>
<span class="k">foreach</span> <span class="o">(</span><span class="nv">$row</span> <span class="k">in</span> <span class="nv">$ComplexObject</span><span class="o">)</span> <span class="o">{</span>
<span class="nv">$Hash</span>.add<span class="o">(</span><span class="nv">$Row</span>.Property, <span class="nv">$ComplexObject</span>.Where<span class="o">({</span><span class="nv">$_</span>.Property -eq <span class="nv">$row</span>.property<span class="o">})</span>.sum<span class="o">)</span>
<span class="o">}</span>
<span class="nv">$RealObject</span> <span class="o">=</span> <span class="o">[</span>pscustomobject]<span class="nv">$Hash</span></code></pre></figure>
<p>This will take an object (in this cast <code class="highlighter-rouge">$ComplexObject</code>) and for each row in it turn that into a property of the new object (or a “column” from a visualisation help). This is a great start and achieved what we wanted but it’s not particularly reusable.</p>
<p>Lets look at making this a bit more reusable and helpful for other output from Measure-Object that we might want to work with. First up we’ll turn it into a function:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="k">function </span>Convert-MeasureObject <span class="o">{</span>
<span class="o">[</span><span class="na">cmdletbinding</span><span class="o">()]</span>
<span class="k">param</span> <span class="o">(</span>
<span class="o">[</span><span class="kt">Object</span><span class="o">[]]</span><span class="nv">$InputObject</span>,
<span class="o">[</span><span class="kt">string</span><span class="o">]</span><span class="nv">$Property</span>
<span class="o">)</span>
<span class="nv">$Hash</span> <span class="o">=</span> @<span class="o">{}</span>
<span class="k">foreach</span> <span class="o">(</span><span class="nv">$row</span> <span class="k">in</span> <span class="nv">$InputObject</span><span class="o">)</span> <span class="o">{</span>
<span class="nv">$Hash</span>.add<span class="o">(</span><span class="nv">$Row</span>.Property, <span class="nv">$InputObject</span>.Where<span class="o">({</span><span class="nv">$_</span>.Property -eq <span class="nv">$row</span>.property<span class="o">})</span>.<span class="nv">$Property</span><span class="o">)</span>
<span class="o">}</span>
<span class="o">[</span>PsCustomObject]<span class="nv">$Hash</span>
<span class="o">}</span></code></pre></figure>
<p>So now we’ve got a function that we can use by calling it with our output from Measure-Object using <code class="highlighter-rouge">Convert-MeasureObject -InputObject $MeasureObjectOutput -Property 'Sum'</code>. But this still feels a little cludgy and extra lines and variable assignments we don’t really need if we’re already using the pipeline for the Measure-Object portion of our data gathering and collation.</p>
<p>So let’s add some pipeline support and tidy things up a little:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="k">function </span>Convert-MeasureObject <span class="o">{</span>
<span class="o">[</span><span class="na">cmdletbinding</span><span class="o">()]</span>
<span class="k">param</span> <span class="o">(</span>
<span class="o">[</span>Parameter<span class="o">(</span>ValueFromPipeline<span class="o">)]</span>
<span class="o">[</span><span class="kt">Object</span><span class="o">[]]</span><span class="nv">$InputObject</span>,
<span class="o">[</span><span class="kt">string</span><span class="o">]</span><span class="nv">$Property</span>
<span class="o">)</span>
<span class="k">begin</span> <span class="o">{</span>
<span class="nv">$Hash</span> <span class="o">=</span> @<span class="o">{}</span>
<span class="o">}</span>
<span class="k">process</span> <span class="o">{</span>
<span class="k">foreach</span> <span class="o">(</span><span class="nv">$row</span> <span class="k">in</span> <span class="nv">$InputObject</span><span class="o">)</span> <span class="o">{</span>
<span class="nv">$Hash</span>.add<span class="o">(</span><span class="nv">$Row</span>.Property, <span class="nv">$InputObject</span>.Where<span class="o">({</span><span class="nv">$_</span>.Property -eq <span class="nv">$row</span>.property<span class="o">})</span>.<span class="nv">$Property</span><span class="o">)</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">end</span> <span class="o">{</span>
<span class="o">[</span>PsCustomObject]<span class="nv">$Hash</span>
<span class="o">}</span>
<span class="o">}</span></code></pre></figure>
<p>Now we’re levaraging the pipeline in a more useful way and can do wonderful things like <code class="highlighter-rouge">&lt;someInput&gt; | Measure-Object -Property 'ThatValue' -Sum | Convert-MeasureObject -Property Sum</code>.</p>
<p>It’s still a little off from a best practice approach as ideally we’d be putting each item back onto the pipeline as they come through in our process block, but the entire point of the object is to do some transformation on pipeline input we kind of need to wait for it all to come through before sending anything back out.</p>
<p>All that’s missing now is a bit of help text, some verbose logging to help people figure out what’s going on, some unit tests and then we’re good to go and can drop this in our module of choice.</p>
<p>The fully completed version of the script is available <a href="https://github.com/ChrisLGardner/PowershellScripts/tree/master/ConvertMeasureObject">here</a> along with all the tests I will have written for it.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Hopefully that’s given a nice look at how to appraoch solving an initial problem and then developing it further into a more reusable and complete solution.</p></content><author><name></name></author><summary type="html">A user on the PowerShell Slack (available here and invites here) asked about getting specific information out of Measure-Object into a more usable PowerShell objects Their initial approach was to use Select-Object and calculatd properties to do this but I suggested a nicer way to handle it by using a short function and pipeline input.</summary></entry><entry><title type="html">Learning to Teach PowerShell</title><link href="https://chrislgardner.dev/powershell/2018/01/19/learning-to-teach-powershell.html" rel="alternate" type="text/html" title="Learning to Teach PowerShell" /><published>2018-01-19T12:00:00-06:00</published><updated>2018-01-19T12:00:00-06:00</updated><id>/powershell/2018/01/19/learning-to-teach-powershell</id><content type="html" xml:base="/powershell/2018/01/19/learning-to-teach-powershell.html"><p>I spend a reasonable amount of time on the PowerShell Slack team (<a href="http://slack.poshcode.org/">click here for invites</a>) and we regularly have people new to PowerShell dropping in and asking questions, it recently got me thinking about the way in which I (and the many others) help people with these problems but also how I teach people in general.</p>
<p>Warning: This is likely to be a somewhat rambling post.</p>
<!--more-->
<h2 id="background">Background</h2>
<p>I’ve been working with PowerShell for around 4 years now, I followed probably the most common route of copying scripts from online source and hacking around them to make them work for me and then moved on to writing my own scripts and now I’m writing modules, doing unit tests, running them through CI pipelines and all those other cool things that make life with PowerShell easier. Throughout all this I’ve found that PowerShell can be difficult to learn but a few simple rules that the language follows makes it a lot easier, however teaching PowerShell can be considerably more difficult, so I thought I’d document some of the things I’ve learned while trying to teach PowerShell and some of the methods I use when helping with problems.</p>
<h2 id="learning-styles">Learning styles</h2>
<p>Having spent a number of years working in schools I picked up a few things while fixing AV during teachers’ seminars and other situations, one of them is that there are a number different ways that people learn and figuring out the way a person learns best can be very important to helping them progress through school. I’d always assumed that most people in IT based roles learned best by doing, but I wanted to get some data on that so I <a href="https://twitter.com/HalbaradKenafin/status/953328408091615232">asked twitter</a>. The sample size isn’t particularly big but the heavy slant of data towards a ‘learn by doing’ approach is the sign I was looking for.</p>
<h2 id="applying-learning-styles">Applying learning styles</h2>
<p>With this assumption in mind let’s look at some of the questions we regularly see in Slack, <a href="https://reddit.com/r/powershell">Reddit</a> and other places, how I usually deal with them and why I do it that way.</p>
<h3 id="im-new-to-powershell-whats-the-best-book-to-read">I’m new to PowerShell, what’s the best book to read</h3>
<p>This is probably the most common question (or something similar) that you’ll see on Reddit and a few other places, it’s less common on Slack but we still get the occasional one.</p>
<p>The answer to this is invariably <a href="https://www.amazon.co.uk/Learn-Windows-PowerShell-Month-Lunches/dp/1617294160/">Learn PowerShell in a Month of Lunches</a> by Don Jones and Jeff Hicks. It’s a great book and I’ve read it myself a while ago for some of the great hints and tips that even experienced PowerShell users can benefit from. More recently there is <a href="https://leanpub.com/powershell101">PowerShell 101</a> from Mike F Robbins which has received a lot of praise and based on the content of his blog I’m sure it’s a great book.</p>
<h3 id="i-want-to-do-x-with-powershell">I want to do X with PowerShell</h3>
<p>This is the more common question we get in Slack, where X can be a huge range of things. This is generally my least favourite style of question and the one I respond to in the least helpful way (at least I think so).</p>
<p>My typical way of dealing with this sort of request is to suggest some of the cmdlets that might be applicable to their problem, possibly provide some suggestions or small snippets of code that might help. A lot of the time the person at the other end has little PowerShell knowledge but hasn’t expressed that and therefore I’m trying to give them suggestions in the right direction and hopefully they’ll dive into a console or the ISE or VSCode and start playing around with it. More often they want, or need, a bit more and I’m happy to oblige if I can replicate at least part of the problem their trying to solve.</p>
<p>The problem then comes with those people who, for whatever reason, just can’t quite grasp the necessary PowerShell for the task they are trying to complete. In those cases, I try to step back a bit and break down the problem they are trying to solve into more manageable chunks. Show them how to solve a little bit at a time, show how PowerShell does things and how to tie things together and then slowly build up.</p>
<h3 id="ive-got-a-script-for-doing-y-but-it-doesnt-work">I’ve got a script for doing Y but it doesn’t work</h3>
<p>This is probably my favourite question to get, the person has put in some amount of effort to get a working solution and just needs a bit of help to get it finished or troubleshoot some issue. I often dislike troubleshooting my own code because I’ve likely been working on it for too long to notice anything obvious but other people’s code can be interesting and querying some of their choices can help both of you understand things better.</p>
<p>The way I find best to help with these issues is to take a standard troubleshooting approach, start with the error and work from there. If there are no obvious reasons the error should be occurring, then you start with the smallest amount of code necessary and slowly add more until you hit the error. Debuggers like VSCode help a lot with this, as do Pester tests for the various code paths (especially the unhappy paths through the code), once you narrow down the problem it usually becomes reasonably easy to solve it.</p>
<p>The more difficult, and potentially interesting, variety of these problems comes when someone has a script which is working but not quite how they intended it, or their approach seems very inefficient. The interesting parts come when you ask around why they are doing things in certain ways, usually it’s because they didn’t know of alternatives (often with +=’ing arrays) but there are also times when they have strange setups that require jumping through various hoops.</p>
<h2 id="what-ive-learned">What I’ve learned</h2>
<p>The big thing I’ve learned/realised is that I’m a big fan of the “teach a man to fish” approach, I’ll occasionally go with the “give a man a fish” approach when I have to but I’ll usually try giving the man a few little bits of fish first and see if he can put them together with some other bits of fish he finds lying around. There are a few of the other regulars on Slack that I’ve picked up on their usual way to help people and have a reasonable idea of if I should help out as well.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This blog post was partially inspired by <a href="https://donjones.com/2017/10/19/become-the-master-or-go-away/">Don Jones’ post last year</a> because it was becoming apparent to me that even though I’m not a master in every area of PowerShell, I know more than enough to be helping people more. It’s what really pushed me to speak at more events and it’s making me want to write more blog posts, though they’ll likely be more technical where possible because I find them to be easier to write.</p></content><author><name></name></author><summary type="html">I spend a reasonable amount of time on the PowerShell Slack team (click here for invites) and we regularly have people new to PowerShell dropping in and asking questions, it recently got me thinking about the way in which I (and the many others) help people with these problems but also how I teach people in general. Warning: This is likely to be a somewhat rambling post.</summary></entry><entry><title type="html">Find all versions of the same file</title><link href="https://chrislgardner.dev/powershell/2017/11/02/find-versions-of-the-same-file.html" rel="alternate" type="text/html" title="Find all versions of the same file" /><published>2017-11-02T12:00:00-05:00</published><updated>2017-11-02T12:00:00-05:00</updated><id>/powershell/2017/11/02/find-versions-of-the-same-file</id><content type="html" xml:base="/powershell/2017/11/02/find-versions-of-the-same-file.html"><p>While on site with a customer recently we discovered that their VSTS build for producing their Selenium tests was producing around 800MB of artifacts, this seemed pretty high for something that should just be producing a bunch of test DLLs. So I turned to PowerShell to figure out what was actually being produced and where it was.
<!--more--></p>
<p>First up was figuring out if there was any duplication of files, the folder structure looked pretty comprehensive so I guessed they had a lot of projects in Visual Studio and that suggested there might be a bit of overlap between dependencies.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nv">$TotalFiles</span> <span class="o">=</span> <span class="nb">Get-ChildItem</span> -Path .\ -Recurse -File | <span class="nb">Group-Object</span> -Property Name | <span class="nb">Sort-Object</span> -Property Count -Descending</code></pre></figure>
<p>This showed a pretty impressive list of files, lots of 1s and 2s of files named like areas of their application. However it also revealed the main source of the extra bloat; 89 copies of WebDriver.dll, almost as many copies of NewtonSoft.Json.dll and a few other similar common dlls along with the associated pdbs and more. How to deal with this though? Surely every project is using the same versions of those files? Everyone standardises to a single version of common libraries don’t they? The answer was (in reverse order) “Of course they don’t”, “Of course they aren’t” and “PowerShell of course”.</p>
<p>So how can we use PowerShell to solve this? Currently we just have a long list of files and the number of each we have. Luckily PowerShell is helpful enough to provide version information on FileInfo objects (those returned by Get-ChildItem) so with a bit of creative pipelining we can get some more useful information about the versions in use.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nv">$TotalFiles</span> | <span class="nb">Where</span>-Object <span class="o">{</span><span class="nv">$_</span>.Count -ge 5 -and <span class="nv">$_</span>.name -like <span class="k">*</span>.dll<span class="o">}</span> | <span class="k">Foreach</span>-Object <span class="o">{</span> <span class="nb">new-variable</span> <span class="nv">$_</span>.name -value <span class="o">(</span><span class="nb">Get-ChildItem</span> -Path .\ -filter <span class="nv">$_</span>.name -Recurse | <span class="nb">Select-Object</span> -ExpandProperty VersionInfo | <span class="nb">Select-Object</span> -Property ProductVersion -Unique<span class="o">)}</span></code></pre></figure>
<p>That’s a hefty pipeline to take in all at once so lets break it down a bit:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nv">$TotalFiles</span> | <span class="nb">Where</span>-Object <span class="o">{</span><span class="nv">$_</span>.Count -ge 5 -and <span class="nv">$_</span>.name -like <span class="k">*</span>.dll<span class="o">}</span></code></pre></figure>
<p>First we get all the files that end in dll, because we can guarantee the common libraries are correctly versioned, and we also only want anything that appears more than 5 times, mostly this was an arbitrary amount I decided seemed like a reasonable cutoff but it could easily be lower or higher depending on the dataset we’re working with.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="k">Foreach</span>-Object <span class="o">{</span> <span class="nb">new-variable</span> <span class="nv">$_</span>.name -value <span class="o">(</span>....<span class="o">)}</span></code></pre></figure>
<p>Then we iterate over all of those we’ve found using Foreact-Object and create a new variable named the same as the file. We don’t intend to ever directly call any of these uing $ notation but it stores the output in a convenient location that we can query pretty easily assuming our session doesn’t have a huge number of variables for some reason.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nb">Get-ChildItem</span> -Path .\ -filter <span class="nv">$_</span>.name -Recurse | <span class="nb">Select-Object</span> -ExpandProperty VersionInfo | <span class="nb">Select-Object</span> -Property ProductVersion -Unique</code></pre></figure>
<p>This is the real meat of the command, and some very inefficient meat it is as well but for my dataset of a few thousand files it wasn’t too inefficient. First we get all the files named the same as the current item in the pipeline, NewtonSoft.Json.dll for example, we then expand it’s VersionInfo property into a new object on the pipeline. Then we finish off by selecting just the ProductVersion of those items we’ve found and filtering it even further by just selecting the unique ones. All of this is stored in the variable named NewtonSoft.Json</p>
<p>So what do we do with this new information we’ve got sitting in our variables? We can query those variables and find any that have multiple versions.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nb">Get-Variable</span> | <span class="nb">Where</span>-Object <span class="o">{</span><span class="nv">$_</span>.name -like <span class="s1">'*.dll'</span> -and <span class="nv">$_</span>.value -is <span class="s1">'System.Array'</span><span class="o">}</span></code></pre></figure>
<p>This will take all our variables in the session, find any that end in .dll and that are also Arrays. We’re then presented with a (hopefully) short list of variables and their values similar to below.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell">Name Value
---- -----
NewtonSoft.Json.dll @<span class="o">({</span><span class="nv">ProductVerion</span><span class="o">=</span>9.1.0<span class="o">}</span>,<span class="o">{</span><span class="nv">ProductVersion</span><span class="o">=</span>6.5.0<span class="o">})</span></code></pre></figure>
<p>So at a glance we can see the various versions in use. We could have used -ExpandProperty on our final Select-Object statement in the Foreach loop to drop the ProductVersion from this output, I find both to be easily readable and this approach means we can call ${NewtonSoft.Json.dll} and see the usual styled object output.</p>
<h2 id="summary">Summary</h2>
<p>So as we can see it’s pretty easy to get all the versions of any number of files and then figure out how many different versions you actually have. A similar process could be applied to files without VersionInfo using Get-FileHash to compare them and detect uniqueness.</p>
<p>Managing dependency versions can be difficult, especially in larger solutions or solutions that have evolved over a longer length of time, hopefully you catch it early enough that standardising is a less time consuming experience. In the case of the customer they were in pretty good shape as they only had around 3 different versions of a few common libraries so fixing those depenencies should be a pretty easy fix. Once that’s implemented they can clean up their build process to flatten out their folders and then only produce one copy of each and save themselves a lot of space in their build artifacts and a noticeable amount of time in their release pipelines for copying these artifacts around.</p></content><author><name></name></author><summary type="html">While on site with a customer recently we discovered that their VSTS build for producing their Selenium tests was producing around 800MB of artifacts, this seemed pretty high for something that should just be producing a bunch of test DLLs. So I turned to PowerShell to figure out what was actually being produced and where it was.</summary></entry><entry><title type="html">Install MIM Portal with PowerShell</title><link href="https://chrislgardner.dev/powershell/2017/10/18/install-mim-portal-with-ps.html" rel="alternate" type="text/html" title="Install MIM Portal with PowerShell" /><published>2017-10-18T06:00:00-05:00</published><updated>2017-10-18T06:00:00-05:00</updated><id>/powershell/2017/10/18/install-mim-portal-with-ps</id><content type="html" xml:base="/powershell/2017/10/18/install-mim-portal-with-ps.html"><p>Following on from my <a href="/powershell/2017/09/21/install-mim-sync-with-ps.html">earlier post</a> about installing MIM Sync I’ve moved on to installing MIM Portal and Service via PowerShell DSC.</p>
<!--more-->
<p>MIM Portal and Service have some pretty big dependencies, the major one being SharePoint 2013 or 2016. Luckily for me the Azure marketplace has an image with this already installed but not configured, this saved me a good amount of time downloading the ISO, unpacking it and installing it, which proved to be very important when Azure has a 90 minute timeout on the DSC extension.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>MIM also depends on SQL, not a problem as we’ve already got a dedicated SQL server for Sync and SharePoint to use, but it does the dependency in a dumb way by requiring it’s “installed” on the machine running the installer. You can get around this by installing SQL Server Management Studio, I dropped the ISO into Azure Files to speed up downloading it so I can use Copy-Item rather than Invoke-WebRequest (which is very slow for large files due to caching them in memory while downloading).</p>
<p>You can access Azure Files pretty easily as a PSDrive using a simple Script block to connect:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="code"><pre>Script DownloadSSMS <span class="o">{</span>
GetScript <span class="o">=</span> <span class="o">{</span> @<span class="o">{}</span> <span class="o">}</span>
TestScript <span class="o">=</span> <span class="o">{</span><span class="nb">Test-Path</span> -Path <span class="s1">'C:\Packages\SSMS-Setup-ENU.exe'</span><span class="o">}</span>
SetScript <span class="o">=</span> <span class="o">{</span>
<span class="nv">$FileSystemCredential</span> <span class="o">=</span> <span class="nb">New-Object </span>System.Management.Automation.PSCredential <span class="o">(</span><span class="s2">"AZURE\&lt;UserName&gt;"</span>, <span class="o">(</span><span class="nb">ConvertTo-SecureString</span> <span class="s2">"&lt;AccessKey&gt;"</span> -AsPlainText -Force<span class="o">))</span>
<span class="nb">New-PSDrive</span> -Name Q -PSProvider Filesystem -Root \\&lt;StorageAccount&gt;.file.core.windows.net\&lt;Container&gt; -Credential <span class="nv">$FileSystemCredential</span>
<span class="nb">Copy-Item</span> -path <span class="s2">"Q:\SSMS-Setup-ENU.exe"</span> -Destination C:\Packages
<span class="o">}</span>
<span class="o">}</span>
Package InstallSSMS <span class="o">{</span>
Name <span class="o">=</span> <span class="s1">'SQL Server Management Studio'</span>
ProductId <span class="o">=</span> <span class="s1">'CD1FA99A-EEF9-44BE-8A89-8FB17F1C5437'</span>
Path <span class="o">=</span> <span class="s2">"C:\Packages\SSMS-Setup-ENU.exe"</span>
Arguments <span class="o">=</span> <span class="s2">"/install /quiet /norestart /log c:\Packages\ssms.log"</span>
PsDscRunAsCredential <span class="o">=</span> <span class="nv">$DomainCredentials</span>
Ensure <span class="o">=</span> <span class="s1">'Present'</span>
DependsOn <span class="o">=</span> <span class="s1">'[Script]DownloadSSMS'</span>
<span class="o">}</span></pre></td></tr></tbody></table></code></pre></figure>
<p>Ideally you’d store the access key in KeyVault or similar and pull it out at deploy time as a secure string but I’m showing the alternative method here for demo purposes. I chose to copy to C:\Packages as I know that folder will exist at run time as Azure uses it to store all the extensions that it installs and there will always be at least 1 of those, the DSC extension used here.</p>
<p>The documentation for installing MIM Portal and it’s prerequisites assume you are using <a href="https://docs.microsoft.com/en-us/microsoft-identity-manager/prepare-server-sharepoint">SharePoint 2013</a>, however a <a href="http://www.mim.ninja/2017/08/17/installing-mim-on-sharepoint-2016/">helpful blogger</a> linked a post on the changes needed for SharePoint 2016. <strong>One caveat of using SharePoint 2016 is that you will need to use MIM SP1.</strong></p>
<p>As detailed in that blog you’ll need to set the Compatability Level of the site to 15, SharePointDSC can handle this quite easily as part of the creation of the site:</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="code"><pre>SPSite MIMPortalHostSite <span class="o">{</span>
Url <span class="o">=</span> <span class="s2">"https://mim.</span><span class="nv">$DomainName</span><span class="s2">"</span>
OwnerAlias <span class="o">=</span> <span class="nv">$SharePointAdminCredential</span>.UserName
Name <span class="o">=</span> <span class="s1">'MIM'</span>
Template <span class="o">=</span> <span class="s2">"STS#1"</span>
CompatibilityLevel <span class="o">=</span> 15
PsDscRunAsCredential <span class="o">=</span> <span class="nv">$SharePointAdminCredential</span>
DependsOn <span class="o">=</span> <span class="s2">"[SPWebApplication]MIMPortalWebApp"</span>
<span class="o">}</span></pre></td></tr></tbody></table></code></pre></figure>
<p>The other change to SharePoint that needs to be made is disabling Server Side View, this is usually a simple change using the code examples in either the documentation of the blog post however with DSC this is a little less easy. There is no SharePointDSC options for it, a normal Script block won’t do it either as it needs to run as the SharePoint admin, the Credential property on Script just launches an Invoke-Command session as that user but you run into double hop issues that way and due to some issue I never quite diagnosed fully this wouldn’t work when using PSDscRunAsCredential. So my solution was to use the Script resource but create my own Invoke-Command session within it and make use of CredSSP (I’d prefer not to but the SharePoint Snapin talks to SQL), we’d enabled CredSSP earlier in our configuration for our other SharePoint resources.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="code"><pre>Script DisableServerSideView <span class="o">{</span>
GetScript <span class="o">=</span> <span class="o">{</span> @<span class="o">{}</span> <span class="o">}</span>
TestScript <span class="o">=</span> <span class="o">{</span>
<span class="nb">Test-Path</span> -Path C:\Users\sp2016_admin\Documents\ViewStateOnServer.txt
<span class="o">}</span>
SetScript <span class="o">=</span> <span class="o">{</span>
Invoke-Command -ComputerName <span class="nv">$env</span>:computername -ScriptBlock <span class="o">{</span>
Add-PSSnapin -Name Microsoft.SharePoint.Powershell
<span class="nv">$contentService</span> <span class="o">=</span> <span class="o">[</span>Microsoft.SharePoint.Administration.SPWebService]::ContentService
<span class="nv">$contentService</span>.ViewStateOnServer <span class="o">=</span> <span class="nv">$False</span>
<span class="nv">$contentService</span>.Update<span class="o">()</span>
<span class="nb">Set-Content</span> -Path C:\Users\sp2016_admin\Documents\ViewStateOnServer.txt -Value <span class="s1">'View State Changed to False'</span> -force
<span class="o">}</span> -Authentication CredSSP -Credential <span class="nv">$Using</span>:SharePointAdminCredential
<span class="o">}</span>
<span class="o">}</span></pre></td></tr></tbody></table></code></pre></figure>
<h2 id="install-portal-and-service">Install Portal And Service</h2>
<p>With all that done we finally get to the point of actually installing MIM Portal and Service itself. The <a href="https://docs.microsoft.com/en-us/microsoft-identity-manager/install-mim-service-portal">documentation</a> for this of course shows all the lovely screenshots with buttons to click and boxes to fill in, not an option via DSC so out comes Orca and a manual install using /l*v logging to figure out what all the various properties I need to tell it are.</p>
<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
</pre></td><td class="code"><pre><span class="nv">$InstallArguments</span> <span class="o">=</span> @<span class="o">(</span>