completers/ipconfig_completer/ipconfig_completer.ps1
|
# ipconfig tab completion for PowerShell # Builds a help-driven switch catalog and adapter-name cache for ipconfig. Set-StrictMode -Version 2.0 if (-not (Get-Variable -Name IpconfigCompletionCatalog -Scope Script -ErrorAction Ignore)) { $script:IpconfigCompletionCatalog = @{ Initialized = $false Switches = @() SwitchInfo = @{} AdapterValueOptions = @('/renew', '/release', '/renew6', '/release6', '/showclassid', '/setclassid', '/showclassid6', '/setclassid6') FreeFormClassIdOptions = @('/setclassid', '/setclassid6') AdapterCache = @() AdapterCacheUpdated = $null AdapterCacheTtlSeconds = 300 } } function New-IpconfigCompletionResult { param( [string]$CompletionText, [string]$ResultType, [string]$ToolTip ) if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $CompletionText } [System.Management.Automation.CompletionResult]::new( $CompletionText, $CompletionText, $ResultType, $ToolTip ) } function ConvertTo-IpconfigQuotedValue { param( [string]$Value, [bool]$AlwaysQuote = $false ) if ([string]::IsNullOrWhiteSpace($Value)) { return $Value } if (($AlwaysQuote -or $Value -match '\s') -and -not ($Value.StartsWith('"') -and $Value.EndsWith('"'))) { $escaped = $Value.Replace('`', '``').Replace('"', '`"') return '"' + $escaped + '"' } $Value } function Remove-IpconfigOuterQuotes { param([string]$Value) if ([string]::IsNullOrEmpty($Value)) { return '' } if ($Value.Length -ge 2 -and $Value.StartsWith('"') -and $Value.EndsWith('"')) { return $Value.Substring(1, $Value.Length - 2) } $Value.TrimStart('"') } function Test-IpconfigCommandAvailable { [bool](Get-Command -Name ipconfig.exe -ErrorAction Ignore) } function Invoke-IpconfigHelpText { if (-not (Test-IpconfigCommandAvailable)) { return @() } try { @(& ipconfig.exe '/?' 2>$null) } catch { @() } } function Get-IpconfigCurrentToken { param( [string]$Line, [int]$CursorPosition, [string]$Fallback ) if ([string]::IsNullOrWhiteSpace($Line)) { return $Fallback } $safeCursor = [Math]::Min([Math]::Max($CursorPosition, 0), $Line.Length) $prefix = $Line.Substring(0, $safeCursor) if (([regex]::Matches($prefix, '"').Count % 2) -eq 1) { # Inside an unterminated quote: the token runs from that quote to the cursor. return $prefix.Substring($prefix.LastIndexOf('"')) } if ($prefix -match '\s$') { return '' } $parts = @([regex]::Matches($prefix, '"[^"]*"|\S+') | ForEach-Object { $_.Value }) if ($parts.Count -gt 0) { return $parts[-1] } $Fallback } function Get-IpconfigFallbackSwitchInfo { @{ '/?' = 'Display this help message.' '/allcompartments' = 'Display configuration information for all network compartments.' '/all' = 'Display full configuration information.' '/renew' = 'Renew the IPv4 address for the specified adapter.' '/release' = 'Release the IPv4 address for the specified adapter.' '/renew6' = 'Renew the IPv6 address for the specified adapter.' '/release6' = 'Release the IPv6 address for the specified adapter.' '/flushdns' = 'Purges the DNS Resolver cache.' '/displaydns' = 'Display the contents of the DNS Resolver Cache.' '/registerdns' = 'Refresh all DHCP leases and re-register DNS names.' '/showclassid' = 'Displays all the DHCP class IDs allowed for the adapter.' '/setclassid' = 'Modifies the DHCP class ID.' '/showclassid6' = 'Displays all the IPv6 DHCP class IDs allowed for the adapter.' '/setclassid6' = 'Modifies the IPv6 DHCP class ID.' } } function Initialize-IpconfigCompletionCatalog { if ($script:IpconfigCompletionCatalog.Initialized) { return } $switchInfo = [ordered]@{} $helpLines = Invoke-IpconfigHelpText if ($helpLines -and $helpLines.Count -gt 0) { $inOptions = $false foreach ($line in $helpLines) { if ($line -match '^\s*Options:\s*$') { $inOptions = $true continue } if (-not $inOptions) { continue } if ($line -match '^\s*Examples:\s*$') { break } if ($line -match '^\s*(/[A-Za-z0-9?]+)\s{2,}(.+?)\s*$') { $switchInfo[$matches[1].ToLowerInvariant()] = $matches[2].Trim() } } $helpText = $helpLines -join [Environment]::NewLine foreach ($match in [regex]::Matches($helpText, '(?<!\w)/(?:\?|[A-Za-z][A-Za-z0-9]*)')) { $token = $match.Value.ToLowerInvariant() if (-not $switchInfo.Contains($token)) { $switchInfo[$token] = $match.Value } } } foreach ($entry in (Get-IpconfigFallbackSwitchInfo).GetEnumerator()) { $key = $entry.Key.ToLowerInvariant() if (-not $switchInfo.Contains($key) -or [string]::IsNullOrWhiteSpace($switchInfo[$key]) -or $switchInfo[$key].Equals($key, [System.StringComparison]::OrdinalIgnoreCase)) { $switchInfo[$key] = $entry.Value } } $script:IpconfigCompletionCatalog.SwitchInfo = @{} foreach ($entry in $switchInfo.GetEnumerator()) { $script:IpconfigCompletionCatalog.SwitchInfo[$entry.Key] = $entry.Value } $script:IpconfigCompletionCatalog.Switches = @( $script:IpconfigCompletionCatalog.SwitchInfo.Keys | Sort-Object ) $script:IpconfigCompletionCatalog.Initialized = $true } function Get-IpconfigAdapterNames { $now = Get-Date if ($null -ne $script:IpconfigCompletionCatalog.AdapterCacheUpdated -and $now -lt $script:IpconfigCompletionCatalog.AdapterCacheUpdated.AddSeconds($script:IpconfigCompletionCatalog.AdapterCacheTtlSeconds)) { return @($script:IpconfigCompletionCatalog.AdapterCache) } $adapterNames = New-Object System.Collections.Generic.List[string] # The .NET interface list needs no module load (Get-NetAdapter costs over a # second cold). It also lists the loopback and every lightweight filter # binding ('<adapter>-QoS Packet Scheduler-0000'); Get-NetAdapter # -IncludeHidden lists neither, so both are dropped. try { $interfaces = @([System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() | Where-Object { $_.NetworkInterfaceType -ne [System.Net.NetworkInformation.NetworkInterfaceType]::Loopback -and -not [string]::IsNullOrWhiteSpace($_.Name) } | ForEach-Object { $_.Name }) } catch { $interfaces = @() } foreach ($name in $interfaces) { $isFilterBinding = $false if ($name -match '-\d{4}$') { foreach ($baseName in $interfaces) { if ($name.StartsWith($baseName + '-', [System.StringComparison]::Ordinal)) { $isFilterBinding = $true break } } } if (-not $isFilterBinding) { $adapterNames.Add($name) } } if ($adapterNames.Count -eq 0 -and (Get-Command -Name Get-NetAdapter -ErrorAction Ignore)) { try { # ipconfig addresses adapters by connection name, and it prints and # accepts the hidden tunnel and pseudo-interface names too. foreach ($name in (Get-NetAdapter -IncludeHidden -ErrorAction Stop | Select-Object -ExpandProperty Name)) { if (-not [string]::IsNullOrWhiteSpace($name)) { $adapterNames.Add($name) } } } catch { } } if ($adapterNames.Count -eq 0 -and (Test-IpconfigCommandAvailable)) { try { $output = @(& ipconfig.exe 2>$null) $text = $output -join [Environment]::NewLine foreach ($match in [regex]::Matches($text, '(?im)^[^\r\n:]* adapter (.+?):\s*$')) { $name = $match.Groups[1].Value.Trim() if (-not [string]::IsNullOrWhiteSpace($name)) { $adapterNames.Add($name) } } } catch { } } $script:IpconfigCompletionCatalog.AdapterCache = @($adapterNames.ToArray() | Sort-Object -Unique) $script:IpconfigCompletionCatalog.AdapterCacheUpdated = $now @($script:IpconfigCompletionCatalog.AdapterCache) } function Get-IpconfigValueContext { param([string[]]$TokensBeforeCurrent) $switchIndexes = New-Object System.Collections.Generic.List[int] for ($i = 0; $i -lt $TokensBeforeCurrent.Count; $i++) { if ($TokensBeforeCurrent[$i].StartsWith('/')) { $switchIndexes.Add($i) } } if ($switchIndexes.Count -eq 0) { return $null } $lastSwitchIndex = $switchIndexes[$switchIndexes.Count - 1] $switchToken = $TokensBeforeCurrent[$lastSwitchIndex].ToLowerInvariant() $values = @() if ($lastSwitchIndex -lt ($TokensBeforeCurrent.Count - 1)) { $values = @($TokensBeforeCurrent[($lastSwitchIndex + 1)..($TokensBeforeCurrent.Count - 1)]) } switch ($switchToken) { '/renew' { if ($values.Count -eq 0) { return 'adapter' } } '/release' { if ($values.Count -eq 0) { return 'adapter' } } '/renew6' { if ($values.Count -eq 0) { return 'adapter' } } '/release6' { if ($values.Count -eq 0) { return 'adapter' } } '/showclassid' { if ($values.Count -eq 0) { return 'adapter' } } '/showclassid6' { if ($values.Count -eq 0) { return 'adapter' } } '/setclassid' { if ($values.Count -eq 0) { return 'adapter' } if ($values.Count -eq 1) { return 'classid' } } '/setclassid6' { if ($values.Count -eq 0) { return 'adapter' } if ($values.Count -eq 1) { return 'classid' } } } $null } function Get-IpconfigAdapterCompletions { param([string]$CurrentWord) $adapterNames = Get-IpconfigAdapterNames $typedValue = Remove-IpconfigOuterQuotes -Value $CurrentWord $alwaysQuote = -not [string]::IsNullOrEmpty($CurrentWord) -and $CurrentWord.StartsWith('"') $suggestions = New-Object System.Collections.Generic.List[object] # ipconfig accepts * and ? in the adapter operand ('/renew EL*'), so a typed # wildcard matches adapter names as a pattern and is itself a valid value. $hasWildcard = $typedValue.IndexOfAny([char[]]@('*', '?')) -ge 0 if ($hasWildcard) { $matched = New-Object System.Collections.Generic.List[string] foreach ($name in $adapterNames) { if ($name -like $typedValue) { $matched.Add($name) } } $suggestions.Add((New-IpconfigCompletionResult ` -CompletionText (ConvertTo-IpconfigQuotedValue -Value $typedValue -AlwaysQuote:$alwaysQuote) ` -ResultType 'ParameterValue' ` -ToolTip ('Wildcard adapter selector matching ' + $matched.Count + ' adapter(s).'))) foreach ($name in $matched) { $suggestions.Add((New-IpconfigCompletionResult ` -CompletionText (ConvertTo-IpconfigQuotedValue -Value $name -AlwaysQuote:$alwaysQuote) ` -ResultType 'ParameterValue' ` -ToolTip 'Adapter name.')) } return @($suggestions.ToArray()) } $candidates = @('*') + $adapterNames foreach ($candidate in ($candidates | Sort-Object -Unique)) { if (-not [string]::IsNullOrWhiteSpace($typedValue) -and -not $candidate.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) { continue } $toolTip = if ($candidate -eq '*') { 'Wildcard adapter selector.' } else { 'Adapter name.' } $suggestions.Add((New-IpconfigCompletionResult ` -CompletionText (ConvertTo-IpconfigQuotedValue -Value $candidate -AlwaysQuote:$alwaysQuote) ` -ResultType 'ParameterValue' ` -ToolTip $toolTip)) } @($suggestions.ToArray()) } function Get-IpconfigSwitchCompletions { param( [string]$CurrentWord, [string[]]$UsedSwitches ) $switches = @( $script:IpconfigCompletionCatalog.Switches | Where-Object { $UsedSwitches -notcontains $_ } ) if (-not [string]::IsNullOrWhiteSpace($CurrentWord)) { $prefix = $CurrentWord.ToLowerInvariant() $switches = @($switches | Where-Object { $_.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase) }) } $switches | ForEach-Object { $toolTip = $script:IpconfigCompletionCatalog.SwitchInfo[$_.ToLowerInvariant()] New-IpconfigCompletionResult -CompletionText $_ -ResultType 'ParameterName' -ToolTip $toolTip } } function Complete-Ipconfig { param( [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [int]$cursorPosition ) Initialize-IpconfigCompletionCatalog # Tokens are split at the cursor rather than at the end of the line, so a # cursor inside an earlier token completes that token and ignores the rest. $line = $commandAst.ToString() $currentWord = if ($cursorPosition -gt $commandAst.Extent.EndOffset) { '' } else { Get-IpconfigCurrentToken -Line $line -CursorPosition ($cursorPosition - $commandAst.Extent.StartOffset) -Fallback $wordToComplete } $tokensBeforeCurrent = @( $commandAst.CommandElements | Select-Object -Skip 1 | Where-Object { $_.Extent.EndOffset -lt $cursorPosition } | ForEach-Object { $_.Extent.Text } ) $valueContext = Get-IpconfigValueContext -TokensBeforeCurrent $tokensBeforeCurrent if ($valueContext -eq 'adapter') { return @(Get-IpconfigAdapterCompletions -CurrentWord $currentWord) } if ($valueContext -eq 'classid') { if ([string]::IsNullOrEmpty($currentWord)) { return @(New-IpconfigCompletionResult -CompletionText '<class-id>' -ResultType 'ParameterValue' -ToolTip 'DHCP class ID value; omit it to clear the class ID.') } return @(New-IpconfigCompletionResult -CompletionText $currentWord -ResultType 'ParameterValue' -ToolTip 'Class ID value.') } $usedSwitches = @( $tokensBeforeCurrent | Where-Object { $_.StartsWith('/') } | ForEach-Object { $_.ToLowerInvariant() } | Sort-Object -Unique ) $terminalSwitches = @($usedSwitches | Where-Object { $_ -ne '/allcompartments' }) if ($terminalSwitches.Count -gt 0) { return @() } if ([string]::IsNullOrWhiteSpace($currentWord) -or $currentWord.StartsWith('/')) { return @(Get-IpconfigSwitchCompletions -CurrentWord $currentWord -UsedSwitches $usedSwitches) } @() } Register-ArgumentCompleter -Native -CommandName 'ipconfig', 'ipconfig.exe' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-Ipconfig -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } |