completers/robocopy_completer/robocopy_completer.ps1
|
# robocopy tab completion for PowerShell # Builds completion data from robocopy built-in help. Set-StrictMode -Version 2.0 if (-not (Get-Variable -Name RobocopyCompletionCatalog -Scope Script -ErrorAction Ignore)) { $script:RobocopyCompletionCatalog = @{ Initialized = $false Options = @() OptionInfoByKey = @{} MaxPathResults = 300 } } function Invoke-RobocopyHelpText { if (-not (Get-Command -Name robocopy.exe -ErrorAction SilentlyContinue)) { return @() } try { @(& robocopy.exe '/?' 2>$null) } catch { @() } } function Get-RobocopyStaticOptionMetadata { @{ '/copy' = @{ Key = '/copy' Display = '/COPY:copyflag[s]' CompletionText = '/COPY:' Description = 'Specify which file properties to copy.' InlineValueKind = 'Flags' AllowedCharacters = 'DATSOUX' Suggestions = @('DAT', 'DATS', 'DATSOU', 'DATSOUX') } '/dcopy' = @{ Key = '/dcopy' Display = '/DCOPY:copyflag[s]' CompletionText = '/DCOPY:' Description = 'Specify which directory properties to copy.' InlineValueKind = 'Flags' AllowedCharacters = 'DATEX' Suggestions = @('DA', 'DAT', 'DATEX') } '/a+' = @{ Key = '/a+' Display = '/A+:[RASHCNET]' CompletionText = '/A+:' Description = 'Add file attributes to copied files.' InlineValueKind = 'Flags' AllowedCharacters = 'RASHCNET' Suggestions = @('R', 'A', 'H', 'S', 'C', 'N', 'E', 'T') } '/a-' = @{ Key = '/a-' Display = '/A-:[RASHCNETO]' CompletionText = '/A-:' Description = 'Remove file attributes from copied files.' InlineValueKind = 'Flags' AllowedCharacters = 'RASHCNETO' Suggestions = @('R', 'A', 'H', 'S', 'C', 'N', 'E', 'T', 'O') } '/xa' = @{ Key = '/xa' Display = '/XA:[RASHCNETO]' CompletionText = '/XA:' Description = 'Exclude files with the specified attributes.' InlineValueKind = 'Flags' AllowedCharacters = 'RASHCNETO' Suggestions = @('R', 'A', 'H', 'S', 'C', 'N', 'E', 'T', 'O') } '/ia' = @{ Key = '/ia' Display = '/IA:[RASHCNETO]' CompletionText = '/IA:' Description = 'Include only files with the specified attributes.' InlineValueKind = 'Flags' AllowedCharacters = 'RASHCNETO' Suggestions = @('R', 'A', 'H', 'S', 'C', 'N', 'E', 'T', 'O') } '/lev' = @{ Key = '/lev' Display = '/LEV:n' CompletionText = '/LEV:' Description = 'Copy only the top n levels of the source tree.' InlineValueKind = 'List' Suggestions = @('1', '2', '3', '5', '10') } '/mon' = @{ Key = '/mon' Display = '/MON:n' CompletionText = '/MON:' Description = 'Monitor and rerun when more than n changes are seen.' InlineValueKind = 'List' Suggestions = @('1', '5', '10', '50', '100') } '/mot' = @{ Key = '/mot' Display = '/MOT:m' CompletionText = '/MOT:' Description = 'Monitor and rerun in m minutes when changes are detected.' InlineValueKind = 'List' Suggestions = @('1', '5', '10', '15', '30', '60') } '/rh' = @{ Key = '/rh' Display = '/RH:hhmm-hhmm' CompletionText = '/RH:' Description = 'Restrict new copies to the specified run hours.' InlineValueKind = 'List' Suggestions = @('0000-2359', '0800-1700', '0900-1800') } '/ipg' = @{ Key = '/ipg' Display = '/IPG:n' CompletionText = '/IPG:' Description = 'Set the inter-packet gap in milliseconds.' InlineValueKind = 'List' Suggestions = @('0', '1', '10', '50', '100') } '/mt' = @{ Key = '/mt' Display = '/MT[:n]' CompletionText = '/MT' Description = 'Enable multithreaded copies with an optional thread count.' InlineValueKind = 'List' OptionalInlineValue = $true Suggestions = @('4', '8', '16', '32', '64', '128') } '/sparse' = @{ Key = '/sparse' Display = '/SPARSE[:Y/N]' CompletionText = '/SPARSE' Description = 'Control whether sparse file state is retained during copy.' InlineValueKind = 'List' OptionalInlineValue = $true Suggestions = @('Y', 'N') } '/lfsm' = @{ Key = '/lfsm' Display = '/LFSM[:n[kmg]]' CompletionText = '/LFSM' Description = 'Enable low free space mode with an optional free-space floor.' InlineValueKind = 'List' OptionalInlineValue = $true Suggestions = @('100M', '500M', '1G', '5G') } '/iomaxsize' = @{ Key = '/iomaxsize' Display = '/IOMAXSIZE:n[kmg]' CompletionText = '/IOMAXSIZE:' Description = 'Set the maximum I/O size per read or write.' InlineValueKind = 'List' Suggestions = @('1M', '4M', '8M', '16M') } '/iorate' = @{ Key = '/iorate' Display = '/IORATE:n[kmg]' CompletionText = '/IORATE:' Description = 'Set the maximum I/O bandwidth to use.' InlineValueKind = 'List' Suggestions = @('1M', '10M', '100M', '1G') } '/threshold' = @{ Key = '/threshold' Display = '/THRESHOLD:n[kmg]' CompletionText = '/THRESHOLD:' Description = 'Throttle only files larger than the specified threshold.' InlineValueKind = 'List' Suggestions = @('1M', '10M', '100M', '1G') } '/max' = @{ Key = '/max' Display = '/MAX:n' CompletionText = '/MAX:' Description = 'Specify the maximum file size to copy.' InlineValueKind = 'List' Suggestions = @('1024', '1048576', '1073741824') } '/min' = @{ Key = '/min' Display = '/MIN:n' CompletionText = '/MIN:' Description = 'Specify the minimum file size to copy.' InlineValueKind = 'List' Suggestions = @('1', '1024', '1048576') } '/maxage' = @{ Key = '/maxage' Display = '/MAXAGE:n' CompletionText = '/MAXAGE:' Description = 'Specify the maximum file age to copy.' InlineValueKind = 'List' Suggestions = @('1', '7', '30', '365') } '/minage' = @{ Key = '/minage' Display = '/MINAGE:n' CompletionText = '/MINAGE:' Description = 'Specify the minimum file age to copy.' InlineValueKind = 'List' Suggestions = @('1', '7', '30', '365') } '/maxlad' = @{ Key = '/maxlad' Display = '/MAXLAD:n' CompletionText = '/MAXLAD:' Description = 'Specify the maximum last-access date to copy.' InlineValueKind = 'List' Suggestions = @('1', '7', '30', '365') } '/minlad' = @{ Key = '/minlad' Display = '/MINLAD:n' CompletionText = '/MINLAD:' Description = 'Specify the minimum last-access date to copy.' InlineValueKind = 'List' Suggestions = @('1', '7', '30', '365') } '/r' = @{ Key = '/r' Display = '/R:n' CompletionText = '/R:' Description = 'Specify the number of retries on failed copies.' InlineValueKind = 'List' Suggestions = @('0', '1', '3', '5', '10') } '/w' = @{ Key = '/w' Display = '/W:n' CompletionText = '/W:' Description = 'Specify the wait time between retries in seconds.' InlineValueKind = 'List' Suggestions = @('0', '1', '5', '10', '30') } '/log' = @{ Key = '/log' Display = '/LOG:file' CompletionText = '/LOG:' Description = 'Write status output to the specified log file.' InlineValueKind = 'Path' } '/log+' = @{ Key = '/log+' Display = '/LOG+:file' CompletionText = '/LOG+:' Description = 'Append status output to the specified log file.' InlineValueKind = 'Path' } '/unilog' = @{ Key = '/unilog' Display = '/UNILOG:file' CompletionText = '/UNILOG:' Description = 'Write Unicode status output to the specified log file.' InlineValueKind = 'Path' } '/unilog+' = @{ Key = '/unilog+' Display = '/UNILOG+:file' CompletionText = '/UNILOG+:' Description = 'Append Unicode status output to the specified log file.' InlineValueKind = 'Path' } '/job' = @{ Key = '/job' Display = '/JOB:jobname' CompletionText = '/JOB:' Description = 'Load parameters from the specified job file.' InlineValueKind = 'Job' } '/save' = @{ Key = '/save' Display = '/SAVE:jobname' CompletionText = '/SAVE:' Description = 'Save parameters to the specified job file.' InlineValueKind = 'Job' } '/xf' = @{ Key = '/xf' Display = '/XF file [file]...' CompletionText = '/XF' Description = 'Exclude files matching the given names or paths.' SeparateValueKind = 'FileSpecMulti' } '/xd' = @{ Key = '/xd' Display = '/XD dirs [dirs]...' CompletionText = '/XD' Description = 'Exclude directories matching the given names or paths.' SeparateValueKind = 'DirectorySpecMulti' } } } function New-RobocopyCompletionResult { param( [string]$CompletionText, [string]$ResultType, [string]$ToolTip, [string]$ListItemText ) if ([string]::IsNullOrWhiteSpace($ListItemText)) { $ListItemText = $CompletionText } if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $CompletionText } [System.Management.Automation.CompletionResult]::new( $CompletionText, $ListItemText, $ResultType, $ToolTip ) } function Remove-RobocopyOuterQuotes { param([string]$Text) if ($null -eq $Text) { return $Text } $Text.Trim([char[]]@([char]34, [char]39)) } function ConvertTo-RobocopyQuotedValue { 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('"'))) { return '"' + $Value + '"' } $Value } function ConvertFrom-RobocopyHelpToken { param([string]$Token) $cleanToken = $Token.Trim().TrimEnd('.', ',', ';', ')') if ([string]::IsNullOrWhiteSpace($cleanToken) -or -not $cleanToken.StartsWith('/')) { return $null } $match = [regex]::Match($cleanToken, '^(?<root>/[A-Za-z0-9?]+(?:[+-])?)(?<suffix>.*)$') if (-not $match.Success) { return $null } $root = $match.Groups['root'].Value $suffix = $match.Groups['suffix'].Value $completionText = if ($suffix -match '^\[:') { $root } elseif ($suffix.StartsWith(':')) { "${root}:" } else { $root } @{ Key = $root.ToLowerInvariant() Display = $cleanToken CompletionText = $completionText Description = $cleanToken } } function Initialize-RobocopyCompletion { if ($script:RobocopyCompletionCatalog.Initialized) { return } $catalog = @{} $staticMetadata = Get-RobocopyStaticOptionMetadata foreach ($key in $staticMetadata.Keys) { $catalog[$key] = @{} + $staticMetadata[$key] } $helpLines = Invoke-RobocopyHelpText foreach ($line in $helpLines) { if ([string]::IsNullOrWhiteSpace($line)) { continue } # Only a canonical '<option> :: <text>' line describes an option, and only # the option that starts the line. Cross-references inside another # option's text and the Remarks prose may register a token they mention # but never overwrite its description. $describedKey = $null $description = '' $canonical = [regex]::Match($line, '^\s*(?<token>/[A-Za-z0-9?][^\s]*)[^:]*::\s*(?<description>.+)$') if ($canonical.Success) { $describedToken = ConvertFrom-RobocopyHelpToken -Token $canonical.Groups['token'].Value if ($null -ne $describedToken) { $describedKey = $describedToken.Key $description = $canonical.Groups['description'].Value.Trim() } } foreach ($match in [regex]::Matches($line, '(?<!\S)/[A-Za-z0-9?][^\s]*')) { $parsed = ConvertFrom-RobocopyHelpToken -Token $match.Value if ($null -eq $parsed) { continue } $key = $parsed.Key $isDescribed = ($key -eq $describedKey) -and -not [string]::IsNullOrWhiteSpace($description) if ($catalog.ContainsKey($key)) { if ($isDescribed) { $catalog[$key]['Description'] = $description } if (-not $catalog[$key].ContainsKey('Display')) { $catalog[$key]['Display'] = $parsed.Display } if (-not $catalog[$key].ContainsKey('CompletionText')) { $catalog[$key]['CompletionText'] = $parsed.CompletionText } continue } $catalog[$key] = @{} + $parsed if ($isDescribed) { $catalog[$key]['Description'] = $description } } } $script:RobocopyCompletionCatalog.Options = @( foreach ($entry in $catalog.Values) { [pscustomobject]$entry } ) | Sort-Object -Property CompletionText, Display -Unique $script:RobocopyCompletionCatalog.OptionInfoByKey = @{} foreach ($option in $script:RobocopyCompletionCatalog.Options) { $script:RobocopyCompletionCatalog.OptionInfoByKey[$option.Key] = $option } $script:RobocopyCompletionCatalog.Initialized = $true } function Get-RobocopyOptionKey { param([string]$Token) $cleanToken = Remove-RobocopyOuterQuotes $Token if ([string]::IsNullOrWhiteSpace($cleanToken) -or -not $cleanToken.StartsWith('/')) { return $null } $match = [regex]::Match($cleanToken, '^(?<root>/[A-Za-z0-9?]+(?:[+-])?)') if ($match.Success) { return $match.Groups['root'].Value.ToLowerInvariant() } $null } function Resolve-RobocopySourcePath { param([string]$PathText) $cleanPath = Remove-RobocopyOuterQuotes $PathText if ([string]::IsNullOrWhiteSpace($cleanPath) -or $cleanPath.StartsWith('\\')) { # A UNC source is never resolved from the completion thread. return $null } $errorCountBefore = $Error.Count try { (Resolve-Path -LiteralPath $cleanPath -ErrorAction Stop | Select-Object -First 1 -ExpandProperty Path) } catch { $null } finally { while ($Error.Count -gt $errorCountBefore) { $Error.RemoveAt(0) } } } function Get-RobocopyArgumentList { param( [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) $arguments = @() foreach ($element in $CommandAst.CommandElements | Select-Object -Skip 1) { if ($element.Extent.EndOffset -lt $CursorPosition) { $arguments += $element.Extent.Text } } $arguments } function Get-RobocopyCompletionContext { param([string[]]$Arguments) Initialize-RobocopyCompletion $positionals = New-Object System.Collections.Generic.List[string] $pendingMode = $null $sawOption = $false foreach ($argument in $Arguments) { if ([string]::IsNullOrWhiteSpace($argument)) { continue } $optionKey = Get-RobocopyOptionKey -Token $argument if ($pendingMode) { if ($null -eq $optionKey) { continue } $pendingMode = $null } if ($null -ne $optionKey) { $sawOption = $true if ($script:RobocopyCompletionCatalog.OptionInfoByKey.ContainsKey($optionKey)) { $optionInfo = $script:RobocopyCompletionCatalog.OptionInfoByKey[$optionKey] if ($optionInfo.PSObject.Properties.Name -contains 'SeparateValueKind') { $pendingMode = $optionInfo.SeparateValueKind } } continue } $positionals.Add($argument) } [pscustomobject]@{ PendingMode = $pendingMode SawOption = $sawOption Positionals = @($positionals) SourcePath = if ($positionals.Count -gt 0) { Resolve-RobocopySourcePath -PathText $positionals[0] } else { $null } } } function Get-RobocopyUniqueCompletions { param([System.Management.Automation.CompletionResult[]]$Results) $seen = @{} $unique = New-Object System.Collections.Generic.List[object] foreach ($result in $Results) { if ($null -eq $result) { continue } if ($seen.ContainsKey($result.CompletionText)) { continue } $seen[$result.CompletionText] = $true [void]$unique.Add($result) } @($unique.ToArray()) } function Get-RobocopyPathCompletions { param( [string]$InputPath, [string]$Kind, [string]$CompletionPrefix = '' ) $cleanInput = Remove-RobocopyOuterQuotes $InputPath $alwaysQuote = -not [string]::IsNullOrEmpty($InputPath) -and $InputPath.StartsWith('"') if ($cleanInput.StartsWith('\\')) { # robocopy's dominant use is UNC, but a share is never enumerated from # the completion thread: keep the typed path and say so. return @( New-RobocopyCompletionResult -CompletionText $InputPath -ListItemText '<\\server\share\path>' -ResultType 'ParameterValue' -ToolTip 'UNC path; network shares are not enumerated during completion.' ) } if ([string]::IsNullOrWhiteSpace($cleanInput)) { $parent = '.' $leaf = '' } elseif ($cleanInput -match '[\\/]$') { $parent = $cleanInput $leaf = '' } else { $parent = Split-Path -Path $cleanInput -Parent if ([string]::IsNullOrWhiteSpace($parent)) { $parent = '.' } $leaf = Split-Path -Path $cleanInput -Leaf } $inputIsRooted = -not [string]::IsNullOrWhiteSpace($cleanInput) -and [System.IO.Path]::IsPathRooted($cleanInput) $items = @(Get-ChildItem -LiteralPath $parent -ErrorAction SilentlyContinue) $items = $items | Where-Object { $_.Name -like ([System.Management.Automation.WildcardPattern]::Escape($leaf) + '*') } if ($Kind -eq 'Directory') { $items = $items | Where-Object { $_.PSIsContainer } } elseif ($Kind -eq 'File') { $items = $items | Where-Object { -not $_.PSIsContainer } } $results = New-Object System.Collections.Generic.List[object] foreach ($item in $items | Sort-Object -Property Name | Select-Object -First $script:RobocopyCompletionCatalog.MaxPathResults) { if ($inputIsRooted) { $pathText = Join-Path -Path $parent -ChildPath $item.Name } elseif ($parent -eq '.' -or [string]::IsNullOrWhiteSpace($cleanInput)) { $pathText = $item.Name } else { $pathText = Join-Path -Path $parent -ChildPath $item.Name } if ($item.PSIsContainer -and -not $pathText.EndsWith('\')) { $pathText += '\' } $tokenText = if ([string]::IsNullOrEmpty($CompletionPrefix)) { $pathText } else { $CompletionPrefix + $pathText } [void]$results.Add((New-RobocopyCompletionResult ` -CompletionText (ConvertTo-RobocopyQuotedValue -Value $tokenText -AlwaysQuote $alwaysQuote) ` -ListItemText $tokenText ` -ResultType 'ParameterValue' ` -ToolTip $item.FullName)) } if ($results.Count -eq 0 -and $Kind -eq 'Directory' -and [string]::IsNullOrEmpty($CompletionPrefix)) { # The source and destination operands must be directories; without a # placeholder the engine would substitute file names here. $placeholder = if ([string]::IsNullOrWhiteSpace($cleanInput)) { '<directory>' } else { $InputPath } [void]$results.Add((New-RobocopyCompletionResult -CompletionText $placeholder -ListItemText '<directory>' -ResultType 'ParameterValue' -ToolTip 'Directory path (drive:\path or \\server\share\path).')) } @($results.ToArray()) } function Get-RobocopySourceRelativeCompletions { param( [string]$InputPath, [string]$SourcePath, [string]$Kind ) $cleanInput = Remove-RobocopyOuterQuotes $InputPath if ([string]::IsNullOrWhiteSpace($SourcePath) -or -not (Test-Path -LiteralPath $SourcePath -PathType Container)) { return @() } if (-not [string]::IsNullOrWhiteSpace($cleanInput) -and [System.IO.Path]::IsPathRooted($cleanInput)) { return @(Get-RobocopyPathCompletions -InputPath $InputPath -Kind $Kind) } $alwaysQuote = -not [string]::IsNullOrEmpty($InputPath) -and $InputPath.StartsWith('"') if ([string]::IsNullOrWhiteSpace($cleanInput)) { $relativeParent = '' $leaf = '' } elseif ($cleanInput -match '[\\/]$') { $relativeParent = $cleanInput.TrimEnd([char[]]@([char]92, [char]47)) $leaf = '' } else { $relativeParent = Split-Path -Path $cleanInput -Parent $leaf = Split-Path -Path $cleanInput -Leaf } $basePath = if ([string]::IsNullOrWhiteSpace($relativeParent)) { $SourcePath } else { Join-Path -Path $SourcePath -ChildPath $relativeParent } $items = @(Get-ChildItem -LiteralPath $basePath -ErrorAction SilentlyContinue) $items = $items | Where-Object { $_.Name -like ([System.Management.Automation.WildcardPattern]::Escape($leaf) + '*') } if ($Kind -eq 'Directory') { $items = $items | Where-Object { $_.PSIsContainer } } elseif ($Kind -eq 'File') { $items = $items | Where-Object { -not $_.PSIsContainer } } # A source such as System32 holds thousands of entries; cap what is emitted # so the completion thread never builds the whole listing. foreach ($item in $items | Sort-Object -Property Name | Select-Object -First $script:RobocopyCompletionCatalog.MaxPathResults) { $pathText = if ([string]::IsNullOrWhiteSpace($relativeParent)) { $item.Name } else { Join-Path -Path $relativeParent -ChildPath $item.Name } if ($item.PSIsContainer -and -not $pathText.EndsWith('\')) { $pathText += '\' } New-RobocopyCompletionResult ` -CompletionText (ConvertTo-RobocopyQuotedValue -Value $pathText -AlwaysQuote $alwaysQuote) ` -ListItemText $pathText ` -ResultType 'ParameterValue' ` -ToolTip $item.FullName } } function Get-RobocopyWildcardSuggestions { param( [string]$WordToComplete, [string[]]$Candidates ) $cleanWord = Remove-RobocopyOuterQuotes $WordToComplete foreach ($candidate in ($Candidates | Sort-Object -Unique)) { if ($candidate -like ([System.Management.Automation.WildcardPattern]::Escape($cleanWord) + '*')) { New-RobocopyCompletionResult -CompletionText $candidate -ListItemText $candidate -ResultType 'ParameterValue' -ToolTip 'Wildcard selection pattern.' } } } function Get-RobocopyFileSpecCompletions { param( [string]$WordToComplete, [string]$SourcePath ) # 'file :: File(s) to copy (names/wildcards)' and '/XF file' take file names; # directories belong to /XD. $results = New-Object System.Collections.Generic.List[object] foreach ($result in @(Get-RobocopyWildcardSuggestions -WordToComplete $WordToComplete -Candidates @('*', '*.*'))) { [void]$results.Add($result) } foreach ($result in @(Get-RobocopySourceRelativeCompletions -InputPath $WordToComplete -SourcePath $SourcePath -Kind 'File')) { [void]$results.Add($result) } Get-RobocopyUniqueCompletions -Results $results.ToArray() } function Get-RobocopyDirectorySpecCompletions { param( [string]$WordToComplete, [string]$SourcePath ) $results = @() $results += @(Get-RobocopyWildcardSuggestions -WordToComplete $WordToComplete -Candidates @('*')) $results += @(Get-RobocopySourceRelativeCompletions -InputPath $WordToComplete -SourcePath $SourcePath -Kind 'Directory') Get-RobocopyUniqueCompletions -Results $results } function Get-RobocopyPrefixedSuggestions { param( [string]$Prefix, [string]$CurrentValue, [string[]]$Suggestions, [string]$ToolTip ) $cleanCurrentValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue.ToUpperInvariant() } foreach ($suggestion in ($Suggestions | Sort-Object -Unique)) { if ($suggestion.ToUpperInvariant().StartsWith($cleanCurrentValue)) { $tokenText = $Prefix + $suggestion New-RobocopyCompletionResult -CompletionText (ConvertTo-RobocopyQuotedValue -Value $tokenText) -ListItemText $tokenText -ResultType 'ParameterValue' -ToolTip $ToolTip } } } function Get-RobocopyFlagValueCompletions { param( [string]$Prefix, [string]$CurrentValue, [string]$AllowedCharacters, [string[]]$Suggestions, [string]$ToolTip ) $results = @() $currentUpper = if ($null -eq $CurrentValue) { '' } else { $CurrentValue.ToUpperInvariant() } $results += @(Get-RobocopyPrefixedSuggestions -Prefix $Prefix -CurrentValue $CurrentValue -Suggestions $Suggestions -ToolTip $ToolTip) foreach ($character in $AllowedCharacters.ToCharArray()) { if ($currentUpper.IndexOf([string]$character) -ge 0) { continue } $candidate = $currentUpper + [string]$character $tokenText = $Prefix + $candidate $results += New-RobocopyCompletionResult -CompletionText $tokenText -ListItemText $tokenText -ResultType 'ParameterValue' -ToolTip $ToolTip } Get-RobocopyUniqueCompletions -Results $results } function Get-RobocopyJobCompletions { param( [string]$Prefix, [string]$CurrentValue, [string]$ToolTip ) $cleanCurrentValue = Remove-RobocopyOuterQuotes $CurrentValue $jobFiles = @(Get-ChildItem -LiteralPath . -Filter '*.rcj' -File -ErrorAction SilentlyContinue) $results = New-Object System.Collections.Generic.List[object] foreach ($jobFile in $jobFiles | Sort-Object -Property Name) { $jobName = [System.IO.Path]::GetFileNameWithoutExtension($jobFile.Name) if ($jobName -like ([System.Management.Automation.WildcardPattern]::Escape($cleanCurrentValue) + '*')) { $tokenText = $Prefix + $jobName [void]$results.Add((New-RobocopyCompletionResult -CompletionText (ConvertTo-RobocopyQuotedValue -Value $tokenText) -ListItemText $tokenText -ResultType 'ParameterValue' -ToolTip $jobFile.FullName)) } } if ($results.Count -eq 0) { # No matching .rcj in the cwd: name the slot instead of letting the option # list echo the '/JOB:' the user already typed. $tokenText = if ([string]::IsNullOrWhiteSpace($cleanCurrentValue)) { $Prefix + '<jobname>' } else { $Prefix + $CurrentValue } [void]$results.Add((New-RobocopyCompletionResult -CompletionText $tokenText -ListItemText ($Prefix + '<jobname>') -ResultType 'ParameterValue' -ToolTip $ToolTip)) } @($results.ToArray()) } function Get-RobocopyInlineValueCompletions { param([string]$WordToComplete) Initialize-RobocopyCompletion $cleanWord = Remove-RobocopyOuterQuotes $WordToComplete $match = [regex]::Match($cleanWord, '^(?<root>/[A-Za-z0-9?]+(?:[+-])?)(?<separator>:)(?<value>.*)$') if (-not $match.Success) { return @() } $key = $match.Groups['root'].Value.ToLowerInvariant() if (-not $script:RobocopyCompletionCatalog.OptionInfoByKey.ContainsKey($key)) { return @() } $optionInfo = $script:RobocopyCompletionCatalog.OptionInfoByKey[$key] if (-not ($optionInfo.PSObject.Properties.Name -contains 'InlineValueKind')) { return @() } $prefix = $match.Groups['root'].Value + ':' $currentValue = $match.Groups['value'].Value switch ($optionInfo.InlineValueKind) { 'Path' { return @(Get-RobocopyPathCompletions -InputPath $currentValue -Kind 'Any' -CompletionPrefix $prefix) } 'Job' { return @(Get-RobocopyJobCompletions -Prefix $prefix -CurrentValue $currentValue -ToolTip $optionInfo.Description) } 'List' { return @(Get-RobocopyPrefixedSuggestions -Prefix $prefix -CurrentValue $currentValue -Suggestions $optionInfo.Suggestions -ToolTip $optionInfo.Description) } 'Flags' { return @(Get-RobocopyFlagValueCompletions -Prefix $prefix -CurrentValue $currentValue -AllowedCharacters $optionInfo.AllowedCharacters -Suggestions $optionInfo.Suggestions -ToolTip $optionInfo.Description) } } @() } function Get-RobocopyOptionCompletions { param([string]$WordToComplete) Initialize-RobocopyCompletion $prefix = if ([string]::IsNullOrWhiteSpace($WordToComplete)) { '' } else { (Remove-RobocopyOuterQuotes $WordToComplete).ToUpperInvariant() } foreach ($option in $script:RobocopyCompletionCatalog.Options) { if ($option.CompletionText.ToUpperInvariant().StartsWith($prefix) -or $option.Display.ToUpperInvariant().StartsWith($prefix)) { New-RobocopyCompletionResult ` -CompletionText $option.CompletionText ` -ListItemText $option.Display ` -ResultType 'ParameterName' ` -ToolTip $option.Description } } } function Complete-Robocopy { param( [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [int]$cursorPosition ) if (-not (Get-Command -Name robocopy.exe -ErrorAction SilentlyContinue)) { return @() } Initialize-RobocopyCompletion $currentWord = if ($null -eq $wordToComplete) { '' } else { $wordToComplete } if (-not [string]::IsNullOrEmpty($currentWord) -and (Remove-RobocopyOuterQuotes $currentWord).StartsWith('/')) { $inlineValueCompletions = @(Get-RobocopyInlineValueCompletions -WordToComplete $currentWord) if ($inlineValueCompletions.Count -gt 0) { return $inlineValueCompletions } return @(Get-RobocopyOptionCompletions -WordToComplete $currentWord) } $arguments = @(Get-RobocopyArgumentList -CommandAst $commandAst -CursorPosition $cursorPosition) $context = Get-RobocopyCompletionContext -Arguments $arguments switch ($context.PendingMode) { 'FileSpecMulti' { return @(Get-RobocopyFileSpecCompletions -WordToComplete $currentWord -SourcePath $context.SourcePath) } 'DirectorySpecMulti' { return @(Get-RobocopyDirectorySpecCompletions -WordToComplete $currentWord -SourcePath $context.SourcePath) } } if ($context.Positionals.Count -lt 2) { return @(Get-RobocopyPathCompletions -InputPath $currentWord -Kind 'Directory') } if ([string]::IsNullOrWhiteSpace($currentWord)) { $results = @() $results += @(Get-RobocopyFileSpecCompletions -WordToComplete $currentWord -SourcePath $context.SourcePath) $results += @(Get-RobocopyOptionCompletions -WordToComplete $currentWord) return @(Get-RobocopyUniqueCompletions -Results $results) } @(Get-RobocopyFileSpecCompletions -WordToComplete $currentWord -SourcePath $context.SourcePath) } Register-ArgumentCompleter -Native -CommandName 'robocopy', 'robocopy.exe' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-Robocopy -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } |