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