Completion.ps1
[hashtable]$CacheAllCompletions = [ordered]@{} [hashtable]$CacheCommands = [ordered]@{} $PSVersion = $PSVersionTable.PSVersion $keywords = '#listitemtext', '#type','#tooltip' class CacheCommandData { $Commands = $null [ScriptBlock]$Filter = $null [ScriptBlock]$Sort = $null [ScriptBlock]$Where = $null CacheCommandData ($c, [ScriptBlock]$f, [ScriptBlock]$s, [ScriptBlock]$w) { $this.Commands = $c $this.Filter = $f $this.Sort = $s $this.Where = $w } } <# .SYNOPSIS Convert to hashtable format. .DESCRIPTION Recursive conversion of data to hashtable format, by default, hashtable value is emtpy string, but if this hashtable key in `@('#listitemtext', '#type','#tooltip')`, it will be reserved, because it is used by the construct `System.Management.Automation.CompletionResult` .PARAMETER InputObject Input data, support for basic data types .EXAMPLE ConvertTo-Hash 'hello','world' # output: @{'hello' = ''; 'world' = ''} Convert array to hashtable format .EXAMPLE ConvertTo-Hash 100 # output: @{100 = ''} Convert number to hashtable format .EXAMPLE ConvertTo-Hash '{arg1: "hello", arg2: "world"}' # output: @{arg1 = @{'hello' = ''}; arg2 = @{'world' = ''}} Convert object to hashtable format .EXAMPLE ConvertTo-Hash '[{arg1: {"#tooltip": "arg1 tooltip"}, arg2: {"#tooltip": "arg2 tooltip"}}]' # output: @{arg1 = @{'#tooltip' = 'arg1 tooltip'}; arg2 = @{'#tooltip' = 'arg2 tooltip'}} Convert Javascript object to hashtable format width keywords .INPUTS None. .OUTPUTS System.Collections.Hashtable .LINK https://github.com/aliuq/Register-Completion #> function ConvertTo-Hash { Param($InputObject) if (!$InputObject) { return "" } [hashtable]$hash = [ordered]@{} $inputType = $InputObject.getType() if ($inputType -eq [hashtable]) { $InputObject.Keys | ForEach-Object { if ($_.ToString().ToLower() -in $keywords) { $hash[$_] = $InputObject[$_] } else { $hash[$_] = ConvertTo-Hash $InputObject[$_] } } } elseif ($inputType -eq [Object[]]) { $InputObject | ForEach-Object { $hash += ConvertTo-Hash $_ } } elseif ($inputType -eq [System.Management.Automation.PSCustomObject]) { $InputObject.psobject.Properties | ForEach-Object { if ($_.Name.ToString().ToLower() -in $keywords) { $hash[$_.Name] = $_.Value } else { $hash[$_.Name] = ConvertTo-Hash $_.Value } } } else { try { if ($PSVersion -lt "7.0") { $json = ConvertFrom-Json -InputObject $InputObject } else { $json = ConvertFrom-Json -InputObject $InputObject -AsHashtable } $jsonType = $json.getType() if ($jsonType -in [hashtable],[Object[]],[System.Management.Automation.PSCustomObject]) { $hash = ConvertTo-Hash $json } else { $hash.Add($json, "") } } catch { $hash.Add($InputObject, "") } } return $hash } <# .SYNOPSIS According to input datas, returns avaliable completion object keys. .DESCRIPTION According to input word and data, return the corresponding command keys. it usually used in the cmdlet `Register-ArgumentCompleter`, when provide datasets, it will return the avaliable completion keys. .PARAMETER Word The input word. From `$wordToComplete` .PARAMETER Ast The input data. From `$commandAst` .PARAMETER HashList The datasets, support basic data types. .PARAMETER Filter The filter function. if provided, it will be used to filter and sort the completion object keys. .PARAMETER Where The where function. if provided, it will be used to filter the completion object keys. .PARAMETER Sort The sort function. if provided, it will be used to sort the completion object keys. .EXAMPLE Get-CompletionKeys '' nc 'hello','world' # output: @(@{hello = ''}, @{world = ''}) Returns object array .INPUTS None. .OUTPUTS Object[] .LINK https://github.com/aliuq/Register-Completion #> function Get-CompletionKeys { Param( [string]$Word, $Ast, $HashList, [ScriptBlock]$Filter, [ScriptBlock]$Where, [ScriptBlock]$Sort ) if (!$HashList) { return @() } $arr = $Ast.ToString().Split().ToLower() | Where-Object { $null -ne $_ } # Empty, need to return children completion keys if (!$Word) { [string]$key = ($arr -join ".").trim(".") $keyLevel = $arr } # Character, need to return sibling completion keys else { [string]$key = (($arr | Select-Object -SkipLast 1) -join ".").trim(".") $keyLevel = $key | ForEach-Object { $_.split(".") } } if (!$CacheAllCompletions.ContainsKey($key)) { $map = ConvertTo-Hash $HashList $prefix = "" $keyLevel | ForEach-Object { if ($prefix) { $map = $map[$_] $prefix = "$prefix.$($_)" } else { $prefix = $_ } if (!$CacheAllCompletions.ContainsKey($prefix)) { if ($null -ne $map) { $CacheAllCompletions[$prefix] = $map } else { $CacheAllCompletions[$prefix] = @{} } } } } # Convert HashtableEnumerator to Object[] $keyArrs = $CacheAllCompletions[$key].GetEnumerator() | ForEach-Object { $_ } if ($Filter -is [scriptblock]) { & $Filter $keyArrs $Word } else { if ($Where -is [scriptblock]) { $keyArrs = & $Where $keyArrs $Word } else { $keyArrs = $keyArrs | Where-Object { $_.Key -Like "*$Word*" } } if ($Word) { $keyArrs = $keyArrs | Sort-Object -Property ` @{Expression = { $Word -And $_.Key.ToString().StartsWith($Word) }; Descending = $true }, ` @{Expression = { $Word -And $_.Key.ToString().indexOf($Word) }; Descending = $false } } $keyArrs = $keyArrs | Sort-Object -Property ` @{Expression = { $_.Key.ToString().StartsWith('-') }; Descending = $false }, ` @{Expression = { $_.Key }; Descending = $false } if ($Sort -is [scriptblock]) { $keyArrs = & $Sort $keyArrs } } $keyArrs | Where-Object { $_.Key.ToString().ToLower() -notin $keywords } } <# .SYNOPSIS Remove a completion. .DESCRIPTION According to input command, remove the completion. .PARAMETER Command The command name. use dot to separate the command name. .EXAMPLE Remove-Completion nc .EXAMPLE Remove-Completion 'nc.hello' .INPUTS None. .OUTPUTS None. .LINK https://github.com/aliuq/Register-Completion #> function Remove-Completion { Param([string]$Command) if ($CacheCommands.ContainsKey($Command)) { $CacheCommands.Remove($Command) } if ($CacheCommands.ContainsKey("$Command--filter")) { $CacheCommands.Remove("$Command--filter") } $CacheAllCompletions.Clone().Keys | Where-Object { $_.StartsWith("$Command.") -or ($_ -eq $Command) } | ForEach-Object { $CacheAllCompletions.Remove($_) } } <# .SYNOPSIS Register a completion. .DESCRIPTION Register a completion. provide the command name and the completion datasets. when type the command name, and press `Tab`, it will show the completion keys. .PARAMETER Command The command name. .PARAMETER HashList The datasets, support basic data types. .PARAMETER Force Enable replaced the existing completion. default is false. .PARAMETER Filter The filter function. if provided, it will be used to filter the completion keys. The function will be called with two parameters: $Keys and $Word, and the return value is the filtered and sorted keys. .PARAMETER Where The where function. if provided, it will be used to filter the completion object keys. .PARAMETER Sort The sort function. if provided, it will be used to sort the completion object keys. .EXAMPLE New-Completion demo "hello","world" Register a completion with command name `demo` and datasets `hello`、`world`. Press `demo <Tab>` will get `demo hello` .EXAMPLE New-Completion demo "100" -Force Replace the existing completion with command name `demo` and datasets `100`. Press `demo <Tab>` will get `demo 100` .EXAMPLE $cmds = "{ 'access': ['public', { grant: ['read-only', 'read-write'] }, 'revoke', 'edit', '--help'], '--help': '' }" New-Completion nc $cmds -filter { Param($Keys, $Word) $Keys | Where-Object { $_ -Like "*$Word*" } | Sort-Object -Descending } Replace the default filter function, and will returns the filtered completion keys with provided fitler function. .INPUTS None. .OUTPUTS None. .LINK https://github.com/aliuq/Register-Completion #> function New-Completion { Param( [string]$Command, $HashList, [switch]$Force = $false, [ScriptBlock]$Filter, [ScriptBlock]$Sort, [ScriptBlock]$Where ) if ($CacheCommands.ContainsKey($Command)) { if ($Force) { Remove-Completion $Command } else { return } } $CacheCommands.Add($Command, [CacheCommandData]::new($HashList, $Filter, $Sort, $Where)) Register-ArgumentCompleter -Native -CommandName $Command -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) [Console]::InputEncoding = [Console]::OutputEncoding = $OutputEncoding = [System.Text.Utf8Encoding]::new() $cmd = $commandAst.CommandElements[0].Value $data = $CacheCommands[$cmd] if ($data) { Get-CompletionKeys $wordToComplete $commandAst $data.Commands -Filter $data.Filter -Sort $data.Sort -Where $data.Where | ForEach-Object { $key = $_.Key $value = $_.Value $type = "ParameterValue" $listItemText = $key $tooltip = $key if ($value) { $value.GetEnumerator() | ForEach-Object { $lowerKey = $_.Key.ToString().ToLower() if ($lowerKey -eq '#tooltip') { $tooltip = $_.Value } elseif ($lowerKey -eq '#type') { $type = $_.Value } elseif ($lowerKey -eq '#listitemtext') { $listItemText = $_.Value } } } [System.Management.Automation.CompletionResult]::new($key, $listItemText, $type, $tooltip) } } } } |