Skip to content

Commit fe28761

Browse files
author
James Brundage
committed
Adding support for Dotting (#107)
1 parent 4230b66 commit fe28761

File tree

1 file changed

+276
-0
lines changed

1 file changed

+276
-0
lines changed

Transpilers/Syntax/Dot.psx.ps1

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
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

Comments
 (0)