1
+ <#
2
+ . SYNOPSIS
3
+ Dot Notation
4
+ . DESCRIPTION
5
+ Dot Notation simplifies multiple operations on one or more objects.
6
+
7
+ Any command named . (followed by a letter) will be treated as the name of a method or property.
8
+
9
+ .Name will be considered the name of a property or method
10
+
11
+ If it is followed by parenthesis, these will be treated as method arguments.
12
+
13
+ If it is followed by a ScriptBlock, a dynamic property will be created that will return the result of that script block.
14
+
15
+ If any other arguments are found before the next .Name, they will be considered arguments to a method.
16
+ . EXAMPLE
17
+ .> {
18
+ [DateTime]::now | .Month .Day .Year
19
+ }
20
+ . EXAMPLE
21
+ .> {
22
+ "abc", "123", "abc123" | .Length
23
+ }
24
+ . EXAMPLE
25
+ .> { 1.99 | .ToString 'C' [CultureInfo]'gb-gb' }
26
+ . EXAMPLE
27
+ .> { 1.99 | .ToString('C') }
28
+ . EXAMPLE
29
+ 1..5 | .Number { $_ } .Even { -not ($_ % 2) } .Odd { ($_ % 2) -as [bool]}
30
+ #>
31
+ [ValidateScript ({
32
+ $commandAst = $_
33
+ $DotChainPattern = ' ^\.\p{L}'
34
+ if ($commandAst.CommandElements [0 ].Value -match ' ^\.\p{L}' ) {
35
+ return $true
36
+ }
37
+ return $false
38
+ })]
39
+ param (
40
+ [Parameter (Mandatory , ParameterSetName = ' Command' , ValueFromPipeline )]
41
+ [Management.Automation.Language.CommandAst ]
42
+ $CommandAst
43
+ )
44
+
45
+ begin {
46
+ $DotProperty = {
47
+ if ($in.PSObject.Methods [$PropertyName ].OverloadDefinitions -match ' \(\)$' ) {
48
+ $in .$PropertyName.Invoke ()
49
+ } elseif ($in.PSObject.Properties [$PropertyName ]) {
50
+ $in .$PropertyName
51
+ }
52
+ }
53
+
54
+ $DotMethod = { $in .$MethodName ($MethodArguments ) }
55
+ }
56
+
57
+ process {
58
+
59
+ # Create a collection for the entire chain of operations and their arguments.
60
+ $DotChain = @ ()
61
+ $DotArgsAst = @ ()
62
+ $DotChainPart = ' '
63
+ $DotChainPattern = ' ^\.\p{L}'
64
+
65
+ # Then, walk over each element of the commands
66
+ $CommandElements = $CommandAst.CommandElements
67
+ for ( $elementIndex = 0 ; $elementIndex -lt $CommandElements.Count ; $elementIndex ++ ) {
68
+ # If we are on the first element, or the command element starts with the DotChainPattern
69
+ if ($elementIndex -eq 0 -or $CommandElements [$elementIndex ].Value -match $DotChainPattern ) {
70
+ if ($DotChainPart ) {
71
+ $DotChain += [PSCustomObject ]@ {
72
+ PSTypeName = ' PipeScript.Dot.Chain'
73
+ Name = $DotChainPart
74
+ Arguments = $DotArgsAst
75
+ }
76
+ }
77
+
78
+ $DotArgsAst = @ ()
79
+
80
+ # A given step started with dots can have more than one step in the chain specified.
81
+ $elementDotChain = $CommandElements [$elementIndex ].Value.Split(' .' )
82
+ [Array ]::Reverse($elementDotChain )
83
+ $LastElement , $otherElements = $elementDotChain
84
+ if ($otherElements ) {
85
+ foreach ($element in $otherElements ) {
86
+ $DotChain += [PSCustomObject ]@ {
87
+ PSTypeName = ' PipeScript.Dot.Chain'
88
+ Name = $element
89
+ Arguments = @ ()
90
+ }
91
+ }
92
+ }
93
+
94
+ $DotChainPart = $LastElement
95
+ }
96
+ # If we are not on the first index or the element does not start with a dot, it is an argument.
97
+ else {
98
+ $DotArgsAst += $CommandElements [$elementIndex ]
99
+ }
100
+ }
101
+
102
+ if ($DotChainPart ) {
103
+ $DotChain += [PSCustomObject ]@ {
104
+ PSTypeName = ' PipeScript.Dot.Chain'
105
+ Name = $DotChainPart
106
+ Arguments = $DotArgsAst
107
+ }
108
+ }
109
+
110
+
111
+ $NewScript = @ ()
112
+ $indent = 0
113
+ $WasPipedTo =
114
+ $CommandAst.Parent -and
115
+ $CommandAst.Parent.PipelineElements -and
116
+ $CommandAst.Parent.PipelineElements.IndexOf ($CommandAst ) -gt 0
117
+
118
+
119
+ # By default, we are not creating a property bag.
120
+ # This default will change if:
121
+ # * More than one property is defined
122
+ # * A property is explicitly assigned
123
+ $isPropertyBag = $false
124
+
125
+ # If we were piped to, adjust indent (for now)
126
+ if ($WasPipedTo ) {
127
+ $indent += 4
128
+ }
129
+
130
+ # Declare the start of the chain (even if we don't use it)
131
+ $propertyBagStart = (' ' * $indent ) + ' [PSCustomObject][Ordered]@{'
132
+ # and keep track of all items we must post process.
133
+ $PostProcess = @ ()
134
+
135
+ # If more than one item was in the chain
136
+ if ($DotChain.Length -ge 0 ) {
137
+ $indent += 4 # adjust indentation
138
+ }
139
+
140
+ # Walk thru all items in the chain of properties.
141
+ foreach ($Dot in $DotChain ) {
142
+ $firstDotArg , $secondDotArg , $restDotArgs = $dot.Arguments
143
+ # Determine what will be the segment of the dot chain.
144
+ $thisSegement =
145
+ # If the dot chain has no arguments, treat it as a property
146
+ if (-not $dot.Arguments ) {
147
+ $DotProperty -replace ' \$PropertyName' , " '$ ( $dot.Name ) '"
148
+ }
149
+ # If the dot chain's first argument is an assignment
150
+ elseif ($firstDotArg -is [Management.Automation.Language.StringConstantExpressionAst ] -and
151
+ $firstDotArg.Value -eq ' =' ) {
152
+ $isPropertyBag = $true
153
+ # and the second is a script block
154
+ if ($secondDotArg -is [Management.Automation.Language.ScriptBlockExpressionAst ]) {
155
+ # it will become either a [ScriptMethod] or [ScriptProperty]
156
+ $secondScriptBlock = [ScriptBlock ]::Create(
157
+ $secondDotArg.Extent.ToString () -replace ' ^\{' -replace ' \}$'
158
+ )
159
+
160
+ # If the script block had parameters (even if they were empty parameters)
161
+ # It should become a ScriptMethod
162
+ if ($secondScriptBlock.Ast.ParamBlock ) {
163
+ " [PSScriptMethod]::New('$ ( $dot.Name ) ', $secondDotArg )"
164
+ } else {
165
+ # Otherwise, it will become a ScriptProperty
166
+ " [PSScriptProperty]::New('$ ( $dot.Name ) ', $secondDotArg )"
167
+ }
168
+ $PostProcess += $dot.Name
169
+ }
170
+ # If we had an array of arguments, and both elements were ScriptBlocks
171
+ elseif ($secondDotArg -is [Management.Automation.Language.ArrayLiteralAst ] -and
172
+ $secondDotArg.Elements.Count -eq 2 -and
173
+ $secondDotArg.Elements [0 ] -is [Management.Automation.Language.ScriptBlockExpressionAst ] -and
174
+ $secondDotArg.Elements [1 ] -is [Management.Automation.Language.ScriptBlockExpressionAst ]
175
+ ) {
176
+ # Then we will treat this as a settable script block
177
+ $PostProcess += $dot.Name
178
+ " [PSScriptProperty]::New('$ ( $dot.Name ) ', $ ( $secondDotArg.Elements [0 ]) , $ ( $secondDotArg.Elements [1 ]) )"
179
+ }
180
+ elseif (-not $restDotArgs ) {
181
+ # Otherwise, if we only have one argument, use the expression directly
182
+ $secondDotArg.Extent.ToString ()
183
+ } elseif ($restDotArgs ) {
184
+ # Otherwise, if we had multiple values, create a list.
185
+ @ (
186
+ $secondDotArg.Extent.ToString ()
187
+ foreach ($otherDotArg in $restDotArgs ) {
188
+ $otherDotArg.Extent.Tostring ()
189
+ }
190
+ ) -join ' ,'
191
+ }
192
+ }
193
+ # If the dot chain's first argument is a ScriptBlock
194
+ elseif ($firstDotArg -is [Management.Automation.Language.ScriptBlockExpressionAst ])
195
+ {
196
+ # Run that script block
197
+ " & $ ( $firstDotArg.Extent.ToString ()) "
198
+ }
199
+ elseif ($firstDotArg -is [Management.Automation.Language.ParenExpressionAst ]) {
200
+ # If the first argument is a parenthesis, assume the contents to be method arguments
201
+ $DotMethod -replace ' \$MethodName' , $dot.Name -replace ' \(\$MethodArguments\)' , $firstDotArg.ToString ()
202
+ }
203
+ else {
204
+ # If the first argument is anything else, assume all remaining arguments to be method parameters.
205
+ $DotMethod -replace ' \$MethodName' , $dot.Name -replace ' \(\$MethodArguments\)' , (
206
+ ' (' + ($dot.Arguments -join ' ,' ) + ' )'
207
+ )
208
+ }
209
+
210
+ # Now we add the segment to the total script
211
+ $NewScript +=
212
+ if (-not $isPropertyBag -and $DotChain.Length -eq 1 -and $thisSegement -notmatch ' ^\[PS' ) {
213
+ # If the dot chain is a single item, and not part of a property bag, include it directly
214
+ " $ ( ' ' * ($indent - 4 )) $thisSegement "
215
+ } else {
216
+
217
+ $isPropertyBag = $true
218
+ # Otherwise include this segment as a hashtable assignment with the correct indentation.
219
+ $thisSegement = @ ($thisSegement -split ' [\r\n]+' -ne ' ' -replace ' $' , (' ' * 8 )) -join [Environment ]::NewLine
220
+ @"
221
+ $ ( ' ' * $indent ) '$ ( $dot.Name.Replace (" '" , " ''" )) ' =
222
+ $ ( ' ' * ($indent + 4 )) $thisSegement
223
+ "@
224
+ }
225
+ }
226
+
227
+
228
+ # If we were generating a property bag
229
+ if ($isPropertyBag ) {
230
+ if ($WasPipedTo ) { # and it was piped to
231
+ # Add the start of the pipeline and the property bag start to the top of the script.
232
+ $NewScript = @ (' & { process {' ) + ((' ' * $indent ) + ' $in = $this = $_' ) + $propertyBagStart + $NewScript
233
+ } else {
234
+ # If it was not piped to, just add the start of the property bag
235
+ $newScript = @ ($propertyBagStart ) + $NewScript
236
+ }
237
+ } elseif ($WasPipedTo ) {
238
+ # If we were piped to (but were not a property bag)
239
+ $indent -= 4
240
+ # add the start of the pipeline to the top of the script.
241
+ $newScript = @ (' & { process {' ) + ((' ' * $indent ) + ' $in = $this = $_' ) + $NewScript
242
+ }
243
+
244
+ # If we were a property bag
245
+ if ($isPropertyBag ) {
246
+ # close out the script
247
+ $NewScript += ($ (' ' * $indent ) + ' }' )
248
+ $indent -= 4
249
+ }
250
+
251
+ # If there was post processing
252
+ if ($PostProcess ) {
253
+ # Change the property bag start to assign it to a variable
254
+ $NewScript = $newScript -replace ($propertyBagStart -replace ' \W' , ' \$0' ), " `$ Out = $propertyBagStart "
255
+ foreach ($post in $PostProcess ) {
256
+ # and change any [PSScriptProperty] or [PSScriptMethod] into a method on that object.
257
+ $newScript += " `$ Out.PSObject.Members.Add(`$ out.$Post )"
258
+ }
259
+ # Then output.
260
+ $NewScript += ' $Out'
261
+ }
262
+
263
+ # If we were piped to
264
+ if ($WasPipedTo ) {
265
+ # close off the script.
266
+ $NewScript += ' } }'
267
+ } else {
268
+ # otherwise, make it a subexpression
269
+ $NewScript = ' $(' + ($NewScript -join [Environment ]::NewLine) + ' )'
270
+ }
271
+
272
+ $NewScript = $NewScript -join [Environment ]::Newline
273
+
274
+ # Return the created script.
275
+ [scriptblock ]::Create($NewScript )
276
+ }
0 commit comments