Skip to content

Commit

Permalink
Adding 'all' keyword (Fixes #244)
Browse files Browse the repository at this point in the history
  • Loading branch information
James Brundage committed Oct 8, 2022
1 parent fcce1c4 commit e1706ad
Show file tree
Hide file tree
Showing 2 changed files with 399 additions and 0 deletions.
338 changes: 338 additions & 0 deletions Transpilers/Keywords/All.psx.ps1
@@ -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"
}
}

0 comments on commit e1706ad

Please sign in to comment.