Last active
October 16, 2023 16:14
-
-
Save Hashbrown777/a5a02e2fd3eeed4485d4ba073ef3b143 to your computer and use it in GitHub Desktop.
Useful utils for pwsh
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#just to name the gist as per https://stackoverflow.com/a/19904644/2518317 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#asynchronous - How to execute a PowerShell function several times in parallel? - Stack Overflow | |
#https://stackoverflow.com/questions/12766174/how-to-execute-a-powershell-function-several-times-in-parallel/60932479#60932479 | |
. "$PSScriptRoot/asynclet.ps1" | |
#asynchronously run a pool of tasks, | |
#and aggregate the results back into a synchronous output | |
Function Async { Param( | |
$Asynclet, | |
#maximum permitted simultaneous background tasks | |
[int]$BatchSize = [int]$env:NUMBER_OF_PROCESSORS * 3, | |
#number of items received in the input to cache before instantiating a single job to process in bulk | |
#only takes effect if -AsJob is false | |
[int]$Collate = 1, | |
#the task that accepts input on a pipe to execute in the background | |
#may be a string representing a cmdlet | |
<#[scriptblock]#>$Func, | |
#output data as soon as it's received from each asynchronous process | |
#the default is to output all data from processes at each of their conclusions | |
[switch]$Immediate, | |
#because your task is in a subshell you wont have access to your outer scope, | |
#you may pass them in here | |
[array]$ArgumentList = @(), | |
[System.Collections.IDictionary]$Parameters = @{}, | |
#the title of the progress bar | |
[string]$Name = 'Processing', | |
#instead of being backgrounded itself, your -Func may return a [Job]/[Task] | |
#it must accept @{job;input;args;params} as its only argument, and insert the job to that object | |
#optionally job may be a [scriptblock] which will be backgrounded, in which case you should overwrite input, args, and params as well | |
[switch]$AsJob, | |
#if you know the number of tasks ahead of time, | |
#providing it here will have the progress bar show an ETA | |
[int]$Expected, | |
#outputs of this stream will be @{job;input;error} where job is the result | |
[switch]$PassThru, | |
#the time it takes to give up on one job type if there are others waiting | |
[int]$Retry = 5, | |
[switch]$IgnoreError | |
) | |
Begin { | |
$ArgumentList = [Array]::AsReadOnly($ArgumentList) | |
$Parameters = $Parameters.GetEnumerator() ` | |
| &{ | |
Begin { $params=[ordered]@{} } | |
Process { $params.Add($_.Key, $_.Value) } | |
End { $params.AsReadOnly() } | |
} | |
#the currently running background tasks | |
$running = @{} | |
$counts = [PSCustomObject]@{ | |
completed = 0 | |
jobs = 0 | |
tasks = 0 | |
results = 0 | |
} | |
#a lazy attempt at uniquely IDing this instance for Write-Progress | |
$asyncId = Get-Random | |
#a timer for Write-Progress | |
$timer = [system.diagnostics.stopwatch]::StartNew() | |
$bulk = [PSCustomObject]@{ | |
pwsh = $NULL | |
count = 0 | |
} | |
$inputs = @{} | |
$pool = [RunspaceFactory]::CreateRunspacePool(1, $BatchSize) | |
$pool.Open() | |
#called whenever we want to update the progress bar | |
Function Progress { Param($Reason) | |
#calculate ETA if applicable | |
$eta = -1 | |
$total = [math]::Max(1, $counts.completed + $running.Count) | |
if ($Expected) { | |
$total = [math]::Max($total, [math]::Ceiling($Expected / $Collate)) | |
if ($counts.completed) { | |
$eta = ( | |
($total - $counts.completed) * ` | |
$timer.Elapsed.TotalSeconds / ` | |
$counts.completed | |
) | |
} | |
} | |
$Reason=Switch -regex ($Reason) { | |
'^done$' { "Finishing up the final $($running.Count) jobs." } | |
'^(do|next)$' { " | |
Running | |
$($running.Count) | |
jobs concurrently. | |
$(@('Adding','Waiting to add')[!($Reason -eq 'do')]) | |
job # | |
$($counts.completed + $running.Count + 1) | |
" -replace '\r?\n\t*','' } | |
Default { " | |
Running $($running.Count) jobs concurrently. | |
Emitting | |
$($counts.completed) | |
$(@{1='st';2='nd';3='rd'}[$counts.completed % 10] -replace '^$','th') | |
result. | |
" -replace '\r?\n\t*','' } | |
} | |
Write-Progress ` | |
-Id $asyncId ` | |
-Activity $Name ` | |
-SecondsRemaining $eta ` | |
-Status (" | |
$($counts.completed) | |
jobs completed in | |
$([Math]::Floor($timer.Elapsed.TotalMinutes)) | |
: | |
$($timer.Elapsed.Seconds -replace '^(.)$','0$1') | |
" -replace '\r?\n\t*','') ` | |
-CurrentOperation $Reason ` | |
-PercentComplete (100 * $counts.completed / $total) | |
} | |
#called with the [Job]'s that have completed | |
Filter Done { | |
$job = $NULL | |
$id = $_.Id | |
$error = $NULL | |
$in = $inputs.Item($Id) | |
$running.Remove($id) | |
$inputs.Remove($id) | |
++$counts.completed | |
Progress | |
if ($_ -is [System.Management.Automation.Job]) { | |
--$counts.jobs | |
try { | |
$job = $_ | Receive-Job | |
} | |
catch { | |
$error = $_ | |
} | |
} | |
elseif ($_.pwsh) { | |
--$counts.results | |
try { | |
$job = $_.pwsh.done() | |
} | |
catch { | |
#[System.Management.Automation.MethodInvocationException] | |
$error = $_.Exception.InnerException | |
} | |
} | |
elseif ($_.IsFaulted) { | |
--$counts.tasks | |
#[System.AggregateException] | |
$error = $_.Exception.InnerException | |
} | |
else { | |
--$counts.tasks | |
$job = $_.Result | |
} | |
if ($PassThru) { | |
[PSCustomObject]@{ | |
job = $job | |
input = $in | |
error = $error | |
} | |
} | |
elseif ($error -and !$IgnoreError) { | |
throw $error | |
} | |
else { | |
$job | |
} | |
} | |
$handle = { $_.AsyncWaitHandle } | |
$isJob = { $_ -is [System.Management.Automation.Job] } | |
$isTask = { $_ -is [System.Threading.Tasks.Task] } | |
$isResult = { $_ -is [IAsyncResult] } | |
$isFinished = { | |
$_.IsCompleted -or ` | |
( | |
$_.JobStateInfo.State -gt 1 -and | |
$_.JobStateInfo.State -ne 6 -and | |
$_.JobStateInfo.State -ne 8 | |
) | |
} | |
Function Jobs { Param($Filter) | |
$running.Values | ? $Filter | |
} | |
#called whenever we need to wait for at least one task to completed | |
#outputs the completed tasks | |
Function Wait { Param([switch]$Finishing) | |
#if we are at the max background tasks this instant | |
while ($running.Count -ge $BatchSize) { | |
Progress -Reason @('done','next')[!$Finishing] | |
if ($Immediate) { | |
Jobs -Filter { $_.pwsh.has() } ` | |
| %{ $_.pwsh.readAll() } | |
} | |
$value = @('jobs', 'tasks', 'results') ` | |
| %{ $counts.($_) } ` | |
| measure -Maximum -Sum | |
$wait =` | |
if ($value.Maximum -lt $value.Sum) { | |
$Retry | |
} | |
else { | |
-1 | |
} | |
$value = Switch -exact ($value.Maximum) { | |
$counts.jobs { | |
(Wait-Job ` | |
-Any ` | |
-Job (Jobs -Filter $isJob) ` | |
-Timeout $wait | |
).Count -lt 1 | |
break | |
} | |
#[task]s have `AsyncWaitHandle`s just like [IAsyncResult]s, so there's no need for specific code | |
# $counts.tasks { | |
# [System.Threading.Tasks.Task]::WaitAny( | |
# (Jobs -Filter $isTask), | |
# [math]::Max($wait * 1000, -1) | |
# ) -lt 0 | |
# break | |
# } | |
# $counts.results { | |
# [System.Threading.WaitHandle]::WaitAny( | |
# (Jobs -Filter $isResult | % $handle), | |
# [math]::Max($wait * 1000, -1) | |
# ) -eq [System.Threading.WaitHandle]::WaitTimeout | |
# break | |
# } | |
Default { | |
[System.Threading.WaitHandle]::WaitAny( | |
(Jobs -Filter $handle | % $handle), | |
[math]::Max($wait * 1000, -1) | |
) -eq [System.Threading.WaitHandle]::WaitTimeout | |
} | |
} | |
(Jobs -Filter $isFinished) | Done | |
} | |
} | |
Function Run { Param($run=$NULL) | |
Wait | |
Progress -Reason 'do' | |
$run = [PSCustomObject]@{ | |
input = $run | |
job = $Func | |
args = $ArgumentList | |
params = $Parameters | |
} | |
if ($AsJob) { | |
$run.job = $NULL | |
Invoke-Command ` | |
-ScriptBlock $Func ` | |
-ArgumentList @($run) ` | |
| Out-Null | |
} | |
if ($run.job | % $isJob) { | |
++$counts.jobs | |
} | |
elseif ($run.job | % $isTask) { | |
++$counts.tasks | |
} | |
#if we weren't given a [Job] we need to spawn it for them | |
else {#if ($run.job -is [ScriptBlock]) { | |
if ($Asynclet) { | |
$bulk.pwsh = $Asynclet::new($pool, $run.job, $run.params, $run.args) | |
} | |
else { | |
$bulk.pwsh = [Asynclet]::new($pool, $run.job, $run.params, $run.args) | |
} | |
$run.job = $bulk.pwsh.job ` | |
| Add-Member ` | |
-MemberType NoteProperty ` | |
-Name pwsh ` | |
-Value $bulk.pwsh ` | |
-PassThru ` | |
| Add-Member ` | |
-MemberType NoteProperty ` | |
-Name Id ` | |
-Value $bulk.pwsh.job.AsyncWaitHandle.Handle.ToString() ` | |
-PassThru | |
++$counts.results | |
if ($AsJob) { | |
$run.input | %{ $bulk.pwsh.input($_) } | |
$bulk.pwsh.input() | |
} | |
} | |
# else { | |
# throw "$($run.job.GetType()) needs to be a ScriptBlock" | |
# } | |
$running.Add($run.job.Id, $run.job) | Out-Null | |
if ($PassThru) { | |
$inputs.Add($run.job.Id, $run.input) | Out-Null | |
} | |
} | |
} | |
#accepts inputs to spawn a new background task with | |
Process { | |
if ($AsJob) { | |
Run -run $_ | |
return | |
} | |
if (!$bulk.pwsh) { | |
Run | |
$bulk.count = 0 | |
} | |
$bulk.pwsh.input($_) | |
if (++$bulk.count -ge $Collate) { | |
$bulk.pwsh.input() | |
$bulk.pwsh = $NULL | |
} | |
} | |
End { | |
if ($bulk.pwsh -and !$AsJob) { | |
$bulk.pwsh.input() | |
} | |
#wait for the remaining running processes | |
$BatchSize=1 | |
Wait -Finishing | |
Write-Progress -Id $asyncId -Activity $Name -Completed | |
$pool.Close() | |
$pool.Dispose() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#allows you to run a cmdlet seperate to the script's program-flow, and pipe data in | |
#at various points of the program, passing the cmdlet instance, and getting the | |
#resultant stream later | |
class Asynclet { | |
$put=[System.Management.Automation.PSDataCollection[PSObject]]::new() | |
$out=[System.Management.Automation.PSDataCollection[PSObject]]::new() | |
$pwsh=$NULL | |
[IAsyncResult]$job=$NULL | |
hidden init( | |
$Cmdlet, | |
[System.Collections.IDictionary]$Parameters, | |
[array]$ArgumentList, | |
$pool | |
) { | |
$this.pwsh=[powershell]::Create() | |
if ($pool) { | |
$this.pwsh.RunspacePool = $pool | |
} | |
if ($Cmdlet -is [ScriptBlock]) { | |
$this.pwsh.AddScript($Cmdlet) | |
} | |
else { | |
$this.pwsh.AddCommand($Cmdlet) | |
} | |
$ArgumentList | %{ $this.pwsh.AddArgument($_) } | |
$this.job=$this.pwsh.AddParameters($Parameters).BeginInvoke($this.put, $this.out) | |
} | |
Asynclet( [string] $Cmdlet, [System.Collections.IDictionary]$Parameters, [array]$ArgumentList) { $this.init($Cmdlet, $Parameters, $ArgumentList, $NULL) } | |
Asynclet( [string] $Cmdlet, [System.Collections.IDictionary]$Parameters ) { $this.init($Cmdlet, $Parameters, @() , $NULL) } | |
Asynclet( [string] $Cmdlet, [array]$ArgumentList) { $this.init($Cmdlet, @{} , $ArgumentList, $NULL) } | |
Asynclet( [string] $Cmdlet ) { $this.init($Cmdlet, @{} , @() , $NULL) } | |
Asynclet( [scriptblock]$Cmdlet, [System.Collections.IDictionary]$Parameters, [array]$ArgumentList) { $this.init($Cmdlet, $Parameters, $ArgumentList, $NULL) } | |
Asynclet( [scriptblock]$Cmdlet, [System.Collections.IDictionary]$Parameters ) { $this.init($Cmdlet, $Parameters, @() , $NULL) } | |
Asynclet( [scriptblock]$Cmdlet, [array]$ArgumentList) { $this.init($Cmdlet, @{} , $ArgumentList, $NULL) } | |
Asynclet( [scriptblock]$Cmdlet ) { $this.init($Cmdlet, @{} , @() , $NULL) } | |
Asynclet([System.Management.Automation.Runspaces.RunspacePool]$pool, [string] $Cmdlet, [System.Collections.IDictionary]$Parameters, [array]$ArgumentList) { $this.init($Cmdlet, $Parameters, $ArgumentList, $pool) } | |
Asynclet([System.Management.Automation.Runspaces.RunspacePool]$pool, [string] $Cmdlet, [System.Collections.IDictionary]$Parameters ) { $this.init($Cmdlet, $Parameters, @() , $pool) } | |
Asynclet([System.Management.Automation.Runspaces.RunspacePool]$pool, [string] $Cmdlet, [array]$ArgumentList) { $this.init($Cmdlet, @{} , $ArgumentList, $pool) } | |
Asynclet([System.Management.Automation.Runspaces.RunspacePool]$pool, [string] $Cmdlet ) { $this.init($Cmdlet, @{} , @() , $pool) } | |
Asynclet([System.Management.Automation.Runspaces.RunspacePool]$pool, [scriptblock]$Cmdlet, [System.Collections.IDictionary]$Parameters, [array]$ArgumentList) { $this.init($Cmdlet, $Parameters, $ArgumentList, $pool) } | |
Asynclet([System.Management.Automation.Runspaces.RunspacePool]$pool, [scriptblock]$Cmdlet, [System.Collections.IDictionary]$Parameters ) { $this.init($Cmdlet, $Parameters, @() , $pool) } | |
Asynclet([System.Management.Automation.Runspaces.RunspacePool]$pool, [scriptblock]$Cmdlet, [array]$ArgumentList) { $this.init($Cmdlet, @{} , $ArgumentList, $pool) } | |
Asynclet([System.Management.Automation.Runspaces.RunspacePool]$pool, [scriptblock]$Cmdlet ) { $this.init($Cmdlet, @{} , @() , $pool) } | |
input([PSObject]$object) { | |
$this.put.Add($object) | |
} | |
input() { | |
$this.put.Complete() | |
} | |
[int] has() { | |
return $this.out.Count | |
} | |
#TODO currently errors only propagate on EndInvoke(), we should throw them on any read() | |
hidden $pull={ | |
if ($EventSubscriber) { | |
$EventSubscriber | Unregister-Event | |
} | |
$this.out.Item[0] | |
$this.out.RemoveAt(0) | |
} | |
[PSObject] read() { | |
return $( | |
if ($this.has()) { | |
.$this.pull | |
} | |
else { | |
Register-ObjectEvent ` | |
-InputObject $this.out ` | |
-EventName DataAdded ` | |
-Action $this.pull ` | |
| Receive-Job | |
} | |
) | |
} | |
[PSObject[]] readAll() { | |
return $this.out.ReadAll() | |
} | |
[PSObject[]] done() { | |
try { | |
if ($this.put.IsOpen) { | |
$this.input() | |
} | |
$this.pwsh.EndInvoke($this.job) | |
return $this.readAll() | |
} | |
finally { | |
$this.put.Dispose() | |
$this.out.Dispose() | |
$this.pwsh.Dispose() | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Function _Clone { | |
Param($Object) | |
Begin { | |
$output=[PSCustomObject]@{} | |
} | |
Process { | |
$output | Add-Member ` | |
-Force ` | |
-MemberType ($_.MemberType -replace '^(?=Property$)','Note') ` | |
-Name $_.Name ` | |
-Value $Object.($_.Name) | |
} | |
End { | |
$output | |
} | |
} | |
Filter Clone { | |
$_ ` | |
| Get-Member -MemberType Properties,ScriptMethod ` | |
| _Clone -Object $_ | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Function Concat { | |
Param ([switch]$Newlines, $Wrap, $Begin='', $End='', $Join='') | |
Begin { | |
if ($Newlines) { | |
$Join=[System.Environment]::NewLine | |
} | |
$output=[System.Text.StringBuilder]::new() | |
$deliniate=$False | |
if (!$Wrap) { | |
$output.Append($Begin) | Out-Null | |
} | |
elseif ($Wrap -is [string]) { | |
$output.Append(($End=$Wrap)) | Out-Null | |
} | |
else { | |
$output.Append($Wrap[0]) | Out-Null | |
$End=$Wrap[1] | |
} | |
} | |
Process { | |
if (!($_=[string]$_).length) { | |
} | |
elseif ($deliniate) { | |
$output.Append($deliniate) | Out-Null | |
$output.Append($_) | Out-Null | |
} | |
else { | |
$deliniate=$Join | |
$output.Append($_) | Out-Null | |
} | |
} | |
End { | |
$output.Append($End).ToString() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Filter AbsolutePath { | |
($_ | Get-Item -Force).FullName | |
} | |
Function Ancestors { Param([switch]$File) | |
#this needs to be a function for grep.ps1 who steals this as a scriptblock | |
Process { | |
$dir = $_ | Get-Item -Force | |
if ($dir -is [System.IO.FileInfo]) { | |
if ($File) { | |
$dir.FullName | |
} | |
$dir = $dir.Directory | |
} | |
while ($dir) { | |
#terminating slashes are only included in `.FullName` if `gi` was called with one or it is a root | |
#let's just make this consistent.. | |
$dir.FullName -replace '(?<=[^\\/])$',[IO.Path]::DirectorySeparatorChar | |
$dir = $dir.Parent | |
} | |
} } | |
#A much better alternative to `Resolve-Path -Relative`, the following problems with that are fixed here: | |
# - returns broken paths if input is not on the same resource | |
# - returns broken paths if $PWD or input is a UNC path (even if it's the same resource) | |
# - if you want to resolve from a location other than $PWD you cannot | |
#On top of those improvements, this differs from other third-party algorithms online in that this actually uses the filesystem not interpreted strings, | |
#this means that it intrinsically follows any rules of hierarchy and doesn't need to be aware at all of any syntax rules. | |
#However, this does mean that the items need to exist on a mount; hypothetical paths wont resolve. | |
# | |
#You can pass in a result from `Ancestors` directly into `-From` if you're calling this function outside of a pipe repeatedly | |
Function RelativePath { Param( | |
[Parameter(ValueFromPipeline)]$Input, | |
[Parameter(Position=0)]$From='.' | |
) | |
Begin { | |
if ($From -isnot [array]) { | |
$From = @($From | Ancestors) | |
} | |
} | |
Process { | |
$To = $_ | |
if ($To -isnot [array]) { | |
$To = @($To | Ancestors -File) | |
} | |
$toIndex = $To.Count - $From.Count | |
$fromIndex = 0 | |
if ($toIndex -lt 0) { | |
$fromIndex = -$toIndex | |
$toIndex = 0 | |
} | |
while ($True) { | |
if ($fromIndex -ge $From.Count) { | |
return $To[0] | |
} | |
if ($From[$fromIndex] -eq $To[$toIndex]) { | |
break | |
} | |
++$fromIndex | |
++$toIndex | |
} | |
$output = '' | |
while (--$fromIndex -ge 0) { | |
$output += '..' + [IO.Path]::DirectorySeparatorChar | |
} | |
if (!$output) { | |
$output = '.' + [IO.Path]::DirectorySeparatorChar | |
} | |
$output + $To[0].SubString($To[$toIndex].Length) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$encoding = [System.Text.UTF8Encoding]::new($False) | |
Filter Gunzip { | |
$stream = $NULL | |
$gzip = $NULL | |
try { | |
$stream = [System.IO.FileStream]::new( | |
$_.FullName, | |
[IO.FileMode]::Open, | |
[IO.FileAccess]::Read, | |
[IO.FileShare]::Read | |
) | |
$gzip = [System.IO.Compression.GZipStream]::new( | |
$stream, | |
[System.IO.Compression.CompressionMode]::Decompress | |
) | |
[System.IO.StreamReader]::new($gzip, $encoding).ReadToEnd() | |
} | |
finally { | |
$gzip.Dispose() | |
$stream.Dispose() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Function Html { | |
Begin { | |
'<p> </p>' | |
Function Safe { | |
Param ($Text) | |
return ( | |
$Text -split '\r?\n' ` | |
| %{ [System.Net.WebUtility]::HtmlEncode($_) } | |
) -join '<br>' | |
} | |
Function Row { | |
Param ($Data, $Url) | |
Begin { | |
'<tr>' | |
} | |
Process { | |
@('<td {0}>','<th {0}>')[$row -eq $NULL] ` | |
-f @('','style="background-color:#f9fafb"')[!$row] | |
$content=@($Data.($_.Name),$_.Name)[$row -eq $NULL] | |
if ($_.Name -eq 'address' -and $Url) { | |
'<a href="' | |
Safe -Text $Url | |
'">' | |
Safe -Text $content | |
'</a>' | |
} | |
else { | |
Safe -Text $content | |
} | |
@('</td>','</th>')[$row -eq $NULL] | |
} | |
End { | |
'</tr>' | |
} | |
} | |
$row=$NULL | |
'<table cellspacing="5" cellpadding="5">' | |
} | |
Process { | |
if ($row -eq $NULL) { | |
$_.PsObject.Properties | Row -Data $_ | |
} | |
$row=!$row | |
$_.PsObject.Properties | Row -Data $_ -Url $_._.url | |
} | |
End { | |
'</table>' | |
'<p> </p>' | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#dot-source this script with a reference to a function defining your variables that returns any wanted unconsumed arguments | |
#if this script returns nothing, you must error code 1 | |
#eg `if (!($args = . ./PermitArrays.ps1 (Get-Command Params) $args)) { Exit 1 }` | |
#The parameters as declared in Params will now be defined in your scope, and a correctly populated args is returned. | |
#Permits multiple identical flags and respects end-of-parameters '--' | |
# NB for whoever is calling your code | |
# within powershell, '--' must be '`--' | |
# outside powershell, '-switchParam:$True' must be '-switchParam=true' ([bool] params may remain '-param:false') | |
Param($_command, $_arguments) | |
try { | |
$_params = @{''=[System.Collections.ArrayList]@()} | |
$_types = @{} | |
$_command.Parameters.Values ` | |
| %{ | |
$_checking = $_ | |
if ($_checking.Aliases -in 'vb','db','ea','wa','infa','ev','wv','iv','ov','ob','pv') { | |
return | |
} | |
@($_checking.Name) + $_checking.Aliases ` | |
| %{ | |
if ($_checking.SwitchParameter) { | |
$_checking = 'switch' | |
} | |
elseif ($_checking.ParameterType -eq [bool]) { | |
$_checking = 'bool' | |
} | |
else { | |
$_checking = '' | |
} | |
$_types[$_] = $_checking | |
} | |
} | |
$_arguments = [System.Collections.Queue]$_arguments | |
for ($_arg = $NULL; $_arguments.Count;) { | |
$_checking = $_arguments.Dequeue() | |
$_keep = $False | |
if ('--' -eq $_checking) { | |
break | |
} | |
elseif ( | |
$_checking -match '^--?' -and | |
$_types.ContainsKey(($_checking -replace '^--?|(:|=.*)$','')) | |
) { | |
$_arg = ($_checking -replace '^--?|(:|=.*)$') | |
#when calling from bash, '-x:False' is preprocessed by pwsh and made ambiguous with '-x $False' | |
#so I'm introducing '-x=False' | |
if ($_checking -match '=') { | |
$_checking = $_checking -replace '^[^=]*=','' | |
if ( | |
($_types[$_arg] -in 'bool','switch') -and | |
($_checking -in 'True','False','1','0') | |
) { | |
$_checking = $_checking -in 'True','1' | |
} | |
} | |
elseif ( | |
($_types[$_arg] -eq 'switch') -and | |
($_checking -notmatch ':$') | |
) { | |
$_checking = $True | |
} | |
else { | |
$_keep = $True | |
$_checking = $NULL | |
} | |
if ($_params.ContainsKey($_arg)) { | |
#Issue if calling from pwsh and passing objects that have Count defined, such as trying to create arrays of arrays. | |
#Remember I wanted 'multiple arguments', even if that ends up being represented as an array, I'm not implementing 'array passing'. | |
#I've added support to calling from pwsh as much as I can, but I'm not going to support mixing multiple AND array arguments on the same parameters. | |
if ($_params[$_arg].Count -lt 2) { | |
$_params[$_arg] = @($_params[$_arg]) | |
} | |
$_params[$_arg] += ,$_checking | |
} | |
else { | |
$_params[$_arg] = $_checking | |
} | |
} | |
elseif ($_arg) { | |
if ( | |
($_types[$_arg] -eq 'switch') -and | |
($_checking -in 'True','False') | |
) { | |
$_checking = $_checking -match 'True' | |
} | |
if ($_params[$_arg].Count -lt 2) { | |
$_params[$_arg] = $_checking | |
} | |
else { | |
$_params[$_arg][-1] = $_checking | |
} | |
} | |
else { | |
$_params[''].Add($_checking) >$NULL | |
} | |
if (!$_keep) { | |
$_arg = $NULL | |
} | |
} | |
$_keep = $_params[''] | |
$_params.Remove('') | |
$_keep = . $_command @_params @_keep | |
return ,@($_keep + $_arguments) | |
} | |
#mimic the real ParameterArgumentTransformationError message | |
catch { | |
#Write-Error doesn't listen to ConciseView | |
if ($ErrorView -eq 'ConciseView') { | |
[Console]::ForegroundColor = 'red' | |
[Console]::Error.WriteLine("$((Get-PSCallStack)[1].Command): $($_.Exception.Message)") | |
[Console]::ResetColor() | |
} | |
else { | |
Write-Error ` | |
-Message $_.Exception.Message ` | |
-Category $_.CategoryInfo.Category ` | |
-ErrorId $_.Exception.ErrorId | |
#none of the other options seem to be able to replace the leading "./Params.ps1 : " | |
} | |
} | |
finally { | |
#on top of the attempted non-clobbering with underscoring, actually delete the variables too | |
Remove-Variable _arguments,_command,_params,_checking,_types,_arg,_keep | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#List of all colors available for PowerShell? - Stack Overflow | |
#https://stackoverflow.com/questions/20541456/list-of-all-colors-available-for-powershell/60684606#60684606 | |
. "$PSScriptRoot/unicode.ps1" | |
#console colours (VT escape sequences) | |
$style=@( | |
#ones in double quotes dont work in Windows terminals | |
0, 'default', 'heavy', "soft", 'italic', 'underline', "blink", "rapid", 'negative', "conceal", "strike", | |
21, "double", 'noweight', 'noitalic', 'nounderline', 'noblink', | |
27, 'nonegative', 'noconceal', 'nostrike', | |
"_font", | |
10, 'default', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'fractur', | |
'_fg', | |
30, 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', | |
39, 'default', | |
'_bg', | |
40, 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', | |
49, 'default', | |
'_bf', | |
90, 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', | |
'_bb', | |
100, 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', | |
"_mark", | |
51, 'frame', 'circle', 'line', 'default', 'noline', | |
"_gram", | |
60, 'ur', 'dur', 'ol', 'dol', 'stress', 'default' | |
) ` | |
| &{ | |
Begin { | |
$sequence=27,'[{0}m' | Unicode | |
$style=@{ | |
fg=@{ | |
rgb=$sequence -f '38;2;{0};{1};{2}' | |
x=$sequence -f '38;5;{0}' | |
}; | |
bg=@{ | |
rgb=$sequence -f '48;2;{0};{1};{2}' | |
x=$sequence -f '48;5;{0}' | |
}; | |
} | |
$current=$style | |
$index=$NULL | |
} | |
Process { | |
Switch -regex ($_) { | |
'^\d' { $index=$_ } | |
'^_' { | |
$_=$_ -replace '^.','' | |
if (!$style[$_]) { | |
$current=$style[$_]=@{} | |
} | |
$current=$style[$_] | |
} | |
Default { | |
$current[$_]=$sequence -f $index++ | |
} | |
} | |
} | |
End { | |
$style | |
} | |
} | |
#writes hyperlinks; although it only works on Mac/Gnome terminals | |
Function Hyperlink { Param($Link, $Text) | |
27,']8;;',$Link,7,$Text,27,']8;;',7 | Unicode | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#takes in a stream of strings and integers, | |
#where integers are unicode codepoints, | |
#and concatenates these into valid UTF16 | |
Function Unicode { | |
Begin { | |
$output=[System.Text.StringBuilder]::new() | |
} | |
Process { | |
$output.Append($( | |
if ($_ -is [string]) { | |
[string]$_ | |
} | |
elseif ($_ -lt 256) { | |
[char]$_ | |
} | |
else { | |
[char]::ConvertFromUtf32($_) | |
} | |
)) ` | |
| Out-Null | |
} | |
End { $output.ToString() } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
. "$PSScriptRoot/unicode.ps1" | |
#Writes titles | |
$_TITLE=(Get-Process -Id $PID).MainWindowTitle | |
Function Title { Param([parameter(Position=0)][string]$text) | |
$script:_TITLE=$text -replace '^(.{251}).*$','$1...' | |
27,']0;',$_TITLE,7 | Unicode | Write-Host -NoNewline | |
} | |
Function BringToFront { | |
(New-Object -ComObject WScript.Shell).AppActivate($args[0]) | Out-Null | |
} | |
Function ResetScreen { | |
Param ( | |
[switch]$Before, | |
[switch]$After, | |
[switch]$Wipe, | |
[switch]$During, | |
[parameter(ValueFromPipeline=$True)]$x | |
) | |
Begin { | |
Function Perform { | |
if ($Wipe) { | |
Clear-Host | |
return | |
} | |
try { | |
[System.Console]::SetWindowPosition(0, [System.Console]::CursorTop) | |
} | |
catch {} | |
} | |
if (!($Before -or $During -or $After)) { | |
$Before=$During=$After=$True | |
} | |
if ($Before) { | |
Perform | |
} | |
} | |
Process { | |
if ($During) { | |
Perform | |
} | |
$x | |
} | |
End { | |
if ($After) { | |
Perform | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#Is there a way to wordwrap results of a Powershell cmdlet? - Stack Overflow | |
#https://stackoverflow.com/questions/1059663/is-there-a-way-to-wordwrap-results-of-a-powershell-cmdlet/60896762#60896762 | |
. "$PSScriptRoot/concat.ps1" | |
$_WRAP=@{''="`$1$([System.Environment]::NewLine)"} | |
Function _Wrap { | |
Param ($Length, $Step, $Force) | |
$wrap=$Force -join '' -replace '\\|]|-','\$0' | |
$chars="^\n\r$wrap" | |
$preExtra="[$chars\S]*" | |
$postExtra="[^\s$wrap]" | |
$chars="[$chars]" | |
$postChars="$preExtra$postExtra" | |
if ($wrap) { | |
$wrap="[$wrap]" | |
$wrap | |
$wrap="$wrap(?=\S)" | |
$chars="$chars|$wrap" | |
$postChars="$postChars|$preExtra$wrap" | |
} | |
for ( | |
($extra=0),($next=$NULL),($prev=$NULL); | |
($next=$Length - $Step) -gt 0 -and ($prev=$extra + $Step); | |
($Length=$next),($extra=$prev) | |
) { | |
"(?:$chars){$next,$Length}(?=(?:$postChars){$extra,$prev})" | |
} | |
} | |
Function Wrap { | |
Param ( | |
[int]$Length=80, | |
[int]$Step=5, | |
[char[]]$Force, | |
[parameter(Position=0)][string]$Text | |
) | |
$key="$Length $Step $Force" | |
$wrap=$_WRAP[$key] | |
if (!$wrap) { | |
$wrap=$_WRAP[$key]=_Wrap ` | |
-Length $Length ` | |
-Step $Step ` | |
-Force ($Force -join '') ` | |
| Concat -Join '|' -Wrap '(',')(?:[^\n\r\S])+' | |
} | |
return $Text -replace $wrap,$_WRAP[''] | |
} |
Author
Hashbrown777
commented
Jul 21, 2023
•
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment