Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
James Brundage
committed
Oct 8, 2022
1 parent
fcce1c4
commit e1706ad
Showing
2 changed files
with
399 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,338 @@ | ||
using namespace System.Management.Automation.Language | ||
<# | ||
.SYNOPSIS | ||
all keyword | ||
.DESCRIPTION | ||
The all keyword is a powerful way to accomplish several useful scenarios with a very natural syntax. | ||
`all` can get all of a set of things that match a criteria and run one or more post-conditions. | ||
.EXAMPLE | ||
& { | ||
$glitters = @{glitters=$true} | ||
all that glitters | ||
}.Transpile() | ||
.EXAMPLE | ||
function mallard([switch]$Quack) { $Quack } | ||
Get-Command mallard | Get-Member | Select-Object -ExpandProperty TypeName -Unique | ||
. {all functions that quack are ducks}.Transpile() | ||
Get-Command mallard | Get-Member | Select-Object -ExpandProperty TypeName -Unique | ||
.EXAMPLE | ||
. { | ||
$numbers = 1..100 | ||
$null = all $numbers where { ($_ % 2) -eq 1 } are odd | ||
$null = all $numbers where { ($_ % 2) -eq 0 } are even | ||
}.Transpile() | ||
@( | ||
. { all even $numbers }.Transpile() | ||
).Length | ||
@( | ||
. { all odd $numbers }.Transpile() | ||
).Length | ||
#> | ||
[ValidateScript({ | ||
$validateVar = $_ | ||
if ($validateVar -is [CommandAst]) { | ||
$cmdAst = $validateVar | ||
if ($cmdAst.CommandElements[0].Value -eq 'all') { | ||
return $true | ||
} | ||
} | ||
return $false | ||
})] | ||
param( | ||
# If set, include all functions in the input. | ||
[Alias('Function')] | ||
[switch] | ||
$Functions, | ||
|
||
# If set, include all commands in the input. | ||
[Alias('Command')] | ||
[switch] | ||
$Commands, | ||
|
||
# If set, include all cmdlets in the input | ||
[Alias('Cmdlet')] | ||
[switch] | ||
$Cmdlets, | ||
|
||
# If set, include all aliases in the input | ||
[Alias('Alias')] | ||
[switch] | ||
$Aliases, | ||
|
||
# If set, include all applications in the input | ||
[Alias('Application')] | ||
[switch] | ||
$Applications, | ||
|
||
# If set, include all variables in the inputObject. | ||
[Parameter()] | ||
[Alias('Variable')] | ||
[switch] | ||
$Variables, | ||
|
||
# If set, will include all of the variables, aliases, functions, and scripts in the current directory. | ||
[Parameter()] | ||
[Alias('Thing')] | ||
[switch] | ||
$Things, | ||
|
||
# The input to be searched. | ||
[Parameter(ValueFromPipelineByPropertyName,Position=0)] | ||
[Alias('In','Of', 'The','Object')] | ||
$InputObject, | ||
|
||
# An optional condition | ||
[Parameter(ValueFromPipelineByPropertyName,Position=1)] | ||
[Alias('That','Condition')] | ||
$Where, | ||
|
||
# The action that will be run | ||
[Parameter(ValueFromPipelineByPropertyName,Position=2)] | ||
[Alias('Is','Are','Foreach','Can','Could','Should')] | ||
$For, | ||
|
||
# The Command AST | ||
[Parameter(Mandatory,ParameterSetName='CommandAST',ValueFromPipeline)] | ||
[CommandAst] | ||
$CommandAst | ||
) | ||
|
||
process { | ||
|
||
# Gather some information about our calling context | ||
$myParams = [Ordered]@{} + $PSBoundParameters | ||
# and attempt to parse it as a sentance (only allowing it to match this particular command) | ||
$mySentence = $commandAst.AsSentence($MyInvocation.MyCommand) | ||
$myCmd = $MyInvocation.MyCommand | ||
$myCmdName = $myCmd.Name | ||
|
||
# Determine how many times we've been recursively called, so we can disambiguate variables later. | ||
$callstack = Get-PSCallStack | ||
$callCount = @($callstack | | ||
Where-Object { $_.InvocationInfo.MyCommand.Name -eq $myCmdName}).count - 1 | ||
|
||
# Walk thru all mapped parameters in the sentence | ||
foreach ($paramName in $mySentence.Parameters.Keys) { | ||
if (-not $myParams[$paramName]) { # If the parameter was not directly supplied | ||
$myParams[$paramName] = $mySentence.Parameters[$paramName] # grab it from the sentence. | ||
foreach ($myParam in $myCmd.Parameters.Values) { | ||
if ($myParam.Aliases -contains $paramName) { # set any variables that share the name of an alias | ||
$ExecutionContext.SessionState.PSVariable.Set($myParam.Name, $mySentence.Parameters[$paramName]) | ||
} | ||
} | ||
# and set this variable for this value. | ||
$ExecutionContext.SessionState.PSVariable.Set($paramName, $mySentence.Parameters[$paramName]) | ||
} | ||
} | ||
|
||
# Now all of the remaining code in this transpiler should act as if we called it from the command line. | ||
|
||
# Nowe we need to set up the input set | ||
$inputSet = @( | ||
$commandTypes = [Management.Automation.CommandTypes]0 | ||
foreach ($myParam in $myCmd.Parameters.Values) { | ||
if ($myParam.ParameterType -eq [switch] -and | ||
$ExecutionContext.SessionState.PSVariable.Get($myParam.Name).Value) { | ||
if ($myParam.Name -replace 'e?s$' -as [Management.Automation.CommandTypes]) { | ||
$commandTypes = $commandTypes -bor [Management.Automation.CommandTypes]($myParam.Name -replace 'e?s$') | ||
} | ||
elseif ($myParam.Name -eq 'Things') { | ||
$commandTypes = $commandTypes -bor [Management.Automation.CommandTypes]'Alias,Function,Filter,Cmdlet' | ||
} | ||
elseif ($myParam.Name -eq 'Scripts') { | ||
$commandTypes = $commandTypes -bor [Management.Automation.CommandTypes]'ExternalScript' | ||
} | ||
} | ||
} | ||
|
||
if ($commandTypes) { | ||
[ScriptBlock]::create("`$executionContext.SessionState.InvokeCommand.GetCommands('*','$commandTypes',`$true)") | ||
} | ||
if ($variables -or $Things) { | ||
{Get-ChildItem -Path variable:} | ||
} | ||
if ($InputObject) { | ||
if ($InputObject -is [Ast]) { | ||
if ($InputObject -is [ScriptBlockExpressionAst]) { | ||
$InputObject.ConvertFromAST() | ||
} else { | ||
$InputObject.Extent.ToString() | ||
} | ||
} else { | ||
$InputObject | ||
} | ||
} | ||
) | ||
|
||
# If the sentence had unbound arguments | ||
if ($mySentence.Arguments) { | ||
if (-not $inputSet) { # and we had not yet set input | ||
$inputSet = | ||
foreach ($sentanceArg in $mySentence.Arguments) { | ||
# then anything that is not a [string] or [ScriptBlock] will become input | ||
if ($sentanceArg -isnot [string] -and $sentanceArg -isnot [ScriptBlock]) { | ||
$sentanceArg | ||
} else { | ||
# and [strings]s and [ScriptBlock]s will become -Where parameters. | ||
if (-not $Where) { | ||
$Where = $sentanceArg | ||
} | ||
else { | ||
$where = @($Where) + $sentanceArg | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
||
# If we still don't have an inputset, default it to 'things' | ||
if (-not $InputSet) { | ||
$InputSet = | ||
if ($mySentence.Arguments) { | ||
$mySentence.Arguments | ||
} else { | ||
{$ExecutionContext.SessionState.InvokeCommand.GetCommands('*', 'Alias,Function,Filter,Cmdlet', $true)}, | ||
{Get-ChildItem -Path variable:} | ||
} | ||
} | ||
|
||
# Note: there's still a lot of room for this syntax to grow and become even more natural. | ||
|
||
# But with most of our arguments in hand, now we're ready to create the script | ||
|
||
#region Generate Script | ||
$generatedScript = @( | ||
|
||
# Create an input collection with all of our input | ||
' | ||
# Collect all items into an input collection | ||
$inputCollection =' + $( | ||
@(foreach ($setOfInput in $inputSet) { | ||
if ($setOfInput -is [ScriptBlock]) { | ||
'$(' + [Environment]::NewLine + | ||
$setOfInput + [Environment]::NewLine + ')' | ||
} else { | ||
"`$($setOfInput)" | ||
} | ||
}) -join (',' + [Environment]::NewLine + ' ') | ||
) | ||
|
||
" | ||
# 'unroll' the collection by iterating over it once. | ||
`$filteredCollection = `$inputCollection = | ||
@(foreach (`$in in `$inputCollection) { `$in }) | ||
" | ||
|
||
if ($Where) { | ||
@( | ||
# If -Where was provided, filter the input | ||
|
||
" | ||
# Since filtering conditions have been passed, we must filter item-by-item | ||
`$filteredCollection = foreach (`$item in `$inputCollection) { | ||
# we set `$this, `$psItem, and `$_ for ease-of-use. | ||
`$this = `$_ = `$psItem = `$item | ||
" | ||
foreach ($wh in $where) { | ||
if ($wh -is [ScriptBlockExpressionAst]) { | ||
$wh = $wh.ConvertFromAST() | ||
} | ||
if ($wh -is [ScriptBlock] -or $wh -is [Ast]) { | ||
"if (-not `$($($wh.Transpile()) | ||
)) { continue } " | ||
} | ||
elseif ($wh -is [string]) { | ||
$safeStr = $($wh -replace "'", "''") | ||
"if (-not ( # Unless it | ||
(`$null -ne `$item.'$safeStr') -or # has a '$safeStr' property | ||
(`$null -ne `$item.value.'$safeStr') -or # or it's value has the property '$safeStr' | ||
(`$null -ne `$item.Parameters.'$safeStr') -or # or it's parameters have the property '$safeStr' | ||
(`$item.pstypenames -contains '$safeStr') # or it's typenames have the property '$safeStr' | ||
)) { | ||
continue # keep moving | ||
}" | ||
} | ||
} | ||
" | ||
`$item | ||
}" | ||
) | ||
} | ||
|
||
|
||
if ($For) { | ||
# If -For was | ||
" | ||
# Walk over each item in the filtered collection | ||
foreach (`$item in `$filteredCollection) { | ||
# we set `$this, `$psItem, and `$_ for ease-of-use. | ||
`$this = `$_ = `$psItem = `$item | ||
" | ||
foreach ($fo in $for) { | ||
if ($fo -is [ScriptBlockExpressionAst]) { | ||
$fo = $fo.ConvertFromAST() | ||
} | ||
|
||
if ($fo -is [ScriptBlock] -or $fo -is [Ast]) { | ||
$fo.Transpile() | ||
} | ||
|
||
if ($fo -is [string]) { | ||
$safeStr = $fo -replace "'", "''" | ||
" | ||
if (`$item.value -and `$item.value.pstypenames.insert) { | ||
if (`$item.value.pstypenames -notcontains '$safeStr') { | ||
`$item.value.pstypenames.insert(0, '$safeStr') | ||
} | ||
} | ||
elseif (`$item.pstypenames.insert -and `$item.pstypenames -notcontains '$safeStr') { | ||
`$item.pstypenames.insert(0, '$safeStr') | ||
} | ||
" | ||
} | ||
|
||
} | ||
|
||
" | ||
`$item | ||
} | ||
" | ||
} else { | ||
"`$filteredCollection" | ||
} | ||
) | ||
|
||
#endregion Generate Script | ||
|
||
# If the command was assigned or piped from, wrap the script in a subexpression | ||
if ($CommandAst.IsAssigned -or $CommandAst.PipelinePosition -lt $CommandAst.PipelineLength) { | ||
$generatedScript = "`$($($generatedScript -join [Environment]::NewLine))" | ||
} | ||
# If the command was piped to, wrap the script in a command expression. | ||
if ($CommandAst.IsPiped) { | ||
$generatedScript = "& { process { | ||
$generatedScript | ||
} }" | ||
} | ||
|
||
# Generate the scriptblock | ||
$generatedScript = [ScriptBlock]::create( | ||
$generatedScript -join [Environment]::NewLine | ||
) | ||
|
||
if (-not $generatedScript) { return } | ||
|
||
# Rename the variables in the generated script, using our callstack count. | ||
.>RenameVariable -ScriptBlock $generatedScript -VariableRename @{ | ||
'item' = "$('_' * $callcount)item" | ||
"filteredCollection" = "$('_' * $callcount)filteredCollection" | ||
"inputCollection" = "$('_' * $callcount)inputCollection" | ||
} | ||
} |
Oops, something went wrong.