diff --git a/Transpilers/Keywords/All.psx.ps1 b/Transpilers/Keywords/All.psx.ps1 new file mode 100644 index 000000000..1febcbdae --- /dev/null +++ b/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" + } +} \ No newline at end of file diff --git a/Transpilers/Keywords/All.tests.ps1 b/Transpilers/Keywords/All.tests.ps1 new file mode 100644 index 000000000..9796264e7 --- /dev/null +++ b/Transpilers/Keywords/All.tests.ps1 @@ -0,0 +1,61 @@ +describe all { + context 'all is an iterator over everything' { + it 'Can get all functions' { + $allFunctions = @(& ({all functions}.Transpile())) + + $allFunctionInfos = @($allFunctions -as [Management.Automation.FunctionInfo[]]) + $allFunctionInfos.Length | Should -Be $allFunctions.Length + } + + it 'Can get all aliases' { + $allAliases = @(& ({all aliases}.Transpile())) + + $allAliasInfos = @($allAliases -as [Management.Automation.AliasInfo[]]) + $allAliasInfos.Length | Should -Be $allAliases.Length + } + + it 'Can get all applications' { + $allApps = @(& ({all applications}.Transpile())) + + $allAppInfos = @($allApps -as [Management.Automation.ApplicationInfo[]]) + $allAppInfos.Length | Should -Be $allApps.Length + } + + it 'Can get all variables' { + $allVariables = @(& ({all variables}.Transpile())) + + $allPSVariables = @($allVariables) + $allAppInfos.Length | Should -Be $allApps.Length + } + + it 'Can get all things (variables, functions, filters, aliases)' { + $allThings = @(& ({all things}.Transpile())) + $allThings.Length | Should -BeGreaterThan 50 + } + } + + it 'Can inject a typename' { + function mallard([switch]$Quack) { $Quack } + . {all functions that quack are ducks}.Transpile() + Get-Command mallard | + Get-Member | + Select-Object -ExpandProperty TypeName -Unique | + Should -be 'ducks' + } + + it 'Can filter variables' { + . { + $numbers = 1..100 + all $numbers { ($_ % 2) -eq 1 } are odd + all $numbers { ($_ % 2) -eq 0 } are even + }.Transpile() + + @( + . { all even $numbers }.Transpile() + ).Length | Should -be 50 + + @( + . { all odd $numbers }.Transpile() + ).Length | Should -be 50 + } +} \ No newline at end of file