Skip to content

Commit e1706ad

Browse files
author
James Brundage
committed
Adding 'all' keyword (Fixes #244)
1 parent fcce1c4 commit e1706ad

File tree

2 files changed

+399
-0
lines changed

2 files changed

+399
-0
lines changed

Transpilers/Keywords/All.psx.ps1

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
using namespace System.Management.Automation.Language
2+
<#
3+
.SYNOPSIS
4+
all keyword
5+
.DESCRIPTION
6+
The all keyword is a powerful way to accomplish several useful scenarios with a very natural syntax.
7+
8+
`all` can get all of a set of things that match a criteria and run one or more post-conditions.
9+
.EXAMPLE
10+
& {
11+
$glitters = @{glitters=$true}
12+
all that glitters
13+
}.Transpile()
14+
.EXAMPLE
15+
function mallard([switch]$Quack) { $Quack }
16+
Get-Command mallard | Get-Member | Select-Object -ExpandProperty TypeName -Unique
17+
. {all functions that quack are ducks}.Transpile()
18+
Get-Command mallard | Get-Member | Select-Object -ExpandProperty TypeName -Unique
19+
.EXAMPLE
20+
21+
. {
22+
$numbers = 1..100
23+
$null = all $numbers where { ($_ % 2) -eq 1 } are odd
24+
$null = all $numbers where { ($_ % 2) -eq 0 } are even
25+
}.Transpile()
26+
27+
@(
28+
. { all even $numbers }.Transpile()
29+
).Length
30+
31+
@(
32+
. { all odd $numbers }.Transpile()
33+
).Length
34+
35+
36+
#>
37+
[ValidateScript({
38+
$validateVar = $_
39+
if ($validateVar -is [CommandAst]) {
40+
$cmdAst = $validateVar
41+
if ($cmdAst.CommandElements[0].Value -eq 'all') {
42+
return $true
43+
}
44+
}
45+
return $false
46+
})]
47+
param(
48+
# If set, include all functions in the input.
49+
[Alias('Function')]
50+
[switch]
51+
$Functions,
52+
53+
# If set, include all commands in the input.
54+
[Alias('Command')]
55+
[switch]
56+
$Commands,
57+
58+
# If set, include all cmdlets in the input
59+
[Alias('Cmdlet')]
60+
[switch]
61+
$Cmdlets,
62+
63+
# If set, include all aliases in the input
64+
[Alias('Alias')]
65+
[switch]
66+
$Aliases,
67+
68+
# If set, include all applications in the input
69+
[Alias('Application')]
70+
[switch]
71+
$Applications,
72+
73+
# If set, include all variables in the inputObject.
74+
[Parameter()]
75+
[Alias('Variable')]
76+
[switch]
77+
$Variables,
78+
79+
# If set, will include all of the variables, aliases, functions, and scripts in the current directory.
80+
[Parameter()]
81+
[Alias('Thing')]
82+
[switch]
83+
$Things,
84+
85+
# The input to be searched.
86+
[Parameter(ValueFromPipelineByPropertyName,Position=0)]
87+
[Alias('In','Of', 'The','Object')]
88+
$InputObject,
89+
90+
# An optional condition
91+
[Parameter(ValueFromPipelineByPropertyName,Position=1)]
92+
[Alias('That','Condition')]
93+
$Where,
94+
95+
# The action that will be run
96+
[Parameter(ValueFromPipelineByPropertyName,Position=2)]
97+
[Alias('Is','Are','Foreach','Can','Could','Should')]
98+
$For,
99+
100+
# The Command AST
101+
[Parameter(Mandatory,ParameterSetName='CommandAST',ValueFromPipeline)]
102+
[CommandAst]
103+
$CommandAst
104+
)
105+
106+
process {
107+
108+
# Gather some information about our calling context
109+
$myParams = [Ordered]@{} + $PSBoundParameters
110+
# and attempt to parse it as a sentance (only allowing it to match this particular command)
111+
$mySentence = $commandAst.AsSentence($MyInvocation.MyCommand)
112+
$myCmd = $MyInvocation.MyCommand
113+
$myCmdName = $myCmd.Name
114+
115+
# Determine how many times we've been recursively called, so we can disambiguate variables later.
116+
$callstack = Get-PSCallStack
117+
$callCount = @($callstack |
118+
Where-Object { $_.InvocationInfo.MyCommand.Name -eq $myCmdName}).count - 1
119+
120+
# Walk thru all mapped parameters in the sentence
121+
foreach ($paramName in $mySentence.Parameters.Keys) {
122+
if (-not $myParams[$paramName]) { # If the parameter was not directly supplied
123+
$myParams[$paramName] = $mySentence.Parameters[$paramName] # grab it from the sentence.
124+
foreach ($myParam in $myCmd.Parameters.Values) {
125+
if ($myParam.Aliases -contains $paramName) { # set any variables that share the name of an alias
126+
$ExecutionContext.SessionState.PSVariable.Set($myParam.Name, $mySentence.Parameters[$paramName])
127+
}
128+
}
129+
# and set this variable for this value.
130+
$ExecutionContext.SessionState.PSVariable.Set($paramName, $mySentence.Parameters[$paramName])
131+
}
132+
}
133+
134+
# Now all of the remaining code in this transpiler should act as if we called it from the command line.
135+
136+
# Nowe we need to set up the input set
137+
$inputSet = @(
138+
$commandTypes = [Management.Automation.CommandTypes]0
139+
foreach ($myParam in $myCmd.Parameters.Values) {
140+
if ($myParam.ParameterType -eq [switch] -and
141+
$ExecutionContext.SessionState.PSVariable.Get($myParam.Name).Value) {
142+
if ($myParam.Name -replace 'e?s$' -as [Management.Automation.CommandTypes]) {
143+
$commandTypes = $commandTypes -bor [Management.Automation.CommandTypes]($myParam.Name -replace 'e?s$')
144+
}
145+
elseif ($myParam.Name -eq 'Things') {
146+
$commandTypes = $commandTypes -bor [Management.Automation.CommandTypes]'Alias,Function,Filter,Cmdlet'
147+
}
148+
elseif ($myParam.Name -eq 'Scripts') {
149+
$commandTypes = $commandTypes -bor [Management.Automation.CommandTypes]'ExternalScript'
150+
}
151+
}
152+
}
153+
154+
if ($commandTypes) {
155+
[ScriptBlock]::create("`$executionContext.SessionState.InvokeCommand.GetCommands('*','$commandTypes',`$true)")
156+
}
157+
if ($variables -or $Things) {
158+
{Get-ChildItem -Path variable:}
159+
}
160+
if ($InputObject) {
161+
if ($InputObject -is [Ast]) {
162+
if ($InputObject -is [ScriptBlockExpressionAst]) {
163+
$InputObject.ConvertFromAST()
164+
} else {
165+
$InputObject.Extent.ToString()
166+
}
167+
} else {
168+
$InputObject
169+
}
170+
}
171+
)
172+
173+
# If the sentence had unbound arguments
174+
if ($mySentence.Arguments) {
175+
if (-not $inputSet) { # and we had not yet set input
176+
$inputSet =
177+
foreach ($sentanceArg in $mySentence.Arguments) {
178+
# then anything that is not a [string] or [ScriptBlock] will become input
179+
if ($sentanceArg -isnot [string] -and $sentanceArg -isnot [ScriptBlock]) {
180+
$sentanceArg
181+
} else {
182+
# and [strings]s and [ScriptBlock]s will become -Where parameters.
183+
if (-not $Where) {
184+
$Where = $sentanceArg
185+
}
186+
else {
187+
$where = @($Where) + $sentanceArg
188+
}
189+
}
190+
}
191+
}
192+
}
193+
194+
195+
# If we still don't have an inputset, default it to 'things'
196+
if (-not $InputSet) {
197+
$InputSet =
198+
if ($mySentence.Arguments) {
199+
$mySentence.Arguments
200+
} else {
201+
{$ExecutionContext.SessionState.InvokeCommand.GetCommands('*', 'Alias,Function,Filter,Cmdlet', $true)},
202+
{Get-ChildItem -Path variable:}
203+
}
204+
}
205+
206+
# Note: there's still a lot of room for this syntax to grow and become even more natural.
207+
208+
# But with most of our arguments in hand, now we're ready to create the script
209+
210+
#region Generate Script
211+
$generatedScript = @(
212+
213+
# Create an input collection with all of our input
214+
'
215+
# Collect all items into an input collection
216+
$inputCollection =' + $(
217+
@(foreach ($setOfInput in $inputSet) {
218+
if ($setOfInput -is [ScriptBlock]) {
219+
'$(' + [Environment]::NewLine +
220+
$setOfInput + [Environment]::NewLine + ')'
221+
} else {
222+
"`$($setOfInput)"
223+
}
224+
}) -join (',' + [Environment]::NewLine + ' ')
225+
)
226+
227+
"
228+
# 'unroll' the collection by iterating over it once.
229+
`$filteredCollection = `$inputCollection =
230+
@(foreach (`$in in `$inputCollection) { `$in })
231+
"
232+
233+
if ($Where) {
234+
@(
235+
# If -Where was provided, filter the input
236+
237+
"
238+
# Since filtering conditions have been passed, we must filter item-by-item
239+
`$filteredCollection = foreach (`$item in `$inputCollection) {
240+
# we set `$this, `$psItem, and `$_ for ease-of-use.
241+
`$this = `$_ = `$psItem = `$item
242+
"
243+
foreach ($wh in $where) {
244+
if ($wh -is [ScriptBlockExpressionAst]) {
245+
$wh = $wh.ConvertFromAST()
246+
}
247+
if ($wh -is [ScriptBlock] -or $wh -is [Ast]) {
248+
"if (-not `$($($wh.Transpile())
249+
)) { continue } "
250+
}
251+
elseif ($wh -is [string]) {
252+
$safeStr = $($wh -replace "'", "''")
253+
"if (-not ( # Unless it
254+
(`$null -ne `$item.'$safeStr') -or # has a '$safeStr' property
255+
(`$null -ne `$item.value.'$safeStr') -or # or it's value has the property '$safeStr'
256+
(`$null -ne `$item.Parameters.'$safeStr') -or # or it's parameters have the property '$safeStr'
257+
(`$item.pstypenames -contains '$safeStr') # or it's typenames have the property '$safeStr'
258+
)) {
259+
continue # keep moving
260+
}"
261+
}
262+
}
263+
"
264+
`$item
265+
}"
266+
)
267+
}
268+
269+
270+
if ($For) {
271+
# If -For was
272+
"
273+
# Walk over each item in the filtered collection
274+
foreach (`$item in `$filteredCollection) {
275+
# we set `$this, `$psItem, and `$_ for ease-of-use.
276+
`$this = `$_ = `$psItem = `$item
277+
"
278+
foreach ($fo in $for) {
279+
if ($fo -is [ScriptBlockExpressionAst]) {
280+
$fo = $fo.ConvertFromAST()
281+
}
282+
283+
if ($fo -is [ScriptBlock] -or $fo -is [Ast]) {
284+
$fo.Transpile()
285+
}
286+
287+
if ($fo -is [string]) {
288+
$safeStr = $fo -replace "'", "''"
289+
"
290+
if (`$item.value -and `$item.value.pstypenames.insert) {
291+
if (`$item.value.pstypenames -notcontains '$safeStr') {
292+
`$item.value.pstypenames.insert(0, '$safeStr')
293+
}
294+
}
295+
elseif (`$item.pstypenames.insert -and `$item.pstypenames -notcontains '$safeStr') {
296+
`$item.pstypenames.insert(0, '$safeStr')
297+
}
298+
"
299+
}
300+
301+
}
302+
303+
"
304+
`$item
305+
}
306+
"
307+
} else {
308+
"`$filteredCollection"
309+
}
310+
)
311+
312+
#endregion Generate Script
313+
314+
# If the command was assigned or piped from, wrap the script in a subexpression
315+
if ($CommandAst.IsAssigned -or $CommandAst.PipelinePosition -lt $CommandAst.PipelineLength) {
316+
$generatedScript = "`$($($generatedScript -join [Environment]::NewLine))"
317+
}
318+
# If the command was piped to, wrap the script in a command expression.
319+
if ($CommandAst.IsPiped) {
320+
$generatedScript = "& { process {
321+
$generatedScript
322+
} }"
323+
}
324+
325+
# Generate the scriptblock
326+
$generatedScript = [ScriptBlock]::create(
327+
$generatedScript -join [Environment]::NewLine
328+
)
329+
330+
if (-not $generatedScript) { return }
331+
332+
# Rename the variables in the generated script, using our callstack count.
333+
.>RenameVariable -ScriptBlock $generatedScript -VariableRename @{
334+
'item' = "$('_' * $callcount)item"
335+
"filteredCollection" = "$('_' * $callcount)filteredCollection"
336+
"inputCollection" = "$('_' * $callcount)inputCollection"
337+
}
338+
}

0 commit comments

Comments
 (0)