completers/compact_completer/compact_completer.ps1
|
<# .SYNOPSIS Registers a native PowerShell argument completer for compact. .DESCRIPTION Provides a static-first native completer for `compact` and `compact.exe`. The completer covers: - slash-style switch completion - attached value switches such as `/S:`, `/EXE:`, `/CompactOs:`, and `/WinDir:` - local file and directory completion for compact operands - placeholder-safe enum completion for algorithm and CompactOS value slots The script keeps its top level compatible with `Import-CompleterScript`. #> Set-StrictMode -Version 2.0 function New-CompactCompletionResult { param( [string]$CompletionText, [string]$ResultType, [string]$ToolTip, [string]$ListItemText ) if ([string]::IsNullOrWhiteSpace($ListItemText)) { $ListItemText = $CompletionText } if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $ListItemText } [System.Management.Automation.CompletionResult]::new( $CompletionText, $ListItemText, $ResultType, $ToolTip ) } function Remove-CompactOuterQuotes { param([string]$Value) if ([string]::IsNullOrEmpty($Value)) { return '' } $Value.Trim([char[]]@([char]34, [char]39)) } function ConvertTo-CompactQuotedValue { 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 Get-CompactTokenState { param( [string]$Line, [int]$CursorPosition ) if ($null -eq $Line) { $Line = '' } $safeCursor = [Math]::Min([Math]::Max($CursorPosition, 0), $Line.Length) $prefix = $Line.Substring(0, $safeCursor) $tokens = New-Object System.Collections.Generic.List[string] $builder = New-Object System.Text.StringBuilder $quoteChar = [char]0 foreach ($character in $prefix.ToCharArray()) { if (($character -eq [char]34) -or ($character -eq [char]39)) { if ($quoteChar -eq [char]0) { $quoteChar = $character } elseif ($quoteChar -eq $character) { $quoteChar = [char]0 } [void]$builder.Append($character) continue } if ([char]::IsWhiteSpace($character) -and $quoteChar -eq [char]0) { if ($builder.Length -gt 0) { $tokens.Add($builder.ToString()) [void]$builder.Clear() } continue } [void]$builder.Append($character) } $hasTrailingSpace = $prefix -match '\s$' if ($builder.Length -gt 0) { $tokens.Add($builder.ToString()) } if ($hasTrailingSpace) { return [pscustomobject]@{ TokensBeforeCurrent = @($tokens) CurrentToken = '' } } if ($tokens.Count -gt 0) { return [pscustomobject]@{ TokensBeforeCurrent = @($tokens | Select-Object -First ($tokens.Count - 1)) CurrentToken = $tokens[$tokens.Count - 1] } } [pscustomobject]@{ TokensBeforeCurrent = @() CurrentToken = '' } } function Get-CompactArgumentsFromTokenState { param([pscustomobject]$TokenState) $tokensBeforeCurrent = @($TokenState.TokensBeforeCurrent) $currentArgument = if ($null -eq $TokenState.CurrentToken) { '' } else { $TokenState.CurrentToken } if ($tokensBeforeCurrent.Count -gt 0) { $argumentsBeforeCurrent = @($tokensBeforeCurrent | Select-Object -Skip 1) } else { $argumentsBeforeCurrent = @() } if ($tokensBeforeCurrent.Count -eq 0 -and $currentArgument -match '^(?i)compact(?:\.exe)?$') { $currentArgument = '' } [pscustomobject]@{ ArgumentsBeforeCurrent = $argumentsBeforeCurrent CurrentArgument = $currentArgument } } function Get-CompactCatalog { if (Get-Variable -Name CompactCompletionCatalog -Scope Script -ErrorAction Ignore) { return $script:CompactCompletionCatalog } $switches = @( [pscustomobject]@{ Token = '/C'; Description = 'Compress the specified files.' } [pscustomobject]@{ Token = '/U'; Description = 'Uncompress the specified files.' } [pscustomobject]@{ Token = '/S'; Description = 'Process the current directory and all subdirectories, or use /S:dir.' } [pscustomobject]@{ Token = '/S:'; Description = 'Process the specified directory and all subdirectories.'; ValueKind = 'DirectoryPath' } [pscustomobject]@{ Token = '/A'; Description = 'Display files with hidden or system attributes.' } [pscustomobject]@{ Token = '/I'; Description = 'Continue even after errors occur.' } [pscustomobject]@{ Token = '/F'; Description = 'Force compression of already-compressed files.' } [pscustomobject]@{ Token = '/Q'; Description = 'Report only essential information.' } [pscustomobject]@{ Token = '/EXE'; Description = 'Use executable-file compression defaults.' } [pscustomobject]@{ Token = '/EXE:'; Description = 'Use compression optimized for executable files.'; ValueKind = 'ExeAlgorithm' } [pscustomobject]@{ Token = '/CompactOs'; Description = 'Set or query the system Compact state.' } [pscustomobject]@{ Token = '/CompactOs:'; Description = 'Set the system Compact state.'; ValueKind = 'CompactOsOption' } [pscustomobject]@{ Token = '/WinDir:'; Description = 'Specify the offline Windows directory when querying CompactOS.'; ValueKind = 'DirectoryPath' } [pscustomobject]@{ Token = '/?'; Description = 'Show compact help.' } ) $lookup = @{} foreach ($switch in $switches) { $lookup[$switch.Token.ToLowerInvariant()] = $switch } $script:CompactCompletionCatalog = [pscustomobject]@{ Switches = $switches SwitchLookup = $lookup ExeAlgorithms = @('XPRESS4K', 'XPRESS8K', 'XPRESS16K', 'LZX') CompactOsOptions = @('query', 'always', 'never') } $script:CompactCompletionCatalog } function Get-CompactAttachedTokenInfo { param([string]$Token) if ([string]::IsNullOrWhiteSpace($Token)) { return $null } $match = [regex]::Match($Token, '^(?i)(?<root>/(?:S|EXE|CompactOs|WinDir)):(?<value>.*)$') if (-not $match.Success) { return $null } $catalog = Get-CompactCatalog $switchKey = ($match.Groups['root'].Value + ':').ToLowerInvariant() if (-not $catalog.SwitchLookup.ContainsKey($switchKey)) { return $null } $switch = $catalog.SwitchLookup[$switchKey] [pscustomobject]@{ Prefix = $switch.Token Value = $match.Groups['value'].Value Switch = $switch } } function Get-CompactSwitchCompletions { param([string]$CurrentWord) foreach ($switch in (Get-CompactCatalog).Switches) { if (-not [string]::IsNullOrWhiteSpace($CurrentWord) -and -not $switch.Token.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) { continue } New-CompactCompletionResult -CompletionText $switch.Token -ResultType 'ParameterName' -ToolTip $switch.Description -ListItemText $switch.Token } } function Get-CompactPrefixedValueCompletions { param( [string]$Prefix, [string]$CurrentValue, [string[]]$Suggestions, [string]$ToolTip, [string]$Placeholder ) $typedValue = Remove-CompactOuterQuotes -Value $CurrentValue $results = New-Object System.Collections.Generic.List[object] foreach ($suggestion in $Suggestions) { if (-not [string]::IsNullOrWhiteSpace($typedValue) -and -not $suggestion.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) { continue } [void]$results.Add((New-CompactCompletionResult -CompletionText ($Prefix + $suggestion) -ResultType 'ParameterValue' -ToolTip $ToolTip -ListItemText ($Prefix + $suggestion))) } if ($results.Count -eq 0) { $fallback = if ([string]::IsNullOrWhiteSpace($CurrentValue)) { $Prefix + $Placeholder } else { $Prefix + $CurrentValue } [void]$results.Add((New-CompactCompletionResult -CompletionText $fallback -ResultType 'ParameterValue' -ToolTip $ToolTip -ListItemText $fallback)) } @($results.ToArray()) } function Get-CompactPathCompletions { param( [string]$CurrentValue, [string]$Prefix, [ValidateSet('File','Directory','Any')] [string]$Kind, [string]$ToolTip, [string]$Placeholder, [switch]$NoPlaceholder ) $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } $cleanValue = Remove-CompactOuterQuotes -Value $typedValue $alwaysQuote = $typedValue.StartsWith('"') $results = New-Object System.Collections.Generic.List[object] $parentPath = '.' $leaf = '' if (-not [string]::IsNullOrWhiteSpace($cleanValue)) { if ($cleanValue.EndsWith('\') -or $cleanValue.EndsWith('/')) { $parentPath = $cleanValue } else { try { $candidateParent = Split-Path -Path $cleanValue -Parent } catch { $candidateParent = '' } if ([string]::IsNullOrWhiteSpace($candidateParent)) { $leaf = $cleanValue } else { $parentPath = $candidateParent try { $leaf = Split-Path -Path $cleanValue -Leaf } catch { $leaf = $cleanValue } } } } try { $items = @(Get-ChildItem -LiteralPath $parentPath -Force -ErrorAction Stop) } catch { $items = @() } foreach ($item in $items) { if ($Kind -eq 'Directory' -and -not $item.PSIsContainer) { continue } if (-not [string]::IsNullOrWhiteSpace($leaf) -and -not $item.Name.StartsWith($leaf, [System.StringComparison]::OrdinalIgnoreCase)) { continue } $candidate = if ($parentPath -eq '.') { $item.Name } else { Join-Path -Path $parentPath -ChildPath $item.Name } if ($item.PSIsContainer -and -not ($candidate.EndsWith('\') -or $candidate.EndsWith('/'))) { $candidate += '\' } $completionText = ConvertTo-CompactQuotedValue -Value $candidate -AlwaysQuote $alwaysQuote [void]$results.Add((New-CompactCompletionResult -CompletionText ($Prefix + $completionText) -ResultType 'ParameterValue' -ToolTip $item.FullName -ListItemText ($Prefix + $completionText))) } if ($results.Count -eq 0 -and -not $NoPlaceholder) { $fallback = if ([string]::IsNullOrWhiteSpace($typedValue)) { $Prefix + $Placeholder } else { $Prefix + $typedValue } [void]$results.Add((New-CompactCompletionResult -CompletionText $fallback -ResultType 'ParameterValue' -ToolTip $ToolTip -ListItemText $fallback)) } @($results.ToArray()) } function Get-CompactTerminalCompletions { param([string]$CurrentWord) $completionText = if ([string]::IsNullOrEmpty($CurrentWord)) { ' ' } else { $CurrentWord } @( New-CompactCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip 'No further arguments are valid after /?.' -ListItemText $completionText ) } function Complete-Compact { param( [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [int]$cursorPosition ) $tokenState = Get-CompactTokenState -Line $commandAst.ToString() -CursorPosition ($cursorPosition - $commandAst.Extent.StartOffset) $argumentState = Get-CompactArgumentsFromTokenState -TokenState $tokenState $hasTrailingSpace = [string]::IsNullOrEmpty($wordToComplete) if ($hasTrailingSpace -and -not [string]::IsNullOrEmpty($argumentState.CurrentArgument)) { $currentWord = '' $argumentsBeforeCurrent = @($argumentState.ArgumentsBeforeCurrent + $argumentState.CurrentArgument) } else { $currentWord = if ($null -eq $argumentState.CurrentArgument) { '' } else { $argumentState.CurrentArgument } $argumentsBeforeCurrent = @($argumentState.ArgumentsBeforeCurrent) } $helpRequested = $argumentsBeforeCurrent -contains '/?' $catalog = Get-CompactCatalog if ($helpRequested) { return @(Get-CompactTerminalCompletions -CurrentWord $currentWord) } if (-not [string]::IsNullOrWhiteSpace($currentWord) -and $currentWord.StartsWith('/')) { $attached = Get-CompactAttachedTokenInfo -Token $currentWord if ($null -ne $attached) { switch ($attached.Switch.ValueKind) { 'ExeAlgorithm' { return @(Get-CompactPrefixedValueCompletions -Prefix $attached.Prefix -CurrentValue $attached.Value -Suggestions $catalog.ExeAlgorithms -ToolTip $attached.Switch.Description -Placeholder '<algorithm>') } 'CompactOsOption' { return @(Get-CompactPrefixedValueCompletions -Prefix $attached.Prefix -CurrentValue $attached.Value -Suggestions $catalog.CompactOsOptions -ToolTip $attached.Switch.Description -Placeholder '<option>') } 'DirectoryPath' { return @(Get-CompactPathCompletions -CurrentValue $attached.Value -Prefix $attached.Prefix -Kind 'Directory' -ToolTip $attached.Switch.Description -Placeholder '<dir>') } } } return @(Get-CompactSwitchCompletions -CurrentWord $currentWord) } if ([string]::IsNullOrWhiteSpace($currentWord)) { return @( Get-CompactSwitchCompletions -CurrentWord $currentWord Get-CompactPathCompletions -CurrentValue '' -Prefix '' -Kind 'Any' -ToolTip 'File or directory pattern.' -Placeholder '<path>' -NoPlaceholder ) } @(Get-CompactPathCompletions -CurrentValue $currentWord -Prefix '' -Kind 'Any' -ToolTip 'File or directory pattern.' -Placeholder '<path>') } Register-ArgumentCompleter -Native -CommandName @('compact', 'compact.exe') -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-Compact -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } |