diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c6437cb8..ee515f003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## 0.0.14: +* New Transpilers: + * [RemoveParameter] (#159) + * [RenameVariable] (#160) +* Keyword Updates: + * new now supports extended type creation (#164) + * until now supports a TimeSpan, DateTime, or EventName string (#153) +* AST Extended Type Enhancements: + * [TypeConstraintAst] and [AttributeAst] now have .ResolvedCommand (#162) +* Action Updates + * Pulling just before push (#163) + * Not running when there is not a current branch (#158) + * Improving email determination (#156) +* Invoke-PipeScript terminates transpiler errors when run interactively (#161) +--- + ## 0.0.13: * New / Improved Keywords * assert keyword (Support for pipelines) (#143) diff --git a/Github/Actions/PipeScriptAction.ps1 b/Github/Actions/PipeScriptAction.ps1 index b59a5cc2c..1658415d7 100644 --- a/Github/Actions/PipeScriptAction.ps1 +++ b/Github/Actions/PipeScriptAction.ps1 @@ -100,13 +100,30 @@ $processScriptOutput = { process { if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } -if (-not $UserEmail) { $UserEmail = "$UserName@github.com" } +if (-not $UserEmail) { + $GitHubUserEmail = + if ($env:GITHUB_TOKEN) { + Invoke-RestMethod -uri "https://api.github.com/user/emails" -Headers @{ + Authorization = "token $env:GITHUB_TOKEN" + } | + Select-Object -First 1 -ExpandProperty email + } else {''} + $UserEmail = + if ($GitHubUserEmail) { + $GitHubUserEmail + } else { + "$UserName@github.com" + } +} git config --global user.email $UserEmail git config --global user.name $UserName if (-not $env:GITHUB_WORKSPACE) { throw "No GitHub workspace" } -git pull | Out-Host +$branchName = git rev-parse --abrev-ref HEAD +if (-not $branchName) { + return +} $PipeScriptStart = [DateTime]::Now if ($PipeScript) { @@ -143,11 +160,15 @@ if ($CommitMessage -or $anyFilesChanged) { git commit -m $ExecutionContext.SessionState.InvokeCommand.ExpandString($CommitMessage) } + + $checkDetached = git symbolic-ref -q HEAD if (-not $LASTEXITCODE) { + "::notice::Pulling Changes" | Out-Host + git pull | Out-Host "::notice::Pushing Changes" | Out-Host - git push + git push | Out-Host "Git Push Output: $($gitPushed | Out-String)" } else { "::notice::Not pushing changes (on detached head)" | Out-Host diff --git a/Invoke-PipeScript.ps1 b/Invoke-PipeScript.ps1 index 3d1752931..53f0d9e42 100644 --- a/Invoke-PipeScript.ps1 +++ b/Invoke-PipeScript.ps1 @@ -87,7 +87,7 @@ if ($TypeName.IsGeneric) { $TypeNameParams[$typeName.Name] = $typeName.GenericArguments | - UnpackTypeConstraintArgs + TypeConstraintToArguments } elseif (-not $TypeName.IsArray) { $TypeNameArgs += $TypeName.Name } @@ -127,7 +127,7 @@ # If the command is a ```[ScriptBlock]``` - if ($Command -is [scriptblock]) + if ($Command -is [scriptblock]) { # Attempt to transpile it. $TranspiledScriptBlock = $Command | .>Pipescript @ErrorsAndWarnings @@ -142,8 +142,28 @@ }) return } + + if ($TranspilerErrors) { + $failedMessage = @( + "$($command.Source): " + "$($TranspilerErrors.Count) error(s)" + if ($transpilerWarnings) { + "$($TranspilerWarnings.Count) warning(s)" + } + ) -join ',' + Write-Error $failedMessage -ErrorId Build.Failed -TargetObject ( + [PSCustomObject][ordered]@{ + Output = $pipescriptOutput + Errors = $TranspilerErrors + Warnings = $TranspilerWarnings + Command = $Command + Parameters = $InvokePipeScriptParameters + } + ) + } + # If it could not be transpiled into a [ScriptBlock] or [ScriptBlock[]] - if ($transpiledScriptBlock -isnot [ScriptBlock] -and -not ($TranspiledScriptBlock -as [scriptblock[]])) { + if ($TranspilerErrors -or + ($transpiledScriptBlock -isnot [ScriptBlock] -and -not ($TranspiledScriptBlock -as [scriptblock[]]))) { # error out. Write-Error "Command {$command} could not be transpiled into [ScriptBlock]s" return diff --git a/New-PipeScript.ps1 b/New-PipeScript.ps1 index aeef5702a..7e75f1d8a 100644 --- a/New-PipeScript.ps1 +++ b/New-PipeScript.ps1 @@ -123,7 +123,16 @@ function New-PipeScript { # The script header. [Parameter(ValueFromPipelineByPropertyName)] [string] - $Header + $Header, + # If provided, will automatically create parameters. + # Parameters will be automatically created for any unassigned variables. + [Alias('AutoParameterize','AutoParameters')] + [switch] + $AutoParameter, + # The type used for automatically generated parameters. + # By default, ```[PSObject]```. + [type] + $AutoParameterType = [PSObject] ) begin { $ParametersToCreate = [Ordered]@{} @@ -228,6 +237,24 @@ function New-PipeScript { # accumulate them. $allEndBlocks += $end } + if ($AutoParameter) { + $variableDefinitions = $Begin, $Process, $End | + Where-Object { $_ } | + Search-PipeScript -AstType VariableExpressionAST | + Select-Object -ExpandProperty Result + foreach ($var in $variableDefinitions) { + $assigned = $var.GetAssignments() + if ($assigned) { continue } + $varName = $var.VariablePath.userPath.ToString() + $ParametersToCreate[$varName] = @( + @( + "[Parameter(ValueFromPipelineByPropertyName)]" + "[$($AutoParameterType.FullName -replace '^System\.')]" + "$var" + ) -join [Environment]::NewLine + ) + } + } } end { # Take all of the accumulated parameters and create a parameter block diff --git a/New-PipeScript.ps1.ps1 b/New-PipeScript.ps1.ps1 index 16c827a4b..457c51953 100644 --- a/New-PipeScript.ps1.ps1 +++ b/New-PipeScript.ps1.ps1 @@ -51,7 +51,18 @@ # The script header. [Parameter(ValueFromPipelineByPropertyName)] [string] - $Header + $Header, + + # If provided, will automatically create parameters. + # Parameters will be automatically created for any unassigned variables. + [Alias('AutoParameterize','AutoParameters')] + [switch] + $AutoParameter, + + # The type used for automatically generated parameters. + # By default, ```[PSObject]```. + [type] + $AutoParameterType = [PSObject] ) begin { @@ -164,6 +175,24 @@ $allEndBlocks += $end } + if ($AutoParameter) { + $variableDefinitions = $Begin, $Process, $End | + Where-Object { $_ } | + Search-PipeScript -AstType VariableExpressionAST | + Select-Object -ExpandProperty Result + foreach ($var in $variableDefinitions) { + $assigned = $var.GetAssignments() + if ($assigned) { continue } + $varName = $var.VariablePath.userPath.ToString() + $ParametersToCreate[$varName] = @( + @( + "[Parameter(ValueFromPipelineByPropertyName)]" + "[$($AutoParameterType.FullName -replace '^System\.')]" + "$var" + ) -join [Environment]::NewLine + ) + } + } } end { diff --git a/PipeScript.format.ps1xml b/PipeScript.format.ps1xml index 18f751ba7..d838f4e77 100644 --- a/PipeScript.format.ps1xml +++ b/PipeScript.format.ps1xml @@ -1,5 +1,5 @@ - + diff --git a/PipeScript.psd1 b/PipeScript.psd1 index 296d9747e..b93e8da22 100644 --- a/PipeScript.psd1 +++ b/PipeScript.psd1 @@ -1,5 +1,5 @@ @{ - ModuleVersion = '0.0.13' + ModuleVersion = '0.0.14' Description = 'An Extensible Transpiler for PowerShell (and anything else)' RootModule = 'PipeScript.psm1' PowerShellVersion = '4.0' @@ -19,6 +19,22 @@ BuildModule = @('EZOut','Piecemeal','PipeScript','HelpOut', 'PSDevOps') Tags = 'PipeScript','PowerShell', 'Transpilation', 'Compiler' ReleaseNotes = @' +## 0.0.14: +* New Transpilers: + * [RemoveParameter] (#159) + * [RenameVariable] (#160) +* Keyword Updates: + * new now supports extended type creation (#164) + * until now supports a TimeSpan, DateTime, or EventName string (#153) +* AST Extended Type Enhancements: + * [TypeConstraintAst] and [AttributeAst] now have .ResolvedCommand (#162) +* Action Updates + * Pulling just before push (#163) + * Not running when there is not a current branch (#158) + * Improving email determination (#156) +* Invoke-PipeScript terminates transpiler errors when run interactively (#161) +--- + ## 0.0.13: * New / Improved Keywords * assert keyword (Support for pipelines) (#143) diff --git a/PipeScript.types.ps1xml b/PipeScript.types.ps1xml index 2282fe015..656edc3b9 100644 --- a/PipeScript.types.ps1xml +++ b/PipeScript.types.ps1xml @@ -1,5 +1,5 @@ - + System.Management.Automation.Language.Ast @@ -110,6 +110,57 @@ foreach ($attributeArg in $argsInOrder) { return $Parameter + + ResolvedCommand + + <# +.SYNOPSIS + Resolves an Attribute to a CommandInfo +.DESCRIPTION + Resolves an Attribute to one or more CommandInfo. +.EXAMPLE + { + [InvokePipeScript()]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [Microsoft.PowerShell.Core.GetCommand()]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [Get_Command()]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [GetCommand()]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [cmd()]$null + }.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +#> +# Get the name of the transpiler. +$transpilerStepName = + if ($this.TypeName.IsGeneric) { + $this.TypeName.TypeName.Name + } else { + $this.TypeName.Name + } +$decamelCase = [Regex]::new('(?<=[a-z])(?=[A-Z])') +@( + # If a Transpiler exists by that name, it will be returned first. + Get-Transpiler -TranspilerName $transpilerStepName + # Then, any periods in the attribute name will be converted to slashes, + $fullCommandName = $transpilerStepName -replace '\.','\' -replace + '_','-' # and any underscores to dashes. + + # Then, the first CamelCased code will have a - injected in between the CamelCase. + $fullCommandName = $decamelCase.Replace($fullCommandName, '-', 1) + # Now we will try to find the command. + $ExecutionContext.SessionState.InvokeCommand.GetCommand($fullCommandName, 'All') +) + + @@ -199,6 +250,12 @@ $this.Parent.PipelineElements.IndexOf($this) + + ResolvedCommand + + $ExecutionContext.SessionState.InvokeCommand.GetCommand($this.CommandElements[0].ToString(), 'All') + + @@ -286,6 +343,62 @@ else { + + System.Management.Automation.Language.TypeConstraintAst + + + ResolvedCommand + + <# +.SYNOPSIS + Resolves an TypeConstraintAST to a CommandInfo +.DESCRIPTION + Resolves an TypeConstraintAST to one or more CommandInfo Objects. +.EXAMPLE + { + [InvokePipeScript[a]]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [Microsoft.PowerShell.Core.GetCommand]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [Get_Command]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [GetCommand]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [cmd]$null + }.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +#> +# Get the name of the transpiler. +$transpilerStepName = + if ($this.TypeName.IsGeneric) { + $this.TypeName.TypeName.Name + } else { + $this.TypeName.Name + } +$decamelCase = [Regex]::new('(?<=[a-z])(?=[A-Z])') +@( + # If a Transpiler exists by that name, it will be returned first. + Get-Transpiler -TranspilerName $transpilerStepName + # Then, any periods in the attribute name will be converted to slashes, + $fullCommandName = $transpilerStepName -replace '\.','\' -replace + '_','-' # and any underscores to dashes. + + # Then, the first CamelCased code will have a - injected in between the CamelCase. + $fullCommandName = $decamelCase.Replace($fullCommandName, '-', 1) + # Now we will try to find the command. + $ExecutionContext.SessionState.InvokeCommand.GetCommand($fullCommandName, 'All') +) + + + + System.Management.Automation.Language.VariableExpressionAst @@ -300,6 +413,179 @@ if ($this.variablePath.userPath -in 'true', 'false', 'null') { $this } + + + + GetAssignments + + + + GetVariableType + diff --git a/Transpilers/Keywords/New.psx.ps1 b/Transpilers/Keywords/New.psx.ps1 index b58cf4d1f..5117d8682 100644 --- a/Transpilers/Keywords/New.psx.ps1 +++ b/Transpilers/Keywords/New.psx.ps1 @@ -31,6 +31,8 @@ .> { new ScriptBlock 'Get-Command'} .EXAMPLE .> { (new PowerShell).AddScript("Get-Command").Invoke() } +.EXAMPLE + .> { new 'https://schema.org/Thing' } #> [ValidateScript({ $CommandAst = $_ @@ -46,7 +48,7 @@ process { $null, $newTypeName, $newArgs = $CommandAst.CommandElements $maybeGeneric = $false $propertiesToCreate = @() - + $newTypeNameAst = $newTypeName $newTypeName = # Non-generic types will be a bareword constant if ($newTypeName.Value) { @@ -54,7 +56,7 @@ process { } elseif ($newTypeName -is [Management.Automation.Language.HashtableAst]) { $propertiesToCreate += $newTypeName - } + } else { # generic types will be an ArrayLiteralAst $maybeGeneric = $true @@ -135,7 +137,14 @@ process { } } elseif ($propertiesToCreate.Count -eq 1 -and -not $newTypeName) { "[PSCustomObject][Ordered]$propertiesToCreate" - } else { + } elseif ($newTypeNameAst -is [Management.Automation.Language.StringConstantExpressionAst]) { + if ($propertiesToCreate) { + "[PSCustomObject]([Ordered]@{PSTypeName=$newTypeNameAst} + ([Ordered]$propertiesToCreate))" + } else { + "[PSCustomObject][Ordered]@{PSTypeName=$newTypeNameAst}" + } + } + else { Write-Error "Unknown type '$newTypeName'" return } diff --git a/Transpilers/Keywords/README.md b/Transpilers/Keywords/README.md index 50c86071b..a7359481e 100644 --- a/Transpilers/Keywords/README.md +++ b/Transpilers/Keywords/README.md @@ -135,6 +135,13 @@ Most keywords will be implemented as a Transpiler that tranforms a CommandAST. .> { (new PowerShell).AddScript("Get-Command").Invoke() } ~~~ +## New Example 10 + + +~~~PowerShell + .> { new 'https://schema.org/Thing' } +~~~ + ## Until Example 1 @@ -152,17 +159,49 @@ Most keywords will be implemented as a Transpiler that tranforms a CommandAST. ~~~PowerShell - { + Invoke-PipeScript { until "00:00:05" { [DateTime]::Now Start-Sleep -Milliseconds 500 } - } | .>PipeScript + } ~~~ ## Until Example 3 +~~~PowerShell + Invoke-PipeScript { + until "12:17 pm" { + [DateTime]::Now + Start-Sleep -Milliseconds 500 + } + } +~~~ + +## Until Example 4 + + +~~~PowerShell + { + $eventCounter = 0 + until "MyEvent" { + $eventCounter++ + $eventCounter + until "00:00:03" { + "sleeping a few seconds" + Start-Sleep -Milliseconds 500 + } + if (-not ($eventCounter % 5)) { + $null = New-Event -SourceIdentifier MyEvent + } + } + } | .>PipeScript +~~~ + +## Until Example 5 + + ~~~PowerShell Invoke-PipeScript { $tries = 3 diff --git a/Transpilers/Keywords/Until.psx.ps1 b/Transpilers/Keywords/Until.psx.ps1 index bae01957c..c07e846ec 100644 --- a/Transpilers/Keywords/Until.psx.ps1 +++ b/Transpilers/Keywords/Until.psx.ps1 @@ -16,11 +16,33 @@ } } |.>PipeScript .EXAMPLE - { + Invoke-PipeScript { until "00:00:05" { [DateTime]::Now Start-Sleep -Milliseconds 500 } + } +.EXAMPLE + Invoke-PipeScript { + until "12:17 pm" { + [DateTime]::Now + Start-Sleep -Milliseconds 500 + } + } +.EXAMPLE + { + $eventCounter = 0 + until "MyEvent" { + $eventCounter++ + $eventCounter + until "00:00:03" { + "sleeping a few seconds" + Start-Sleep -Milliseconds 500 + } + if (-not ($eventCounter % 5)) { + $null = New-Event -SourceIdentifier MyEvent + } + } } | .>PipeScript .EXAMPLE Invoke-PipeScript { @@ -48,6 +70,10 @@ param( $CommandAst ) +begin { + $myCmdName = $MyInvocation.MyCommand.Name +} + process { $CommandName, $CommandArgs = $commandAst.CommandElements if ($commandName -like ':*') { @@ -60,7 +86,9 @@ process { # If the first arg is a command expression, it becomes do {} while ($firstArg) if (-not $firstArg -or $firstArg.GetType().Name -notin 'ParenExpressionAst', 'ScriptBlockExpressionAst', - 'VariableExpressionAst','MemberExpressionAst','ExpandableStringExpressionAst') { + 'VariableExpressionAst','MemberExpressionAst', + 'string', + 'ExpandableStringExpressionAst') { Write-Error "Until must be followed by a Variable, Member, ExpandableString, or Parenthesis Expression" return } @@ -75,13 +103,37 @@ process { $firstArg.Pipeline.PipelineElements.Count -eq 1 -and $firstArg.Pipeline.PipelineElements[0].Expression -and $firstArg.Pipeline.PipelineElements[0].Expression.GetType().Name -in - 'VariableExpressionAst','MemberExpressionAst','ExpandableStringExpressionAst') { + 'VariableExpressionAst','MemberExpressionAst','ExpandableStringExpressionAst', + 'StringConstantExpressionAst') { $condition = $firstArg.Pipeline.PipelineElements[0].Expression } elseif ($firstArg.GetType().Name -eq 'ScriptBlockExpressionAst') { $condition = $firstArg -replace '^\{' -replace '\}$' } + $BeforeLoop = '' + + $callstack = Get-PSCallStack + $callCount = @($callstack | + Where-Object { $_.InvocationInfo.MyCommand.Name -eq $myCmdName}).count - 1 + $untilVar = '$' + ('_' * $callCount) + 'untilStartTime' + + if ($condition -is [string]) { + + + if ($condition -as [Timespan]) { + $beforeLoop = "$untilVar = [DateTime]::Now" + $condition = "(([DateTime]::Now - $untilVar) -ge ([Timespan]'$Condition'))" + } + elseif ($condition -as [DateTime]) { + $condition = "[DateTime]::Now -ge ([DateTime]'$Condition')" + } + else { + $beforeLoop = "$untilVar = [DateTime]::Now" + $condition = 'Get-Event -SourceIdentifier ' + "'$condition'" + " -ErrorAction Ignore | Where-Object TimeGenerated -ge $untilVar" + } + } + $conditionScript = [ScriptBlock]::Create($condition) $LoopScript = $secondArg @@ -98,6 +150,7 @@ process { $newScript = @" +$(if ($BeforeLoop) { $BeforeLoop + [Environment]::NewLine}) $(if ($CommandName -like ':*') { "$CommandName "})do { $untilTranspiled } while $conditionScript diff --git a/Transpilers/Parameters/README.md b/Transpilers/Parameters/README.md index 4a0d8d561..1e555d11b 100644 --- a/Transpilers/Parameters/README.md +++ b/Transpilers/Parameters/README.md @@ -18,6 +18,7 @@ When this is the case it is common for the transpiler to add a ```[ValidateScrip |DisplayName |Synopsis | |----------------------------------------------------|---------------------------------------------------------------------| |[Aliases](Aliases.psx.ps1) |[Dynamically Defines Aliases](Aliases.psx.ps1) | +|[RemoveParameter](RemoveParameter.psx.ps1) |[Removes Parameters from a ScriptBlock](RemoveParameter.psx.ps1) | |[ValidateExtension](ValidateExtension.psx.ps1) |[Validates Extensions](ValidateExtension.psx.ps1) | |[ValidatePlatform](ValidatePlatform.psx.ps1) |[Validates the Platform](ValidatePlatform.psx.ps1) | |[ValidatePropertyName](ValidatePropertyName.psx.ps1)|[Validates Property Names](ValidatePropertyName.psx.ps1) | diff --git a/Transpilers/Parameters/RemoveParameter.psx.ps1 b/Transpilers/Parameters/RemoveParameter.psx.ps1 new file mode 100644 index 000000000..8a9180c1a --- /dev/null +++ b/Transpilers/Parameters/RemoveParameter.psx.ps1 @@ -0,0 +1,28 @@ +<# +.SYNOPSIS + Removes Parameters from a ScriptBlock +.DESCRIPTION + Removes Parameters from a ScriptBlock +.EXAMPLE + { + [RemoveParameter("x")] + param($x, $y) + } | .>PipeScript +.LINK + Update-PipeScript +#> +param( +# The name of one or more parameters to remove +[Parameter(Mandatory,Position=0)] +[string[]] +$ParameterName, + +# The ScriptBlock that declares the parameters. +[Parameter(Mandatory,ValueFromPipeline)] +[scriptblock] +$ScriptBlock +) + +process { + Update-PipeScript -ScriptBlock $ScriptBlock -RemoveParameter $ParameterName +} diff --git a/Transpilers/RenameVariable.psx.ps1 b/Transpilers/RenameVariable.psx.ps1 new file mode 100644 index 000000000..9f5c53455 --- /dev/null +++ b/Transpilers/RenameVariable.psx.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS + Renames variables +.DESCRIPTION + Renames variables in a ScriptBlock +.EXAMPLE + { + [RenameVariable(VariableRename={ + @{ + x='x1' + y='y1' + } + })] + param($x, $y) + } | .>PipeScript +.LINK + Update-PipeScript +#> +param( +# The name of one or more parameters to remove +[Parameter(Mandatory,Position=0)] +[Alias('Variables','RenameVariables', 'RenameVariable','VariableRenames')] +[Collections.IDictionary] +$VariableRename, + +# The ScriptBlock that declares the parameters. +[Parameter(Mandatory,ValueFromPipeline)] +[scriptblock] +$ScriptBlock +) + +process { + Update-PipeScript -ScriptBlock $ScriptBlock -RenameVariable $VariableRename +} + diff --git a/Types/AttributeAST/get_ResolvedCommand.ps1 b/Types/AttributeAST/get_ResolvedCommand.ps1 new file mode 100644 index 000000000..592136641 --- /dev/null +++ b/Types/AttributeAST/get_ResolvedCommand.ps1 @@ -0,0 +1,46 @@ +<# +.SYNOPSIS + Resolves an Attribute to a CommandInfo +.DESCRIPTION + Resolves an Attribute to one or more CommandInfo. +.EXAMPLE + { + [InvokePipeScript()]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [Microsoft.PowerShell.Core.GetCommand()]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [Get_Command()]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [GetCommand()]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [cmd()]$null + }.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +#> +# Get the name of the transpiler. +$transpilerStepName = + if ($this.TypeName.IsGeneric) { + $this.TypeName.TypeName.Name + } else { + $this.TypeName.Name + } +$decamelCase = [Regex]::new('(?<=[a-z])(?=[A-Z])') +@( + # If a Transpiler exists by that name, it will be returned first. + Get-Transpiler -TranspilerName $transpilerStepName + # Then, any periods in the attribute name will be converted to slashes, + $fullCommandName = $transpilerStepName -replace '\.','\' -replace + '_','-' # and any underscores to dashes. + + # Then, the first CamelCased code will have a - injected in between the CamelCase. + $fullCommandName = $decamelCase.Replace($fullCommandName, '-', 1) + # Now we will try to find the command. + $ExecutionContext.SessionState.InvokeCommand.GetCommand($fullCommandName, 'All') +) \ No newline at end of file diff --git a/Types/CommandAST/get_ResolvedCommand.ps1 b/Types/CommandAST/get_ResolvedCommand.ps1 new file mode 100644 index 000000000..03fd53a62 --- /dev/null +++ b/Types/CommandAST/get_ResolvedCommand.ps1 @@ -0,0 +1 @@ +$ExecutionContext.SessionState.InvokeCommand.GetCommand($this.CommandElements[0].ToString(), 'All') \ No newline at end of file diff --git a/Types/TypeConstraintAST/TypeName.txt b/Types/TypeConstraintAST/TypeName.txt new file mode 100644 index 000000000..97a01cab4 --- /dev/null +++ b/Types/TypeConstraintAST/TypeName.txt @@ -0,0 +1 @@ +System.Management.Automation.Language.TypeConstraintAst diff --git a/Types/TypeConstraintAST/get_ResolvedCommand.ps1 b/Types/TypeConstraintAST/get_ResolvedCommand.ps1 new file mode 100644 index 000000000..4844fef57 --- /dev/null +++ b/Types/TypeConstraintAST/get_ResolvedCommand.ps1 @@ -0,0 +1,46 @@ +<# +.SYNOPSIS + Resolves an TypeConstraintAST to a CommandInfo +.DESCRIPTION + Resolves an TypeConstraintAST to one or more CommandInfo Objects. +.EXAMPLE + { + [InvokePipeScript[a]]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [Microsoft.PowerShell.Core.GetCommand]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [Get_Command]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [GetCommand]$null + }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +.EXAMPLE + { + [cmd]$null + }.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute.ResolvedCommand +#> +# Get the name of the transpiler. +$transpilerStepName = + if ($this.TypeName.IsGeneric) { + $this.TypeName.TypeName.Name + } else { + $this.TypeName.Name + } +$decamelCase = [Regex]::new('(?<=[a-z])(?=[A-Z])') +@( + # If a Transpiler exists by that name, it will be returned first. + Get-Transpiler -TranspilerName $transpilerStepName + # Then, any periods in the attribute name will be converted to slashes, + $fullCommandName = $transpilerStepName -replace '\.','\' -replace + '_','-' # and any underscores to dashes. + + # Then, the first CamelCased code will have a - injected in between the CamelCase. + $fullCommandName = $decamelCase.Replace($fullCommandName, '-', 1) + # Now we will try to find the command. + $ExecutionContext.SessionState.InvokeCommand.GetCommand($fullCommandName, 'All') +) \ No newline at end of file diff --git a/Types/VariableExpressionAST/GetAssignments.ps1 b/Types/VariableExpressionAST/GetAssignments.ps1 new file mode 100644 index 000000000..ce006e3da --- /dev/null +++ b/Types/VariableExpressionAST/GetAssignments.ps1 @@ -0,0 +1,48 @@ +<# +.SYNOPSIS + Gets assignments of a variable +.DESCRIPTION + Searches the abstract syntax tree for assignments of the variable. +.EXAMPLE + { + $x = 1 + $y = 2 + $x * $y + }.Ast.EndBlock.Statements[-1].PipelineElements[0].Expression.Left.GetAssignments() +.EXAMPLE + { + [int]$x, [int]$y = 1, 2 + $x * $y + }.Ast.EndBlock.Statements[-1].PipelineElements[0].Expression.Left.GetAssignments() +.EXAMPLE + { + param($x, $y) + $x * $y + }.Ast.EndBlock.Statements[-1].PipelineElements[0].Expression.Left.GetAssignments() +#> +param() + +$astVariableName = "$this" +$variableFoundAt = @{} +foreach ($parent in $this.GetLineage()) { + $parent.FindAll({ + param($ast) + $IsAssignment = + ( + $ast -is [Management.Automation.Language.AssignmentStatementAst] -and + $ast.Left.Find({ + param($leftAst) + $leftAst -is [Management.Automation.Language.VariableExpressionAST] -and + $leftAst.Extent.ToString() -eq $astVariableName + }, $false) + ) -or ( + $ast -is [Management.Automation.Language.ParameterAst] -and + $ast.Name.ToString() -eq $astVariableName + ) + + if ($IsAssignment -and -not $variableFoundAt[$ast.Extent.StartOffset]) { + $variableFoundAt[$ast.Extent.StartOffset] = $ast + $ast + } + }, $false) +} diff --git a/Types/VariableExpressionAST/GetVariableType.ps1 b/Types/VariableExpressionAST/GetVariableType.ps1 new file mode 100644 index 000000000..0c7b9658d --- /dev/null +++ b/Types/VariableExpressionAST/GetVariableType.ps1 @@ -0,0 +1,113 @@ +<# +.SYNOPSIS + Gets a Variable's Likely Type +.DESCRIPTION + Determines the type of a variable. + + This looks for the closest assignment statement and uses this to determine what type the variable is likely to be. +.NOTES + Subject to revision and improvement. While this covers many potential scenarios, it does not always +.EXAMPLE + { + [int]$x = 1 + $y = 2 + $x + $y + }.Ast.EndBlock.Statements[-1].PipelineElements[0].Expression.Left.GetVariableType() +.EXAMPLE + { + $x = Get-Process + $x + $y + }.Ast.EndBlock.Statements[-1].PipelineElements[0].Expression.Left.GetVariableType() +#> +if ($this.VariablePath.userPath -eq 'psBoundParmeters') { + return [Management.Automation.PSBoundParametersDictionary] +} +$assignments = $this.GetAssignments() +$closestAssignment = $assignments[0] + +# Our easiest scenario is that the variable is assigned in a parameter +if ($closestAssignment -is [Management.Automation.Language.ParameterAst]) { + # If so, the .StaticType will give us our variable type. + return $closestAssignment.StaticType +} + +# Our next simple scenario is that the closest assignment is declaring a hashtable +if ($closestAssignment.Right.Expression -is [Management.Automation.Language.HashtableAst]) { + return [hashtable] +} + +# The left can be a convert expression. +if ($closestAssignment.Left -is [Management.Automation.Language.ConvertExpressionAst]) { + # If the left was [ordered] + if ($closestAssignment.Left.Type.Tostring() -eq '[ordered]') { + return [Collections.specialized.OrderedDictionary] # return an OrderedDictionary + } else { + # If the left side's type can be reflected + $reflectedType = $closestAssignment.Left.Type.TypeName.GetReflectionType() + if ($reflectedType) { + return $reflectedType # return it. + } + else { + # otherwise, return the left's static type. + return $closestAssignment.Left.StaticType + } + } +} + +# Determine if the left side is multiple assignment +$isMultiAssignment =$closestAssignment.Left -is [Management.Automation.Language.ArrayLiteralAst] + +# If the left side is not multiple assignment, but the right side is an array +if (-not $isMultiAssignment -and + $closestAssignment.Right.Expression -is [Management.Automation.ArrayExpressionAst]) { + # then the object is an array. + return [Object[]] +} + +# Next, if the right as a convert expression +if ($closestAssignment.Right.Expression -is [Management.Automation.Language.ConvertExpressionAst]) { + # If it was '[ordered]' + if ($closestAssignment.Right.Expression.Type.Tostring() -eq '[ordered]') { + # return an ordered dictionary + return [Collections.specialized.OrderedDictionary] + } else { + # Otherwise, see if we have a reflected type. + $reflectedType = $closestAssignment.Right.Expression.Type.TypeName.GetReflectionType() + if ($reflectedType) { + return $reflectedType # If we do, return it. + } + else { + # If we don't, return the static type of the expression + return $closestAssignment.Right.Expression.StaticType + } + } +} + + + + +# The right side could be a pipeline +if ($closestAssignment.Right -is [Management.Automation.Language.PipelineAst]) { + # If so, walk backwards thru the pipeline + for ($pipelineElementIndex = $closestAssignment.Right.PipelineElements.Count - 1; + $pipelineElementIndex -ge 0; + $pipelineElementIndex--) { + $commandInfo = $closestAssignment.Right.PipelineElements[$pipelineElementIndex].ResolvedCommand + # If the command had an output type, return it. + if ($commandInfo.OutputType) { + return $commandInfo.OutputType.Type + } + } +} + + + + + + +# If we don't know, return nothing +return + + + + diff --git a/action.yml b/action.yml index aceef8bef..aba63b762 100644 --- a/action.yml +++ b/action.yml @@ -47,11 +47,11 @@ runs: id: PipeScriptAction shell: pwsh env: - SkipBuild: ${{inputs.SkipBuild}} + PipeScript: ${{inputs.PipeScript}} UserName: ${{inputs.UserName}} - CommitMessage: ${{inputs.CommitMessage}} UserEmail: ${{inputs.UserEmail}} - PipeScript: ${{inputs.PipeScript}} + CommitMessage: ${{inputs.CommitMessage}} + SkipBuild: ${{inputs.SkipBuild}} run: | $Parameters = @{} $Parameters.PipeScript = ${env:PipeScript} @@ -168,13 +168,30 @@ runs: if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } - if (-not $UserEmail) { $UserEmail = "$UserName@github.com" } + if (-not $UserEmail) { + $GitHubUserEmail = + if ($env:GITHUB_TOKEN) { + Invoke-RestMethod -uri "https://api.github.com/user/emails" -Headers @{ + Authorization = "token $env:GITHUB_TOKEN" + } | + Select-Object -First 1 -ExpandProperty email + } else {''} + $UserEmail = + if ($GitHubUserEmail) { + $GitHubUserEmail + } else { + "$UserName@github.com" + } + } git config --global user.email $UserEmail git config --global user.name $UserName if (-not $env:GITHUB_WORKSPACE) { throw "No GitHub workspace" } - git pull | Out-Host + $branchName = git rev-parse --abrev-ref HEAD + if (-not $branchName) { + return + } $PipeScriptStart = [DateTime]::Now if ($PipeScript) { @@ -211,11 +228,15 @@ runs: git commit -m $ExecutionContext.SessionState.InvokeCommand.ExpandString($CommitMessage) } + + $checkDetached = git symbolic-ref -q HEAD if (-not $LASTEXITCODE) { + "::notice::Pulling Changes" | Out-Host + git pull | Out-Host "::notice::Pushing Changes" | Out-Host - git push + git push | Out-Host "Git Push Output: $($gitPushed | Out-String)" } else { "::notice::Not pushing changes (on detached head)" | Out-Host diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9c6437cb8..ee515f003 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,3 +1,19 @@ +## 0.0.14: +* New Transpilers: + * [RemoveParameter] (#159) + * [RenameVariable] (#160) +* Keyword Updates: + * new now supports extended type creation (#164) + * until now supports a TimeSpan, DateTime, or EventName string (#153) +* AST Extended Type Enhancements: + * [TypeConstraintAst] and [AttributeAst] now have .ResolvedCommand (#162) +* Action Updates + * Pulling just before push (#163) + * Not running when there is not a current branch (#158) + * Improving email determination (#156) +* Invoke-PipeScript terminates transpiler errors when run interactively (#161) +--- + ## 0.0.13: * New / Improved Keywords * assert keyword (Support for pipelines) (#143) diff --git a/docs/New-PipeScript.md b/docs/New-PipeScript.md index 3d873dcde..055983eb3 100644 --- a/docs/New-PipeScript.md +++ b/docs/New-PipeScript.md @@ -82,9 +82,31 @@ The script header. |--------------|--------|-------|---------------------| |```[String]```|false |6 |true (ByPropertyName)| --- +#### **AutoParameter** + +If provided, will automatically create parameters. +Parameters will be automatically created for any unassigned variables. + + + +|Type |Requried|Postion|PipelineInput| +|--------------|--------|-------|-------------| +|```[Switch]```|false |named |false | +--- +#### **AutoParameterType** + +The type used for automatically generated parameters. +By default, ```[PSObject]```. + + + +|Type |Requried|Postion|PipelineInput| +|------------|--------|-------|-------------| +|```[Type]```|false |7 |false | +--- ### Syntax ```PowerShell -New-PipeScript [[-Parameter] ] [[-DynamicParameter] ] [[-Begin] ] [[-Process] ] [[-End] ] [[-Header] ] [] +New-PipeScript [[-Parameter] ] [[-DynamicParameter] ] [[-Begin] ] [[-Process] ] [[-End] ] [[-Header] ] [-AutoParameter] [[-AutoParameterType] ] [] ``` --- diff --git a/docs/New.md b/docs/New.md index a9f3bc1da..4cebfecde 100644 --- a/docs/New.md +++ b/docs/New.md @@ -66,6 +66,11 @@ If 'new' { (new PowerShell).AddScript("Get-Command").Invoke() } ``` +#### EXAMPLE 10 +```PowerShell +{ new 'https://schema.org/Thing' } +``` + --- ### Parameters #### **CommandAst** diff --git a/docs/RemoveParameter.md b/docs/RemoveParameter.md new file mode 100644 index 000000000..cae0c6d25 --- /dev/null +++ b/docs/RemoveParameter.md @@ -0,0 +1,53 @@ + +RemoveParameter +--------------- +### Synopsis +Removes Parameters from a ScriptBlock + +--- +### Description + +Removes Parameters from a ScriptBlock + +--- +### Related Links +* [Update-PipeScript](Update-PipeScript.md) +--- +### Examples +#### EXAMPLE 1 +```PowerShell +{ + [RemoveParameter("x")] + param($x, $y) +} | .>PipeScript +``` + +--- +### Parameters +#### **ParameterName** + +The name of one or more parameters to remove + + + +|Type |Requried|Postion|PipelineInput| +|----------------|--------|-------|-------------| +|```[String[]]```|true |1 |false | +--- +#### **ScriptBlock** + +The ScriptBlock that declares the parameters. + + + +|Type |Requried|Postion|PipelineInput | +|-------------------|--------|-------|--------------| +|```[ScriptBlock]```|true |named |true (ByValue)| +--- +### Syntax +```PowerShell +RemoveParameter [-ParameterName] -ScriptBlock [] +``` +--- + + diff --git a/docs/RenameVariable.md b/docs/RenameVariable.md new file mode 100644 index 000000000..27e12f3ca --- /dev/null +++ b/docs/RenameVariable.md @@ -0,0 +1,58 @@ + +RenameVariable +-------------- +### Synopsis +Renames variables + +--- +### Description + +Renames variables in a ScriptBlock + +--- +### Related Links +* [Update-PipeScript](Update-PipeScript.md) +--- +### Examples +#### EXAMPLE 1 +```PowerShell +{ + [RenameVariable(VariableRename={ + @{ + x='x1' + y='y1' + } + })] + param($x, $y) +} | .>PipeScript +``` + +--- +### Parameters +#### **VariableRename** + +The name of one or more parameters to remove + + + +|Type |Requried|Postion|PipelineInput| +|-------------------|--------|-------|-------------| +|```[IDictionary]```|true |1 |false | +--- +#### **ScriptBlock** + +The ScriptBlock that declares the parameters. + + + +|Type |Requried|Postion|PipelineInput | +|-------------------|--------|-------|--------------| +|```[ScriptBlock]```|true |named |true (ByValue)| +--- +### Syntax +```PowerShell +RenameVariable [-VariableRename] -ScriptBlock [] +``` +--- + + diff --git a/docs/Until.md b/docs/Until.md index 34e3f7cc8..66e85985e 100644 --- a/docs/Until.md +++ b/docs/Until.md @@ -26,16 +26,44 @@ until will always run at least once, and will run until a condition is true. #### EXAMPLE 2 ```PowerShell -{ +Invoke-PipeScript { until "00:00:05" { [DateTime]::Now Start-Sleep -Milliseconds 500 } -} | .>PipeScript +} ``` #### EXAMPLE 3 ```PowerShell +Invoke-PipeScript { + until "12:17 pm" { + [DateTime]::Now + Start-Sleep -Milliseconds 500 + } +} +``` + +#### EXAMPLE 4 +```PowerShell +{ + $eventCounter = 0 + until "MyEvent" { + $eventCounter++ + $eventCounter + until "00:00:03" { + "sleeping a few seconds" + Start-Sleep -Milliseconds 500 + } + if (-not ($eventCounter % 5)) { + $null = New-Event -SourceIdentifier MyEvent + } + } +} | .>PipeScript +``` + +#### EXAMPLE 5 +```PowerShell Invoke-PipeScript { $tries = 3 until (-not $tries) {