diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..36bd853 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [StartAutomating] diff --git a/.github/workflows/TestBuildAndPublish.yml b/.github/workflows/TestBuildAndPublish.yml index 1f4d131..dd4611b 100644 --- a/.github/workflows/TestBuildAndPublish.yml +++ b/.github/workflows/TestBuildAndPublish.yml @@ -589,4 +589,8 @@ jobs: - name: Run HelpOut uses: StartAutomating/HelpOut@master id: HelpOut + - name: Run Splatter (on branch) + if: ${{github.ref_name != 'main'}} + uses: ./ + id: SplatterBranch diff --git a/@.min.gzip.ps1 b/@.min.gzip.ps1 new file mode 100644 index 0000000..c0e4d2c --- /dev/null +++ b/@.min.gzip.ps1 @@ -0,0 +1,77 @@ +#region Splatter [ 0.5.5 ] : Simple Scripts to Supercharge Splatting (Install-Module Splatter, then Initialize-Splatter -Verb Get,Use,Find -Compress -Minify ) +.([ScriptBlock]::Create(([IO.StreamReader]::new(( + [IO.Compression.GZipStream]::new([IO.MemoryStream]::new( + [Convert]::FromBase64String(' +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 +ravatNDe3J+li/dS/gZtiOV/AdDBNlK4UQAA + ')), + [IO.Compression.CompressionMode]'Decompress')), + [Text.Encoding]::unicode)).ReadToEnd() +)) +#endregion Splatter [ 0.5.5 ] : Simple Scripts to Supercharge Splatting (Install-Module Splatter, then Initialize-Splatter -Verb Get,Use,Find -Compress -Minify ) diff --git a/@.min.ps1 b/@.min.ps1 new file mode 100644 index 0000000..9c5c95d --- /dev/null +++ b/@.min.ps1 @@ -0,0 +1,17 @@ +#region Splatter [ 0.5.5 ] : Simple Scripts to Supercharge Splatting (Install-Module Splatter, then Initialize-Splatter -Verb Get,Use,Find -Minify ) + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification="This Declares Variables for Other Scripts")] +param() +${?@}=${GetSplat}=${gSplat}={[Alias('?@','gSplat')]param([Parameter(Mandatory=$true,Position=0)][PSObject[]]$Command,[Parameter(Mandatory=$true,ValueFromPipeline=$true,Position=1)][Alias('InputObject')][PSObject]$Splat,[switch]$Force)begin{if(-not ${script:_@p}){${script:_@p}=@{}};if(-not ${script:_@c}){${script:_@c}=@{}};if(-not ${script:_@mp}){${script:_@mp}=@{}};if(-not ${script:_@pp}){${script:_@pp}=@{}};$ValidateAttributes={param([Parameter(Mandatory)]$value,[Parameter(Mandatory)]$attributes)foreach($attr in $attributes){$_=$this=$value;if($attr-is[Management.Automation.ValidateScriptAttribute]){$result=. $attr.ScriptBlock;if($result-ne$true){$attr}}elseif($attr-is[Management.Automation.ValidatePatternAttribute]-and(-not [Regex]::new($attr.RegexPattern, $attr.Options, '00:00:05').IsMatch($value))){$attr}elseif($attr-is[Management.Automation.ValidateSetAttribute]-and$attr.ValidValues-notcontains$value){$attr}elseif($attr-is[Management.Automation.ValidateRangeAttribute]-and(($value-gt$attr.MaxRange)-or($value-lt$attr.MinRange))){$attr}}}}process{$ap,$ac,$amp=${script:_@p},${script:_@c},${script:_@mp};if($Splat-is[Collections.IDictionary]){$splat=[PSCustomObject]([Ordered]@{} + $Splat)};$in=$Splat;foreach($cmd in $Command){$rc=if($ac.$cmd){$ac.$cmd}elseif($cmd-is[string]){$fc=$ExecutionContext.SessionState.InvokeCommand.GetCommand($cmd,'Function,Cmdlet,ExternalScript,Alias');$fc=if($fc-is[Management.Automation.AliasInfo]){$fc.ResolvedCommand}else{$fc};$ac.$cmd=$fc;$fc}elseif($cmd-is[ScriptBlock]){$hc=$cmd.GetHashCode();$ExecutionContext.SessionState.PSVariable.Set("function:f$hc", $cmd);$c=$ExecutionContext.SessionState.InvokeCommand.GetCommand("f$hc",'Function');$ac.$cmd=$c;$c}elseif($cmd-is[Management.Automation.CommandInfo]){$ac.$cmd=$cmd;$cmd};if(-not $rc){continue};$cmd=$rc;$outSplat,$Invalid,$Unmapped,$paramMap,$Pipe,$NoPipe=foreach($_ in 1..6){[ordered]@{}};$params=[Collections.ArrayList]::new();$props=@($in.psobject.properties);$pc=$props.Count;if(-not ${script:_@pp}.$cmd){${script:_@pp}.$cmd=@(foreach($param in $cmd.Parameters.Values){foreach($attr in $param.Attributes){if($attr.ValueFromPipeline){$param}}})};$cmdMd=$cmd-as[Management.Automation.CommandMetaData];$problems=@(foreach($vfp in ${script:_@pp}.$cmd){if($in-is$vfp.ParameterType-or($vfp.ParameterType.IsArray-and$in-as$vfp.ParameterType)){$v=$in;$badAttributes=& $ValidateAttributes $v $vfp.Attributes;if($badAttributes){@{$vfp.Name=$badAttributes}};if(-not $badAttributes-or$Force){$null=$params.Add($vfp.Name);$pipe[$vfp.Name]=$v;$outSplat[$vfp.Name]=$v;$paramMap[$vfp.Name]=$vfp.Name;$pipelineParameterSets=@(foreach($attr in $vfp.Attributes){if($attr.ParameterSetName){$attr.ParameterSetName}})}}};:NextProperty foreach($prop in $props){$cp=$cmd.Parameters;$pn=$prop.Name;$pv=$prop.Value;if(-not $cp){continue};$param=$cp.$pn;if(-not $param){$k="${cmd}:$pn";$param=if($ap[$k]){$ap[$k]}else{foreach($p in $cp.Values){foreach($a in $p.Aliases){$ap["${cmd}:$a"]=$p};if($ap[$k]){$ap[$k];break}}}};if(-not $param){$pn;continue};$paramMap[$param.Name]=$pn;if($params-contains$param){continue};$pt=$param.ParameterType;$paramSets=@(foreach($attr in $param.Attributes){if($attr.ParameterSetName){$attr.ParameterSetName}});if($pipelineParameterSets){$ok=$false;foreach($cmdPs in $paramSets){$ok=$ok-bor($pipelineParameterSets-contains$cmdPs)};if(-not $ok-and-not $Force){continue}};$v=$pv-as$pt;if(-not $v-and($pt-eq[ScriptBlock]-or$pt-eq[ScriptBlock[]])){$sb=try{foreach($_ in $pv){[ScriptBlock]::Create($_)}}catch{$null};if($sb){$v=$sb}};if($null-ne$v){$nv=try{[PSVariable]::new("$pn", $v, 'Private',$param.Attributes)}catch{@{$pn=$_}};if($nv-is[PSVariable]-or$Force){$null=$params.Add($param);:CanItPipe do{foreach($attr in $param.Attributes){if($attr.ValueFromPipeline-or$attr.ValueFromPipelineByPropertyName-and((-not $pipelineParameterSets)-or($pipelineParameterSets-contains$attr.ParameterSetName))){$pipe[$prop.Name]=$v;break CanItPipe}};$NoPipe[$prop.Name]=$v}while($false);$outSplat[$prop.Name]=$v};if($nv-isnot[PSVariable]){$nv}}else{@{$pn = $param}}});$Mandatory=@{};foreach($param in $cmdMd.Parameters.Values){foreach($a in $param.Attributes){if(-not $a.Mandatory){continue};if($a-isnot[Management.Automation.ParameterAttribute]){continue};if(-not $Mandatory[$a.ParameterSetName]){$Mandatory[$a.ParameterSetName]=[Ordered]@{}};$mp=($paramMap.($param.Name));$Mandatory[$a.ParameterSetName].($param.Name)=if($mp){if($pipelineParameterName-contains$param.Name){$in}else{$outSplat.$mp}}}};$amp.$cmd=$Mandatory;$mandatory=$amp.$cmd;$missingMandatory=@{};foreach($m in $Mandatory.GetEnumerator()){$missingMandatory[$m.Key]=@(foreach($_ in $m.value.GetEnumerator()){if($null-eq$_.Value){$_.Key}})};$couldRun=if(-not $Mandatory.Count){$true}elseif($missingMandatory.'__AllParameterSets'){$false}else{foreach($_ in $missingMandatory.GetEnumerator()){if(-not $_.Value){$true;break}}};if(-not $couldRun-and-not $Force){continue};foreach($p in $problems){if($p-is[Hashtable]){$Invalid+=$p}else{$Unmapped[$p]=$in.$p}};if($Invalid.Count-eq0){$Invalid=$null};if($Unmapped.Count-eq0){$Unmapped=$null};$realCmd=if($cmd-is[Management.Automation.FunctionInfo]-and$cmd.Name.Contains($cmd.ScriptBlock.GetHashCode().ToString())){$cmd.ScriptBlock}else{$cmd};foreach($_ in ([Ordered]@{ + Command = $realCmd + CouldRun = $couldRun + Invalid = $Invalid + Missing = $missingMandatory + PercentFit = $(if ($pc) {$outSplat.Count / $pc } else { 0}) + Unmapped = $Unmapped + PipelineParameter = $Pipe + NonPipelineParameter = $NoPipe + }).GetEnumerator()){$outSplat.psobject.properties.Add([Management.Automation.PSNoteProperty]::new($_.Key,$_.Value))};$outSplat}}} +${.@}=${UseSplat}={[Alias('.@','uSplat')]param([Parameter(Position=0)][PSObject[]]$Command,[Parameter(Position=1,ValueFromRemainingArguments)][PSObject[]]$ArgumentList,[Parameter(ValueFromPipeline=$true)][PSObject[]]$Splat,[switch]$Force,[Alias('BestFit','BestFitFunction', 'BF','BFF')][switch]$Best,[Alias('Pipe')][switch]$Stream)begin{$pipelines=@{}}process{$WeTrustTheSplat=$false;if(-not $Command-and$splat.Length-eq1-and$splat[0]-is[Collections.IDictionary]-and$Splat[0].psobject.Properties['Command']){$Command=$Splat[0].psobject.Properties['Command'].Value;$WeTrustTheSplat=$true}elseif(-not $command-and$_-is[PSObject]-and$_.Command-and$_.Splat){$WeTrustTheSplat=$true;$splat=$_.Splat;$command=$_.Command};if($Best-and$command.Count){$command=$splat|& ${?@} -Command $command|Sort-Object PercentFit -Descending|Select-Object -ExpandProperty Command -First 1};if(-not $Command){Write-Error -Message "No command found" -Category ObjectNotFound -ErrorId 'Use-Splat.CommandNotFound';return};foreach($cmd in $Command){if($WeTrustTheSplat){if($cmd-is[Management.Automation.CommandInfo]-or$cmd-is[ScriptBlock]){foreach($s in $splat){if($argumentList){& $cmd @s @ArgumentList}else{& $cmd @s}}}}else{$Splat|& ${?@} $cmd -Force:$Force|& {process{$i=$_;$np=$i.NonPipelineParameter;$c=$_.psobject.properties['Command'].Value;if($Stream){if(-not $pipelines[$c]){$stepScript=if($argumentList){{& $c @np @argumentList}}else{{& $c @np}};$stepPipeline=$stepScript.GetSteppablePipeline();$pipelines[$c]=$stepPipeline;$stepPipeline.Begin($true)}else{$stepPipeline=$pipelines[$c]};$stepPipeline.Process([PSCustomObject]$i.PipelineParameter);return};if($c-is[Management.Automation.CommandInfo]-or$c-is[ScriptBlock]){if($ArgumentList){& $c @i @ArgumentList}else{& $c @i}}}}}}}end{if($pipelines.Count){foreach($v in $pipelines.Values){$v.End()}}}} +${??@}=${FindSplat}=${fSplat}={[Alias('??@','fSplat')]param([Parameter(Position=0)][string[]]$Command,[Parameter(ValueFromPipeline=$true,Position=1)][Alias('InputObject')][PSObject]$Splat,[Alias('G')][switch]$Global,[Alias('L')][switch]$Local,[Alias('M')][string[]]$Module,[switch]$Force)begin{$myModule=$MyInvocation.MyCommand.ScriptBlock.Module;$cmdTypes='Function,Cmdlet,ExternalScript,Alias';$resolveAliases={begin{$n=0}process{$n++;if($t-is[int]-and$id){$p=$n*100/$t;Write-Progress "Resolving" "$_ " -PercentComplete $p -Id $id};if($_.ResolvedCommand){$_.ResolvedCommand}else{$_}}end{Write-Progress 'Resolving Aliases' 'Complete' -Id $id}};$filterCmds={process{foreach($c in $Command){if($_-like$c){return $_}}}}}process{if(-not $Splat){return};$id=[Random]::new().Next();$commandList=@(if(-not $Command){Write-Progress -Id $id -Activity 'Getting Commands' -Status ' ';if($MyModule-and$Local){$myModule.ExportedCommands.Values|. $resolveAliases|Select-Object -Unique}elseif($module){foreach($m in $Module){$rm=Get-Module $m;if(-not $rm){continue};$rm.ExportedCommands.Values|. $resolveAliases|Select-Object -Unique}}else{$allcmds=@($ExecutionContext.SessionState.InvokeCommand.GetCommands('*',$cmdTypes, $true));$t=$allcmds.Count;$allcmds|. $resolveAliases|Select-Object -Unique}}elseif($module){foreach($m in $Module){$rm=Get-Module $m;if(-not $rm){continue};$rm.ExportedCommands.Values|. $resolveAliases|. $filterCmds|Select-Object -Unique}}elseif($Global){foreach($c in $Command){$ExecutionContext.SessionState.InvokeCommand.GetCommands($c,$cmdTypes, $true)}}elseif($MyModule-and$Local){$myModule.ExportedCommands.Values|. $filterCmds|. $resolveAliases|Select-Object -Unique}else{foreach($cmd in $Command){if($cmd-is[string]-and$cmd.Contains('*')){$ExecutionContext.SessionState.InvokeCommand.GetCommands($cmd,$cmdTypes, $true)}else{$cmd}}});$psBoundParameters.Command=$commandList;if($Splat-is[Collections.IDictionary]){$Splat=if($splat.GetType().Name-ne'PSBoundParametersDictionary'){[PSCustomObject]$Splat}else{[PSCustomObject]([Ordered]@{} + $Splat)}};$c,$t=0,$commandList.Count;foreach($cmd in $commandList){if($t-gt1){$c++;$p=$c*100/$t;Write-Progress -Id $id -Activity 'Finding Splats' -Status "$cmd " -PercentComplete $p};$Splat|& ${?@} $cmd -Force:$Force|Select-Object -Property * -ExcludeProperty SyncRoot,IsSynchronized,IsReadOnly,IsFixedSize,Count|& {process{if($_.PercentFit-eq0){return};$_.pstypenames.clear();$_.pstypenames.add('Find.Splat.Output');$keys, $values=$_.Keys,$_.Values;$resplat=[Ordered]@{};for($i=0;$i -lt $keys.count;$i++){$resplat[$keys[$i]]=$values[$i]};$_.psobject.properties.remove('Keys');$_.psobject.properties.remove('Values');$_.psobject.properties.Add([Management.Automation.PSNoteProperty]::new('Splat', $resplat));$_}}};Write-Progress -Id $id -Completed -Activity 'Finding Splats' -Status 'Complete!'}} +#endregion Splatter [ 0.5.5 ] : Simple Scripts to Supercharge Splatting (Install-Module Splatter, then Initialize-Splatter -Verb Get,Use,Find -Minify ) diff --git a/@.ps1 b/@.ps1 new file mode 100644 index 0000000..e61a350 --- /dev/null +++ b/@.ps1 @@ -0,0 +1,573 @@ +#region Splatter [ 0.5.5 ] : Simple Scripts to Supercharge Splatting (Install-Module Splatter, then Initialize-Splatter -Verb Get,Use,Find ) + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "", Justification="This Declares Variables for Other Scripts")] +param() +${?@}=${GetSplat}=${gSplat}={ + <# + .Synopsis + Gets a splat + .Description + Gets a splat for a command + .Link + Find-Splat + .Link + Use-Splat + .Example + @{id=$pid} | Get-Splat + .Example + @{id=$Pid} | ?@ # ?@ is an alias for Get-Splat + .Example + @{id=$pid} | & ${?@} # Get-Splat as a script block + #> + [Alias('?@','gSplat')] + param( + # The command that is being splatted. + [Parameter(Mandatory=$true,Position=0)] + [PSObject[]] + $Command, + + # The input object + [Parameter(Mandatory=$true,ValueFromPipeline=$true,Position=1)] + [Alias('InputObject')] + [PSObject] + $Splat, + + # If set, will return regardless of if parameters map, are valid, and have enough mandatory parameters + [switch] + $Force + ) + begin { + # Declare some caches: + if (-not ${script:_@p}) { ${script:_@p} = @{} } # * All Parameters + if (-not ${script:_@c}) { ${script:_@c} = @{} } # * All commands + if (-not ${script:_@mp}) { ${script:_@mp} = @{} } # * All Mandatory Parameters + if (-not ${script:_@pp}) { ${script:_@pp} = @{} } # * All Pipelined Parameters + $ValidateAttributes = { + param( + [Parameter(Mandatory)]$value, + [Parameter(Mandatory)]$attributes + ) + + foreach ($attr in $attributes) { + $_ = $this = $value + if ($attr -is [Management.Automation.ValidateScriptAttribute]) { + $result = . $attr.ScriptBlock + if ($result -ne $true) { + $attr + } + } + elseif ($attr -is [Management.Automation.ValidatePatternAttribute] -and + (-not [Regex]::new($attr.RegexPattern, $attr.Options, '00:00:05').IsMatch($value)) + ) { $attr } + elseif ($attr -is [Management.Automation.ValidateSetAttribute] -and + $attr.ValidValues -notcontains $value) { $attr } + elseif ($attr -is [Management.Automation.ValidateRangeAttribute] -and ( + ($value -gt $attr.MaxRange) -or ($value -lt $attr.MinRange) + )) {$attr} + } + } + } + process { + + $ap,$ac,$amp = ${script:_@p},${script:_@c}, ${script:_@mp} + #region Turn dictionaries into PSObjects + if ($Splat -is [Collections.IDictionary]) { + $splat = [PSCustomObject]([Ordered]@{} + $Splat) + } + #endregion Turn dictionaries into PSObjects + + $in = $Splat + foreach ($cmd in $Command) { # Walk over each command + $rc = + if ($ac.$cmd) { # use cache if available, otherwise: + $ac.$cmd + } elseif ($cmd -is [string]) { # *find it if it's a [string] + $fc = $ExecutionContext.SessionState.InvokeCommand.GetCommand($cmd,'Function,Cmdlet,ExternalScript,Alias') + $fc = + if ($fc -is [Management.Automation.AliasInfo]) { + $fc.ResolvedCommand + } else { + $fc + } + $ac.$cmd = $fc + $fc + } elseif ($cmd -is [ScriptBlock]) { # * Make a temporary command if it's a [ScriptBlock] + $hc = $cmd.GetHashCode() + $ExecutionContext.SessionState.PSVariable.Set("function:f$hc", $cmd) + $c = $ExecutionContext.SessionState.InvokeCommand.GetCommand("f$hc",'Function') + $ac.$cmd = $c + $c + } elseif ($cmd -is [Management.Automation.CommandInfo]) { # * Otherwise, use the command info + $ac.$cmd = $cmd + $cmd + } + if (-not $rc) {continue} + $cmd = $rc + $outSplat,$Invalid,$Unmapped,$paramMap,$Pipe,$NoPipe = foreach ($_ in 1..6){[ordered]@{}} + $params = [Collections.ArrayList]::new() + $props = @($in.psobject.properties) + $pc = $props.Count + + if (-not ${script:_@pp}.$cmd) { + ${script:_@pp}.$cmd = @( + foreach ($param in $cmd.Parameters.Values) { + foreach ($attr in $param.Attributes) { + if ($attr.ValueFromPipeline) { + $param + } + } + }) + } + + $cmdMd = $cmd -as [Management.Automation.CommandMetaData] + $problems = @( + + foreach ($vfp in ${script:_@pp}.$cmd) { + if ($in -is $vfp.ParameterType -or + ($vfp.ParameterType.IsArray -and $in -as $vfp.ParameterType) + ) { + $v = $in + $badAttributes = & $ValidateAttributes $v $vfp.Attributes + if ($badAttributes) { + @{$vfp.Name=$badAttributes} + } + if (-not $badAttributes -or $Force) { + $null = $params.Add($vfp.Name) + $pipe[$vfp.Name] = $v + $outSplat[$vfp.Name] = $v + $paramMap[$vfp.Name] = $vfp.Name + $pipelineParameterSets = + @(foreach ($attr in $vfp.Attributes) { + if ($attr.ParameterSetName) { $attr.ParameterSetName} + }) + } + } + } + + :NextProperty foreach ($prop in $props) { + $cp=$cmd.Parameters + $pn = $prop.Name + $pv = $prop.Value + if (-not $cp) { continue } + $param = $cp.$pn + if (-not $param) { + $k = "${cmd}:$pn" + $param = + if ($ap[$k]) { + $ap[$k] + } else { + foreach ($p in $cp.Values) { + foreach ($a in $p.Aliases) { + $ap["${cmd}:$a"] = $p + } + if ($ap[$k]) { $ap[$k]; break } + } + } + } + + if (-not $param) { + $pn + continue + } + $paramMap[$param.Name] = $pn + if ($params -contains $param) { continue } + $pt=$param.ParameterType + $paramSets = + @(foreach ($attr in $param.Attributes) { + if ($attr.ParameterSetName) { $attr.ParameterSetName } + }) + + if ($pipelineParameterSets) { + $ok = $false + foreach ($cmdPs in $paramSets) { + $ok = $ok -bor ($pipelineParameterSets -contains $cmdPs) + } + if (-not $ok -and -not $Force) { continue } + } + $v = $pv -as $pt + if (-not $v -and + ($pt -eq [ScriptBlock] -or + $pt -eq [ScriptBlock[]])) { + $sb = try { foreach ($_ in $pv) { [ScriptBlock]::Create($_) }} catch {$null} + if ($sb) { $v = $sb } + } + if ($null -ne $v) { + $nv = try { + [PSVariable]::new("$pn", $v, 'Private',$param.Attributes) + } catch { + @{$pn=$_} + } + if ($nv -is [PSVariable] -or $Force) { + $null = $params.Add($param) + :CanItPipe do { + foreach ($attr in $param.Attributes) { + if ($attr.ValueFromPipeline -or $attr.ValueFromPipelineByPropertyName -and + ((-not $pipelineParameterSets) -or ($pipelineParameterSets -contains $attr.ParameterSetName)) + ) { + $pipe[$prop.Name] = $v + break CanItPipe + } + } + $NoPipe[$prop.Name] = $v + } while ($false) + $outSplat[$prop.Name] = $v + } + + if ($nv -isnot [PSVariable]) { $nv } + } else { + @{$pn = $param} + } + }) + + + $Mandatory = @{} + + foreach ($param in $cmdMd.Parameters.Values) { + foreach ($a in $param.Attributes) { + if (-not $a.Mandatory) { continue } + if ($a -isnot [Management.Automation.ParameterAttribute]) { continue } + if (-not $Mandatory[$a.ParameterSetName]) { $Mandatory[$a.ParameterSetName] = [Ordered]@{} } + $mp = ($paramMap.($param.Name)) + $Mandatory[$a.ParameterSetName].($param.Name) = + if ($mp) { + if ($pipelineParameterName -contains $param.Name) { + $in + } else { + $outSplat.$mp + } + } + } + } + $amp.$cmd = $Mandatory + + $mandatory = $amp.$cmd + + $missingMandatory = @{} + foreach ($m in $Mandatory.GetEnumerator()) { + $missingMandatory[$m.Key] = + @(foreach ($_ in $m.value.GetEnumerator()) { + if ($null -eq $_.Value) { $_.Key } + }) + } + $couldRun = + if (-not $Mandatory.Count) { $true } + elseif ($missingMandatory.'__AllParameterSets') { + $false + } + else { + foreach ($_ in $missingMandatory.GetEnumerator()) { + if (-not $_.Value) { $true;break } + } + } + + if (-not $couldRun -and -not $Force) { continue } + foreach ($p in $problems) { + if ($p -is [Hashtable]) { + $Invalid += $p + } else { $Unmapped[$p] = $in.$p } + } + if ($Invalid.Count -eq 0) { $Invalid = $null } + if ($Unmapped.Count -eq 0) { $Unmapped = $null } + + $realCmd = + if ($cmd -is [Management.Automation.FunctionInfo] -and + $cmd.Name.Contains($cmd.ScriptBlock.GetHashCode().ToString())) { + $cmd.ScriptBlock + } else { $cmd } + + foreach($_ in ([Ordered]@{ + Command = $realCmd + CouldRun = $couldRun + Invalid = $Invalid + Missing = $missingMandatory + PercentFit = $(if ($pc) {$outSplat.Count / $pc } else { 0}) + Unmapped = $Unmapped + PipelineParameter = $Pipe + NonPipelineParameter = $NoPipe + }).GetEnumerator()) { + $outSplat.psobject.properties.Add([Management.Automation.PSNoteProperty]::new($_.Key,$_.Value)) + } + $outSplat + } + } +} +${.@}=${UseSplat}={ + <# + .Synopsis + Uses a splat. + .Description + Uses a splat to call a command. + If passed from Find-Splat,Get-Splat or Test-Splat, the command will be automatically detected. + If called as .@, this will run only provided commands + If called as *@, this will run any found commands + .Link + Get-Splat + .Link + Find-Splat + .Link + Test-Splat + .Example + @{id=$pid} | Use-Splat gps # When calling Use-Splat is globally imported + .Example + @{id=$pid} | & ${.@} gps # When calling Use-Splat is nested + .Example + @{LogName='System';InstanceId=43,44}, + @{LogName='Application';InstanceId=10000,10005} | + .@ Get-EventLog # get a bunch of different log events + #> + [Alias('.@','uSplat')] + param( + # One or more commands + [Parameter(Position=0)] + [PSObject[]] + $Command, + + # Any additional positional arguments that would be passed to the command + [Parameter(Position=1,ValueFromRemainingArguments)] + [PSObject[]] + $ArgumentList = @(), + + # The splat + [Parameter(ValueFromPipeline=$true)] + [PSObject[]] + $Splat, + + # If set, will run regardless of if parameters map, are valid, and have enough mandatory parameters. + [switch] + $Force, + + # If set, will run the best fit out of multiple commands. + # The best fit is the command that will use the most of the input splat. + [Alias('BestFit','BestFitFunction', 'BF','BFF')] + [switch] + $Best, + + # If set, will stream input into a single pipeline of each command. + # The non-pipeable parameters of the first input splat will be used to start the pipeline. + # By default, a command will be run once per input splat. + [Alias('Pipe')] + [switch] + $Stream) + + begin { + $pipelines = @{} + } + process { + $WeTrustTheSplat = $false + if (-not $Command -and + $splat.Length -eq 1 -and + $splat[0] -is [Collections.IDictionary] -and + $Splat[0].psobject.Properties['Command']) { + $Command = $Splat[0].psobject.Properties['Command'].Value + $WeTrustTheSplat = $true + } elseif (-not $command -and $_ -is [PSObject] -and $_.Command -and $_.Splat) { + $WeTrustTheSplat = $true + $splat = $_.Splat + $command = $_.Command + } + + if ($Best -and $command.Count) { + $command = $splat | + & ${?@} -Command $command | + Sort-Object PercentFit -Descending | + Select-Object -ExpandProperty Command -First 1 + } + + if (-not $Command) { + Write-Error -Message "No command found" -Category ObjectNotFound -ErrorId 'Use-Splat.CommandNotFound' ;return + } + #region UseTheSplat + foreach ($cmd in $Command) { + if ($WeTrustTheSplat) { + if ($cmd -is [Management.Automation.CommandInfo] -or $cmd -is [ScriptBlock]) { + foreach ($s in $splat) { + if ($argumentList) { + & $cmd @s @ArgumentList + } else { + & $cmd @s + } + } + } + } else { + $Splat | + & ${?@} $cmd -Force:$Force | + & { process { + + $i = $_ + $np = $i.NonPipelineParameter + $c = $_.psobject.properties['Command'].Value + if ($Stream) { + if (-not $pipelines[$c]) { + + $stepScript = if ($argumentList) { {& $c @np @argumentList} } else { {& $c @np} } + + $stepPipeline = $stepScript.GetSteppablePipeline() + $pipelines[$c] = $stepPipeline + $stepPipeline.Begin($true) + } else { + $stepPipeline = $pipelines[$c] + } + $stepPipeline.Process([PSCustomObject]$i.PipelineParameter) + return + } + + if ($c -is [Management.Automation.CommandInfo] -or $c -is [ScriptBlock]) { + if ($ArgumentList) { + & $c @i @ArgumentList + } else { + & $c @i + } + } + }} + } + } + #endregion UseTheSplat + } + + end { + if ($pipelines.Count) { + foreach ($v in $pipelines.Values) { $v.End() } + } + } +} +${??@}=${FindSplat}=${fSplat}={ + <# + .Synopsis + Finds commands that can be splatted to given an input. + .Description + Finds the commands whose input parameters match an input object, and returns an [ordered] dictionary of parameters. + .Link + Get-Splat + .Link + Use-Splat + .Example + @{Id=$pid} | Find-Splat -Global + #> + [Alias('??@','fSplat')] + param( + # One or more commands. + # If not provided, commands from the current module will be searched. + # If there is no current module, all commands will be searched. + [Parameter(Position=0)] + [string[]]$Command, + + # The splat + [Parameter(ValueFromPipeline=$true,Position=1)] + [Alias('InputObject')] + [PSObject]$Splat, + + # If set, will look for all commands, even if Find-Splat is used within a module. + [Alias('G')][switch]$Global, + + # If set, will look for commands within the current module. + # To make this work within your own module Install-Splat. + [Alias('L')][switch]$Local, + + # If provided, will look for commands within any number of loaded modules. + [Alias('M')][string[]]$Module, + + # If set, will return regardless of if parameters map, are valid, and have enough mandatory parameters + [switch]$Force) + begin { + $myModule = $MyInvocation.MyCommand.ScriptBlock.Module + $cmdTypes = 'Function,Cmdlet,ExternalScript,Alias' + $resolveAliases = { begin { + $n = 0 + } process { + $n++ + if ($t -is [int] -and $id) { + $p = $n* 100 / $t + Write-Progress "Resolving" "$_ " -PercentComplete $p -Id $id + } + if ($_.ResolvedCommand) { $_.ResolvedCommand } + else { $_ } + } end { + Write-Progress 'Resolving Aliases' 'Complete' -Id $id + } } + $filterCmds = { process { + foreach ($c in $Command) { if ($_ -like $c) { return $_ } } + } } + } + process { + if (-not $Splat) { return } + $id =[Random]::new().Next() + + $commandList = + @(if (-not $Command) { + Write-Progress -Id $id -Activity 'Getting Commands' -Status ' ' + if ($MyModule -and $Local) { + $myModule.ExportedCommands.Values | . $resolveAliases | Select-Object -Unique + } elseif ($module) { + foreach ($m in $Module) { + $rm = Get-Module $m + if (-not $rm) { continue } + $rm.ExportedCommands.Values | . $resolveAliases | Select-Object -Unique + } + } else { + $allcmds = @($ExecutionContext.SessionState.InvokeCommand.GetCommands('*',$cmdTypes, $true)) + $t = $allcmds.Count + $allcmds |. $resolveAliases | Select-Object -Unique + } + } elseif ($module) { + foreach ($m in $Module) { + $rm = Get-Module $m + if (-not $rm) { continue } + $rm.ExportedCommands.Values | + . $resolveAliases | . $filterCmds | + Select-Object -Unique + } + } elseif ($Global) { + foreach ($c in $Command) { + $ExecutionContext.SessionState.InvokeCommand.GetCommands($c,$cmdTypes, $true) + } + } elseif ($MyModule -and $Local) { + $myModule.ExportedCommands.Values | + . $filterCmds | . $resolveAliases | Select-Object -Unique + } else { + foreach ($cmd in $Command) { + if ($cmd -is [string] -and $cmd.Contains('*')) { + $ExecutionContext.SessionState.InvokeCommand.GetCommands($cmd,$cmdTypes, $true) + } else { + $cmd + } + } + }) + $psBoundParameters.Command = $commandList + if ($Splat -is [Collections.IDictionary]) { + $Splat = + if ($splat.GetType().Name -ne 'PSBoundParametersDictionary') { + [PSCustomObject]$Splat + } else { + [PSCustomObject]([Ordered]@{} + $Splat) + } + } + + $c,$t= 0, $commandList.Count + foreach ($cmd in $commandList) { + if ($t -gt 1) { + $c++;$p=$c*100/$t + Write-Progress -Id $id -Activity 'Finding Splats' -Status "$cmd " -PercentComplete $p + } + $Splat | + & ${?@} $cmd -Force:$Force | + Select-Object -Property * -ExcludeProperty SyncRoot,IsSynchronized,IsReadOnly,IsFixedSize,Count | + & { process { + if ($_.PercentFit -eq 0) { return } + $_.pstypenames.clear() + $_.pstypenames.add('Find.Splat.Output') + $keys, $values = @($_.Keys), @($_.Values) + $resplat = [Ordered]@{} + for ($i=0;$i -lt $keys.count;$i++) { + $resplat[$keys[$i]] = $values[$i] + } + $_.psobject.properties.remove('Keys') + $_.psobject.properties.remove('Values') + $_.psobject.properties.Add([Management.Automation.PSNoteProperty]::new('Splat', $resplat)) + $_ + } } + } + + Write-Progress -Id $id -Completed -Activity 'Finding Splats' -Status 'Complete!' + } +} +#endregion Splatter [ 0.5.5 ] : Simple Scripts to Supercharge Splatting (Install-Module Splatter, then Initialize-Splatter -Verb Get,Use,Find ) diff --git a/Build/Splatter.GitHubAction.PSDevOps.ps1 b/Build/Splatter.GitHubAction.PSDevOps.ps1 new file mode 100644 index 0000000..c4f0db6 --- /dev/null +++ b/Build/Splatter.GitHubAction.PSDevOps.ps1 @@ -0,0 +1,8 @@ +#requires -Module PSDevOps +#requires -Module Splatter +Import-BuildStep -ModuleName Splatter +Push-Location ($PSScriptRoot | Split-Path) +New-GitHubAction -Name "UseSplatter" -Description @' +Simple Scripts to Supercharge Splatting +'@ -Action SplatterAction -Icon at-sign -OutputPath .\action.yml +Pop-Location \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d69f734..70e4ee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +### 0.5.5: + +* Splatter is now a GitHub Action! (#18) +* Initialize-Splatter now returns a `[ScriptBlock]` (#21) +* Initialize-Splatter/Out-Splat now have -OutputPath (#19/#20) +* Added Sponsorship (#22) + +--- + ### 0.5.4: * New Splatter Logo (#12) diff --git a/GitHub/Actions/SplatterAction.ps1 b/GitHub/Actions/SplatterAction.ps1 new file mode 100644 index 0000000..85532ea --- /dev/null +++ b/GitHub/Actions/SplatterAction.ps1 @@ -0,0 +1,262 @@ +<# +.Synopsis + GitHub Action for Splatter +.Description + GitHub Action for Splatter. This will: + + * Import Splatter + * Run all *.Splatter.ps1 files beneath the workflow directory + * Run a .SplatterScript parameter + + Any files changed can be outputted by the script, and those changes can be checked back into the repo. + Make sure to use the "persistCredentials" option with checkout. +#> + +param( +# A PowerShell Script that uses Splatter. +# Any files outputted from the script will be added to the repository. +# If those files have a .Message attached to them, they will be committed with that message. +[string] +$SplatterScript, + +# If set, will not process any files named *.Splatter.ps1 +[switch] +$SkipSplatterPS1, + +# A list of modules to be installed from the PowerShell gallery before scripts run. +[string[]] +$InstallModule = @(), + +# If provided, will commit any remaining changes made to the workspace with this commit message. +# If no commit message is provided, changes will not be committed. +[string] +$CommitMessage, + +# The user email associated with a git commit. +[string] +$UserEmail, + +# The user name associated with a git commit. +[string] +$UserName +) + +"::group::Parameters" | Out-Host +[PSCustomObject]$PSBoundParameters | Format-List | Out-Host +"::endgroup::" | Out-Host + +$gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json +} else { $null } + +@" +::group::GitHubEvent +$($gitHubEvent | ConvertTo-Json -Depth 100) +::endgroup:: +"@ | Out-Host + + +# Check to ensure we are on a branch +$branchName = git rev-parse --abrev-ref HEAD +# If we were not, return. +if ((-not $branchName) -or $LASTEXITCODE) { + $LASTEXITCODE = 0 + "::warning title=No Branch Found::Not on a Branch. Can not run." | Out-Host + exit 0 + return +} + +$repoRoot = (git rev-parse --show-toplevel *>&1) -replace '/', [IO.Path]::DirectorySeparatorChar + +# Use ANSI rendering if available +if ($PSStyle.OutputRendering) { + $PSStyle.OutputRendering = 'ANSI' +} + +#region -InstallModule +if ($InstallModule) { + "::group::Installing Modules" | Out-Host + foreach ($moduleToInstall in $InstallModule) { + $moduleInWorkspace = Get-ChildItem -Path $env:GITHUB_WORKSPACE -Recurse -File | + Where-Object Name -eq "$($moduleToInstall).psd1" | + Where-Object { + $(Get-Content $_.FullName -Raw) -match 'ModuleVersion' + } + if (-not $moduleInWorkspace) { + Install-Module $moduleToInstall -Scope CurrentUser -Force + Import-Module $moduleToInstall -Force -PassThru | Out-Host + } + } + "::endgroup::" | Out-Host +} +#endregion -InstallModule + +$PSD1Found = Get-ChildItem -Recurse -Filter "*.psd1" | + Where-Object Name -eq 'Splatter.psd1' | + Select-Object -First 1 + +if ($PSD1Found) { + $PipeScriptModulePath = $PSD1Found + Import-Module $PSD1Found -Force -PassThru | Out-Host +} elseif ($env:GITHUB_ACTION_PATH) { + $SplatterModulePath = Join-Path $env:GITHUB_ACTION_PATH 'Splatter.psd1' + if (Test-path $SplatterModulePath) { + Import-Module $SplatterModulePath -Force -PassThru | Out-Host + } else { + throw "Splatter not found" + } +} elseif (-not (Get-Module Splatter)) { + throw "Action Path not found" +} + +"::notice title=ModuleLoaded::Splatter Loaded from Path - $($SplatterModulePath)" | Out-Host + +$anyFilesChanged = $false +$totalFilesOutputted = 0 +$totalFilesChanged = 0 +$filesOutputted = @() +$filesChanged = @() + +filter ProcessActionOutput { + $out = $_ + + $outItem = Get-Item -Path $out -ErrorAction SilentlyContinue + + $totalFilesOutputted++ + $fullName, $shouldCommit = + if ($out -is [IO.FileInfo]) { + if ($out.FullName -notlike "$repoRoot*") { return } + $out.FullName, (git status $out.Fullname -s) + $filesOutputted += $out + } elseif ($outItem) { + if ($outItem.FullName -notlike "$repoRoot*") { return } + $outItem.FullName, (git status $outItem.Fullname -s) + $filesOutputted += $outItem + } + if ($shouldCommit) { + git add $fullName + $filesChanged += $fullName + if ($out.Message) { + git commit -m "$($out.Message)" | Out-Host + } elseif ($out.CommitMessage) { + git commit -m "$($out.CommitMessage)" | Out-Host + } elseif ($gitHubEvent.head_commit.message) { + git commit -m "$($gitHubEvent.head_commit.message)" | Out-Host + } + $anyFilesChanged = $true + $totalFilesChanged++ + } + $out +} + + + +if (-not $UserName) { + $UserName = + if ($env:GITHUB_TOKEN) { + Invoke-RestMethod -uri "https://api.github.com/user" -Headers @{ + Authorization = "token $env:GITHUB_TOKEN" + } | + Select-Object -First 1 -ExpandProperty name + } else { + $env:GITHUB_ACTOR + } +} + +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" } + +$checkDetached = git symbolic-ref -q HEAD +if (-not $LASTEXITCODE) { + git pull | Out-Host +} + + +$SplatterScriptStart = [DateTime]::Now +if ($SplatterScript) { + Invoke-Expression -Command $SplatterScript | + . ProcessActionOutput | + Out-Host +} +$SplatterScriptTook = [Datetime]::Now - $SplatterScriptStart + +"::notice title=Runtime::$($SplatterScriptTook.TotalMilliseconds)" | Out-Host + +$SplatterPS1Start = [DateTime]::Now +$SplatterPS1List = @() +if (-not $SkipSplatterPS1) { + $SplatterFiles = @( + Get-ChildItem -Recurse -Path $env:GITHUB_WORKSPACE | + Where-Object Name -Match '\.Splatter\.ps1$') + + if ($SplatterFiles) { + $SplatterFiles| + ForEach-Object { + $SplatterPS1List += $_.FullName.Replace($env:GITHUB_WORKSPACE, '').TrimStart('/') + $SplatterPS1Count++ + "::notice title=Running::$($_.Fullname)" | Out-Host + . $_.FullName | + . ProcessActionOutput | + Out-Host + } + } +} + +$SplatterPS1EndStart = [DateTime]::Now +$SplatterPS1Took = [Datetime]::Now - $SplatterPS1Start +"Ran $($SplatterPS1List.Length) Files in $($SplatterPS1Took.TotalMilliseconds)" | Out-Host +if ($filesChanged) { + "::group::$($filesOutputted.Length) files generated with $($filesChanged.Length) changes" | Out-Host + $FilesChanged -join ([Environment]::NewLine) | Out-Host + "::endgroup::" | Out-Host +} else { + "$($filesOutputted.Length) files generated with no changes" +} + +if ($CommitMessage -or $anyFilesChanged) { + if ($CommitMessage) { + Get-ChildItem $env:GITHUB_WORKSPACE -Recurse | + ForEach-Object { + $gitStatusOutput = git status $_.Fullname -s + if ($gitStatusOutput) { + git add $_.Fullname + } + } + + git commit -m $ExecutionContext.SessionState.InvokeCommand.ExpandString($CommitMessage) + } + + $checkDetached = git symbolic-ref -q HEAD 2>&1 + if (-not $LASTEXITCODE) { + "::group::Pulling Changes" | Out-Host + git pull | Out-Host + "::endgroup::" | Out-Host + "::group::Pushing Changes" | Out-Host + git push | Out-Host + "::endgroup::" | Out-Host + } else { + "::warning title=Not pushing changes::(on detached head)" | Out-Host + $LASTEXITCODE = 0 + exit 0 + } +} else { + "Nothing to commit in this build." | Out-Host + exit 0 +} diff --git a/GitHub/Jobs/BuildSplatter.psd1 b/GitHub/Jobs/BuildSplatter.psd1 index cf4b7ac..21a2ea1 100644 --- a/GitHub/Jobs/BuildSplatter.psd1 +++ b/GitHub/Jobs/BuildSplatter.psd1 @@ -22,6 +22,12 @@ name = 'Run HelpOut' uses = 'StartAutomating/HelpOut@master' id = 'HelpOut' - } + }, + @{ + name = 'Run Splatter (on branch)' + if = '${{github.ref_name != ''main''}}' + uses = './' + id = 'SplatterBranch' + } ) } \ No newline at end of file diff --git a/Initialize-Splatter.ps1 b/Initialize-Splatter.ps1 index 2812462..20c29ee 100644 --- a/Initialize-Splatter.ps1 +++ b/Initialize-Splatter.ps1 @@ -58,7 +58,12 @@ # If set, splatter will be defined inline. # This will not preface Splatter with a param() block and PSScriptAnalyzer suppression messages [switch] - $Inline + $Inline, + + # The output path. + # If provided, will output to this file and return the file. + [string] + $OutputPath ) begin { @@ -103,7 +108,7 @@ cXimMTbqP/VR4etMIgAA process { $myParams = @{} + $PSBoundParameters $c, $t, $id = 0, $Verb.Count, [Random]::new().Next() - @( + $SplatterScript = @( if (-not $NoLogo) { $logo = @( $myModule.Name @@ -190,6 +195,22 @@ param()' if (-not $NoLogo) { "#endregion $logo" }) -join [Environment]::NewLine + + if ($outputPath) { + if (-not (Test-Path $outputPath)) { + $null = New-Item -ItemType File -Path $outputPath -Force + } + "$SplatterScript" | Set-Content -Path $outputPath + Get-Item -Path $outputPath + } else { + try { + [ScriptBlock]::Create($SplatterScript) + } catch { + Write-Debug "$($_ | Out-String)" + $SplatterScript + } + } + Write-Progress "Initialized!" " " -Completed -id $id } } \ No newline at end of file diff --git a/Out-Splat.ps1 b/Out-Splat.ps1 index b8c4ae0..1f5854b 100644 --- a/Out-Splat.ps1 +++ b/Out-Splat.ps1 @@ -176,7 +176,12 @@ # This assumes that the first item in the script block is a command, and it will accept the output of the splat as pipelined input [ScriptBlock] [Alias('PipeInto','Pipe')] - $PipeTo) + $PipeTo, + + # The output path. + # If provided, will output to this file and return the file. + [string] + $OutputPath) process { # First, let's find the command. @@ -355,167 +360,168 @@ foreach (`$in in $(if ($inputParameter) { "'$($inputParameter -join "','")'" } e $coreSplat = (@() + $defaultDeclaration + $paramSplat + $cmdDef) -join ([Environment]::NewLine) - if ($FunctionName) { - $commandMeta = $commandExists -as [Management.Automation.CommandMetadata] + $SplatterScript = + if ($FunctionName) { + $commandMeta = $commandExists -as [Management.Automation.CommandMetadata] - foreach ($k in @($commandMeta.Parameters.Keys)) { - if ($InputParameter -notcontains $k) { - $null =$commandMeta.Parameters.Remove($k) - } - } - $originalCmdletBinding = [Management.Automation.Proxycommand]::GetCmdletBindingAttribute($commandExists) - $cmdHelp = Get-Help -Name $commandExists - if ($cmdHelp -is [string]) { $cmdHelp = $null } - $paramBlock = [Management.Automation.Proxycommand]::GetParamBlock($commandExists) -replace '\{(\S{1,})\}', '$1' - - $paramParts = $paramBlock -split ',\W{1,}[\[$]' -ne '' - - $paramBlockParts = - @(foreach ($param in $paramParts) { - $lastDollar = $param.LastIndexOf('$') - $parameterName = $param.Substring($lastDollar + 1).Trim() - $parameterHelp = if ($cmdHelp) { - $cmdHelp.parameters[0].parameter | - Where-Object { $_.Name -eq $parameterName -and $_.Description }| - Select-Object -ExpandProperty Description | - Select-Object -ExpandProperty Text - } else { - $null + foreach ($k in @($commandMeta.Parameters.Keys)) { + if ($InputParameter -notcontains $k) { + $null =$commandMeta.Parameters.Remove($k) + } } + $originalCmdletBinding = [Management.Automation.Proxycommand]::GetCmdletBindingAttribute($commandExists) + $cmdHelp = Get-Help -Name $commandExists + if ($cmdHelp -is [string]) { $cmdHelp = $null } + $paramBlock = [Management.Automation.Proxycommand]::GetParamBlock($commandExists) -replace '\{(\S{1,})\}', '$1' + + $paramParts = $paramBlock -split ',\W{1,}[\[$]' -ne '' + + $paramBlockParts = + @(foreach ($param in $paramParts) { + $lastDollar = $param.LastIndexOf('$') + $parameterName = $param.Substring($lastDollar + 1).Trim() + $parameterHelp = if ($cmdHelp) { + $cmdHelp.parameters[0].parameter | + Where-Object { $_.Name -eq $parameterName -and $_.Description }| + Select-Object -ExpandProperty Description | + Select-Object -ExpandProperty Text + } else { + $null + } - $param = $param.Trim() - if (-not $param.StartsWith('$') -and -not $param.StartsWith('[')) { - $param = "[$param" - } + $param = $param.Trim() + if (-not $param.StartsWith('$') -and -not $param.StartsWith('[')) { + $param = "[$param" + } - if ($ExcludeParameter) { - $shouldExclude = - foreach ($ex in $ExcludeParameter) { - if ($parameterName -like "$ex") { - $true - break + if ($ExcludeParameter) { + $shouldExclude = + foreach ($ex in $ExcludeParameter) { + if ($parameterName -like "$ex") { + $true + break + } } - } - if ($shouldExclude) { continue } - } - - if ($parameterHelp) { - $lines = "$parameterHelp".Split("`r`n",[StringSplitOptions]'RemoveEmptyEntries') - if ($lines.Count -lt 8) { - (@(foreach ($l in $lines) { -" #$l" - }) -join ([Environment]::NewLine)) + - ([Environment]::NewLine) + (' '*4) + $param - } else { -" <# -$parameterHelp - #> - $param" + if ($shouldExclude) { continue } } - } else { - $param - } - }) - - if ($AdditionalParameter) { - foreach ($kv in $AdditionalParameter.GetEnumerator()) { - $varName = if ($kv.Key.StartsWith('$')) { $kv.Key } else { '$' + $kv.Key } - - $newParamBlockPart = - if ($kv.Value -is [type]) { - if ($kv.Value.FullName -like "System.*") { - " [$($kv.Value.Fullname.Substring(7))]$varName" - } elseif ($kv.Value -eq [switch]) { - " [switch]$varName" + + if ($parameterHelp) { + $lines = "$parameterHelp".Split("`r`n",[StringSplitOptions]'RemoveEmptyEntries') + if ($lines.Count -lt 8) { + (@(foreach ($l in $lines) { + " #$l" + }) -join ([Environment]::NewLine)) + + ([Environment]::NewLine) + (' '*4) + $param } else { - " [$($kv.Value.Fullname)]$varName" + " <# + $parameterHelp + #> + $param" } + } else { + $param } - elseif ($kv.Value -is [string]) { - $varDeclared = $false - $newLines = - foreach ($line in $kv.Value -split [Environment]::NewLine) { - $trimLine = $Line.Trim() - if ($trimLine.StartsWith('[')) { - if ($trimLine -like $varName) { + }) + + if ($AdditionalParameter) { + foreach ($kv in $AdditionalParameter.GetEnumerator()) { + $varName = if ($kv.Key.StartsWith('$')) { $kv.Key } else { '$' + $kv.Key } + + $newParamBlockPart = + if ($kv.Value -is [type]) { + if ($kv.Value.FullName -like "System.*") { + " [$($kv.Value.Fullname.Substring(7))]$varName" + } elseif ($kv.Value -eq [switch]) { + " [switch]$varName" + } else { + " [$($kv.Value.Fullname)]$varName" + } + } + elseif ($kv.Value -is [string]) { + $varDeclared = $false + $newLines = + foreach ($line in $kv.Value -split [Environment]::NewLine) { + $trimLine = $Line.Trim() + if ($trimLine.StartsWith('[')) { + if ($trimLine -like $varName) { + $varDeclared = $true + } + (' ' * 4) + $trimLine + } # It's an attribute! + elseif ($trimLine.StartsWith('$')) { # It's a variable! $varDeclared = $true + (' ' * 4) + $trimLine + } + elseif ($trimLine.StartsWith('#')) { # It's a comment! + (' ' * 4) + $trimLine + } + else { # Otherwise, we'll treat it like a comment anyways + (' ' * 4) + '#' + $trimLine } - (' ' * 4) + $trimLine - } # It's an attribute! - elseif ($trimLine.StartsWith('$')) { # It's a variable! - $varDeclared = $true - (' ' * 4) + $trimLine - } - elseif ($trimLine.StartsWith('#')) { # It's a comment! - (' ' * 4) + $trimLine - } - else { # Otherwise, we'll treat it like a comment anyways - (' ' * 4) + '#' + $trimLine } - } - if (-not $varDeclared) { - $newLines += (' ' * 4) + $varName + if (-not $varDeclared) { + $newLines += (' ' * 4) + $varName + } + $newLines -join ([Environment]::NewLine) } - $newLines -join ([Environment]::NewLine) - } - elseif ($kv.Value -is [Object[]]) { + elseif ($kv.Value -is [Object[]]) { - } + } - if ($newParamBlockPart) { - $paramBlockParts += $newParamBlockPart + if ($newParamBlockPart) { + $paramBlockParts += $newParamBlockPart + } } } - } - $paramBlock = $paramBlockParts -join (',' + ([Environment]::NewLine * 2)) + $paramBlock = $paramBlockParts -join (',' + ([Environment]::NewLine * 2)) - if (-not $Synopsis) { - $Synopsis = "Wraps $CommandName" - } + if (-not $Synopsis) { + $Synopsis = "Wraps $CommandName" + } - if (-not $Description) { - $Description = "Calls $CommandName, using splatting" - } + if (-not $Description) { + $Description = "Calls $CommandName, using splatting" + } - $exampleText = - if ($Example) { - @(foreach ($ex in $Example) { - " .Example" - foreach ($ln in $ex -split '(?>\r\n|\n)') { - " $ln" - } - }) -join [Environment]::NewLine - } else { ''} - - $noteText = - if ($Note) { - @( - " .Notes" - foreach ($ln in $Note -split '(?>\r\n|\n)') { - " $ln" - } - ) -join [Environment]::NewLine - } else { ''} - - - - $linkText = - if ($Link) { - @(foreach ($lnk in $Link) { - " .Link" - " $lnk" - }) -join [Environment]::NewLine - } else { @" + $exampleText = + if ($Example) { + @(foreach ($ex in $Example) { + " .Example" + foreach ($ln in $ex -split '(?>\r\n|\n)') { + " $ln" + } + }) -join [Environment]::NewLine + } else { ''} + + $noteText = + if ($Note) { + @( + " .Notes" + foreach ($ln in $Note -split '(?>\r\n|\n)') { + " $ln" + } + ) -join [Environment]::NewLine + } else { ''} + + + + $linkText = + if ($Link) { + @(foreach ($lnk in $Link) { + " .Link" + " $lnk" + }) -join [Environment]::NewLine + } else { @" .Link $CommandName "@ - } - + } + [ScriptBlock]::Create("function $FunctionName { @@ -563,9 +569,19 @@ $(@(foreach ($line in $coreSplat -split ([Environment]::Newline)) { } ") + } else { + [ScriptBlock]::Create($coreSplat) + } + + if ($outputPath) { + if (-not (Test-Path $outputPath)) { + $null = New-Item -ItemType File -Path $outputPath -Force + } + "$SplatterScript" | Set-Content -Path $outputPath + Get-Item -Path $outputPath } else { - [ScriptBlock]::Create($coreSplat) + $SplatterScript } - } } + diff --git a/Splatter.Splatter.ps1 b/Splatter.Splatter.ps1 new file mode 100644 index 0000000..b5f3a7b --- /dev/null +++ b/Splatter.Splatter.ps1 @@ -0,0 +1,3 @@ +Initialize-Splatter -Verb Get, Use, Find -OutputPath .\@.ps1 +Initialize-Splatter -Verb Get, Use, Find -OutputPath .\@.min.ps1 -Minify +Initialize-Splatter -Verb Get, Use, Find -OutputPath .\@.min.gzip.ps1 -Minify -Compress diff --git a/Splatter.psd1 b/Splatter.psd1 index d1885e1..09cb014 100644 --- a/Splatter.psd1 +++ b/Splatter.psd1 @@ -1,9 +1,9 @@ @{ CompanyName = 'Start-Automating' - Copyright = '2019-2021 Start-Automating' + Copyright = '2019-2023 Start-Automating' RootModule = 'Splatter.psm1' Description = 'Simple Scripts to Supercharge Splatting' - ModuleVersion = '0.5.4' + ModuleVersion = '0.5.5' AliasesToExport = '*' VariablesToExport = '*' GUID = '033f35ed-f8a7-4911-bb62-2691f505ed43' @@ -15,6 +15,15 @@ IconURI = 'https://raw.githubusercontent.com/StartAutomating/Splatter/master/Assets/Splatter.png' Tags = 'Splatting', 'PipeScript' ReleaseNotes = @' +### 0.5.5: + +* Splatter is now a GitHub Action! (#18) +* Initialize-Splatter now returns a `[ScriptBlock]` (#21) +* Initialize-Splatter/Out-Splat now have -OutputPath (#19/#20) +* Added Sponsorship (#22) + +--- + ### 0.5.4: * New Splatter Logo (#12) diff --git a/Splatter.psm1 b/Splatter.psm1 index a52d75a..fdb24fb 100644 --- a/Splatter.psm1 +++ b/Splatter.psm1 @@ -8,11 +8,11 @@ param() . $psScriptRoot\Initialize-Splatter.ps1 -# Assign each splatter command to a variable for easy internal access -${?@} = $gSplat = $GetSplat = ${function:Get-Splat} -${??@} = $fSplat = $FindSplat = ${function:Find-Splat} -${*@} = $mSplat = $MergeSplat = ${function:Merge-Splat} -${.@} = $uSplat = $UseSplat = ${function:Use-Splat} -${=>@} = $uSplat = $OutSplat = ${function:Out-Splat} +# Assign each splatter command to a variable for another easy way to access +${?@} = $gSplat = $GetSplat = ${function:Get-Splat} +${??@} = $fSplat = $FindSplat = ${function:Find-Splat} +${*@} = $mSplat = $MergeSplat = ${function:Merge-Splat} +${.@} = $uSplat = $UseSplat = ${function:Use-Splat} +${=>@} = $uSplat = $OutSplat = ${function:Out-Splat} Export-ModuleMember -Alias * -Function * -Variable * diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..49a63b9 --- /dev/null +++ b/action.yml @@ -0,0 +1,323 @@ + +name: UseSplatter +description: Simple Scripts to Supercharge Splatting +inputs: + SplatterScript: + required: false + description: | + A PowerShell Script that uses Splatter. + Any files outputted from the script will be added to the repository. + If those files have a .Message attached to them, they will be committed with that message. + SkipSplatterPS1: + required: false + description: 'If set, will not process any files named *.Splatter.ps1' + InstallModule: + required: false + description: A list of modules to be installed from the PowerShell gallery before scripts run. + CommitMessage: + required: false + description: | + If provided, will commit any remaining changes made to the workspace with this commit message. + If no commit message is provided, changes will not be committed. + UserEmail: + required: false + description: The user email associated with a git commit. + UserName: + required: false + description: The user name associated with a git commit. +branding: + icon: at-sign + color: blue +runs: + using: composite + steps: + - name: SplatterAction + id: SplatterAction + shell: pwsh + env: + UserEmail: ${{inputs.UserEmail}} + SkipSplatterPS1: ${{inputs.SkipSplatterPS1}} + CommitMessage: ${{inputs.CommitMessage}} + InstallModule: ${{inputs.InstallModule}} + UserName: ${{inputs.UserName}} + SplatterScript: ${{inputs.SplatterScript}} + run: | + $Parameters = @{} + $Parameters.SplatterScript = ${env:SplatterScript} + $Parameters.SkipSplatterPS1 = ${env:SkipSplatterPS1} + $Parameters.SkipSplatterPS1 = $parameters.SkipSplatterPS1 -match 'true'; + $Parameters.InstallModule = ${env:InstallModule} + $Parameters.InstallModule = $parameters.InstallModule -split ';' -replace '^[''"]' -replace '[''"]$' + $Parameters.CommitMessage = ${env:CommitMessage} + $Parameters.UserEmail = ${env:UserEmail} + $Parameters.UserName = ${env:UserName} + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: SplatterAction $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {<# + .Synopsis + GitHub Action for Splatter + .Description + GitHub Action for Splatter. This will: + + * Import Splatter + * Run all *.Splatter.ps1 files beneath the workflow directory + * Run a .SplatterScript parameter + + Any files changed can be outputted by the script, and those changes can be checked back into the repo. + Make sure to use the "persistCredentials" option with checkout. + #> + + param( + # A PowerShell Script that uses Splatter. + # Any files outputted from the script will be added to the repository. + # If those files have a .Message attached to them, they will be committed with that message. + [string] + $SplatterScript, + + # If set, will not process any files named *.Splatter.ps1 + [switch] + $SkipSplatterPS1, + + # A list of modules to be installed from the PowerShell gallery before scripts run. + [string[]] + $InstallModule = @(), + + # If provided, will commit any remaining changes made to the workspace with this commit message. + # If no commit message is provided, changes will not be committed. + [string] + $CommitMessage, + + # The user email associated with a git commit. + [string] + $UserEmail, + + # The user name associated with a git commit. + [string] + $UserName + ) + + "::group::Parameters" | Out-Host + [PSCustomObject]$PSBoundParameters | Format-List | Out-Host + "::endgroup::" | Out-Host + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + + # Check to ensure we are on a branch + $branchName = git rev-parse --abrev-ref HEAD + # If we were not, return. + if ((-not $branchName) -or $LASTEXITCODE) { + $LASTEXITCODE = 0 + "::warning title=No Branch Found::Not on a Branch. Can not run." | Out-Host + exit 0 + return + } + + $repoRoot = (git rev-parse --show-toplevel *>&1) -replace '/', [IO.Path]::DirectorySeparatorChar + + # Use ANSI rendering if available + if ($PSStyle.OutputRendering) { + $PSStyle.OutputRendering = 'ANSI' + } + + #region -InstallModule + if ($InstallModule) { + "::group::Installing Modules" | Out-Host + foreach ($moduleToInstall in $InstallModule) { + $moduleInWorkspace = Get-ChildItem -Path $env:GITHUB_WORKSPACE -Recurse -File | + Where-Object Name -eq "$($moduleToInstall).psd1" | + Where-Object { + $(Get-Content $_.FullName -Raw) -match 'ModuleVersion' + } + if (-not $moduleInWorkspace) { + Install-Module $moduleToInstall -Scope CurrentUser -Force + Import-Module $moduleToInstall -Force -PassThru | Out-Host + } + } + "::endgroup::" | Out-Host + } + #endregion -InstallModule + + $PSD1Found = Get-ChildItem -Recurse -Filter "*.psd1" | + Where-Object Name -eq 'Splatter.psd1' | + Select-Object -First 1 + + if ($PSD1Found) { + $PipeScriptModulePath = $PSD1Found + Import-Module $PSD1Found -Force -PassThru | Out-Host + } elseif ($env:GITHUB_ACTION_PATH) { + $SplatterModulePath = Join-Path $env:GITHUB_ACTION_PATH 'Splatter.psd1' + if (Test-path $SplatterModulePath) { + Import-Module $SplatterModulePath -Force -PassThru | Out-Host + } else { + throw "Splatter not found" + } + } elseif (-not (Get-Module Splatter)) { + throw "Action Path not found" + } + + "::notice title=ModuleLoaded::Splatter Loaded from Path - $($SplatterModulePath)" | Out-Host + + $anyFilesChanged = $false + $totalFilesOutputted = 0 + $totalFilesChanged = 0 + $filesOutputted = @() + $filesChanged = @() + + filter ProcessActionOutput { + $out = $_ + + $outItem = Get-Item -Path $out -ErrorAction SilentlyContinue + + $totalFilesOutputted++ + $fullName, $shouldCommit = + if ($out -is [IO.FileInfo]) { + if ($out.FullName -notlike "$repoRoot*") { return } + $out.FullName, (git status $out.Fullname -s) + $filesOutputted += $out + } elseif ($outItem) { + if ($outItem.FullName -notlike "$repoRoot*") { return } + $outItem.FullName, (git status $outItem.Fullname -s) + $filesOutputted += $outItem + } + if ($shouldCommit) { + git add $fullName + $filesChanged += $fullName + if ($out.Message) { + git commit -m "$($out.Message)" | Out-Host + } elseif ($out.CommitMessage) { + git commit -m "$($out.CommitMessage)" | Out-Host + } elseif ($gitHubEvent.head_commit.message) { + git commit -m "$($gitHubEvent.head_commit.message)" | Out-Host + } + $anyFilesChanged = $true + $totalFilesChanged++ + } + $out + } + + + + if (-not $UserName) { + $UserName = + if ($env:GITHUB_TOKEN) { + Invoke-RestMethod -uri "https://api.github.com/user" -Headers @{ + Authorization = "token $env:GITHUB_TOKEN" + } | + Select-Object -First 1 -ExpandProperty name + } else { + $env:GITHUB_ACTOR + } + } + + 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" } + + $checkDetached = git symbolic-ref -q HEAD + if (-not $LASTEXITCODE) { + git pull | Out-Host + } + + + $SplatterScriptStart = [DateTime]::Now + if ($SplatterScript) { + Invoke-Expression -Command $SplatterScript | + . ProcessActionOutput | + Out-Host + } + $SplatterScriptTook = [Datetime]::Now - $SplatterScriptStart + + "::notice title=Runtime::$($SplatterScriptTook.TotalMilliseconds)" | Out-Host + + $SplatterPS1Start = [DateTime]::Now + $SplatterPS1List = @() + if (-not $SkipSplatterPS1) { + $SplatterFiles = @( + Get-ChildItem -Recurse -Path $env:GITHUB_WORKSPACE | + Where-Object Name -Match '\.Splatter\.ps1$') + + if ($SplatterFiles) { + $SplatterFiles| + ForEach-Object { + $SplatterPS1List += $_.FullName.Replace($env:GITHUB_WORKSPACE, '').TrimStart('/') + $SplatterPS1Count++ + "::notice title=Running::$($_.Fullname)" | Out-Host + . $_.FullName | + . ProcessActionOutput | + Out-Host + } + } + } + + $SplatterPS1EndStart = [DateTime]::Now + $SplatterPS1Took = [Datetime]::Now - $SplatterPS1Start + "Ran $($SplatterPS1List.Length) Files in $($SplatterPS1Took.TotalMilliseconds)" | Out-Host + if ($filesChanged) { + "::group::$($filesOutputted.Length) files generated with $($filesChanged.Length) changes" | Out-Host + $FilesChanged -join ([Environment]::NewLine) | Out-Host + "::endgroup::" | Out-Host + } else { + "$($filesOutputted.Length) files generated with no changes" + } + + if ($CommitMessage -or $anyFilesChanged) { + if ($CommitMessage) { + Get-ChildItem $env:GITHUB_WORKSPACE -Recurse | + ForEach-Object { + $gitStatusOutput = git status $_.Fullname -s + if ($gitStatusOutput) { + git add $_.Fullname + } + } + + git commit -m $ExecutionContext.SessionState.InvokeCommand.ExpandString($CommitMessage) + } + + $checkDetached = git symbolic-ref -q HEAD 2>&1 + if (-not $LASTEXITCODE) { + "::group::Pulling Changes" | Out-Host + git pull | Out-Host + "::endgroup::" | Out-Host + "::group::Pushing Changes" | Out-Host + git push | Out-Host + "::endgroup::" | Out-Host + } else { + "::warning title=Not pushing changes::(on detached head)" | Out-Host + $LASTEXITCODE = 0 + exit 0 + } + } else { + "Nothing to commit in this build." | Out-Host + exit 0 + } + } @Parameters + diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d69f734..70e4ee9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,3 +1,12 @@ +### 0.5.5: + +* Splatter is now a GitHub Action! (#18) +* Initialize-Splatter now returns a `[ScriptBlock]` (#21) +* Initialize-Splatter/Out-Splat now have -OutputPath (#19/#20) +* Added Sponsorship (#22) + +--- + ### 0.5.4: * New Splatter Logo (#12) diff --git a/docs/Initialize-Splatter.md b/docs/Initialize-Splatter.md index fbcdefe..adf967e 100644 --- a/docs/Initialize-Splatter.md +++ b/docs/Initialize-Splatter.md @@ -183,6 +183,22 @@ This will not preface Splatter with a param() block and PSScriptAnalyzer suppres +#### **OutputPath** + +The output path. +If provided, will output to this file and return the file. + + + + + + +|Type |Required|Position|PipelineInput| +|----------|--------|--------|-------------| +|`[String]`|false |2 |false | + + + --- @@ -190,5 +206,5 @@ This will not preface Splatter with a param() block and PSScriptAnalyzer suppres ### Syntax ```PowerShell -Initialize-Splatter [[-Verb] ] [-Compress] [-Minify] [-NoLogo] [-NoHelp] [-AsFunction] [-Inline] [] +Initialize-Splatter [[-Verb] ] [-Compress] [-Minify] [-NoLogo] [-NoHelp] [-AsFunction] [-Inline] [[-OutputPath] ] [] ``` diff --git a/docs/Out-Splat.md b/docs/Out-Splat.md index 7daf7f3..1d59351 100644 --- a/docs/Out-Splat.md +++ b/docs/Out-Splat.md @@ -489,6 +489,22 @@ This assumes that the first item in the script block is a command, and it will a +#### **OutputPath** + +The output path. +If provided, will output to this file and return the file. + + + + + + +|Type |Required|Position|PipelineInput| +|----------|--------|--------|-------------| +|`[String]`|false |named |false | + + + --- @@ -507,11 +523,11 @@ This assumes that the first item in the script block is a command, and it will a ### Syntax ```PowerShell -Out-Splat [-CommandName] [[-DefaultParameter] ] [-ArgumentList ] [[-InputParameter] ] [[-ExcludeParameter] ] [-DefaultOverride] [-VariableInput] [-VariableName ] [-SerializationDepth ] [-CrossStream] [-Where ] [-Begin ] [-Process ] [-End ] [-PipeTo ] [] +Out-Splat [-CommandName] [[-DefaultParameter] ] [-ArgumentList ] [[-InputParameter] ] [[-ExcludeParameter] ] [-DefaultOverride] [-VariableInput] [-VariableName ] [-SerializationDepth ] [-CrossStream] [-Where ] [-Begin ] [-Process ] [-End ] [-PipeTo ] [-OutputPath ] [] ``` ```PowerShell -Out-Splat [-CommandName] [[-DefaultParameter] ] [-ArgumentList ] [[-InputParameter] ] [[-ExcludeParameter] ] [-DefaultOverride] [-VariableInput] [-VariableName ] [-FunctionName] [-Synopsis ] [-Description ] [-Example ] [-Link ] [-Note ] [-CmdletBinding ] [-OutputType ] [-AdditionalParameter ] [-SerializationDepth ] [-CrossStream] [-Where ] [-Begin ] [-Process ] [-End ] [-PipeTo ] [] +Out-Splat [-CommandName] [[-DefaultParameter] ] [-ArgumentList ] [[-InputParameter] ] [[-ExcludeParameter] ] [-DefaultOverride] [-VariableInput] [-VariableName ] [-FunctionName] [-Synopsis ] [-Description ] [-Example ] [-Link ] [-Note ] [-CmdletBinding ] [-OutputType ] [-AdditionalParameter ] [-SerializationDepth ] [-CrossStream] [-Where ] [-Begin ] [-Process ] [-End ] [-PipeTo ] [-OutputPath ] [] ``` ```PowerShell -Out-Splat [-CommandName] [[-DefaultParameter] ] [-ArgumentList ] [[-InputParameter] ] [[-ExcludeParameter] ] [-DefaultOverride] [-VariableInput] [-VariableName ] [-SerializationDepth ] -DynamicParameter [-Unpiped] [-Offset ] [-NewParameterSetName ] [-CrossStream] [-Where ] [-Begin ] [-Process ] [-End ] [-PipeTo ] [] +Out-Splat [-CommandName] [[-DefaultParameter] ] [-ArgumentList ] [[-InputParameter] ] [[-ExcludeParameter] ] [-DefaultOverride] [-VariableInput] [-VariableName ] [-SerializationDepth ] -DynamicParameter [-Unpiped] [-Offset ] [-NewParameterSetName ] [-CrossStream] [-Where ] [-Begin ] [-Process ] [-End ] [-PipeTo ] [-OutputPath ] [] ```