Skip to content

Instantly share code, notes, and snippets.

@Hashbrown777
Last active October 16, 2023 16:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Hashbrown777/a5a02e2fd3eeed4485d4ba073ef3b143 to your computer and use it in GitHub Desktop.
Save Hashbrown777/a5a02e2fd3eeed4485d4ba073ef3b143 to your computer and use it in GitHub Desktop.
Useful utils for pwsh
#just to name the gist as per https://stackoverflow.com/a/19904644/2518317
#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()
}
}
#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()
}
}
}
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 $_
}
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()
}
}
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)
}
}
$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()
}
}
Function Html {
Begin {
'<p>&nbsp;</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>&nbsp;</p>'
}
}
#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
}
#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
}
#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() }
}
. "$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
}
}
}
#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['']
}
@Hashbrown777
Copy link
Author

Hashbrown777 commented Jul 21, 2023

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment