├── .ci ├── ci.yml ├── releaseBuild.yml └── test.yml ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── Microsoft.PowerShell.UnixTabCompletion.psd1 ├── Microsoft.PowerShell.UnixTabCompletion ├── BashUtilCompleter.cs ├── Commands │ ├── GetPSUnixUtilCompletersCompleterCommand.cs │ ├── ImportPSUnixUtilCompletersCommand.cs │ ├── RemovePSUnixUtilCompletersCommand.cs │ ├── SetPSUnixUtilCompletersCompleterCommand.cs │ └── Utils.cs ├── IUnixUtilCompleter.cs ├── Microsoft.PowerShell.UnixTabCompletion.csproj ├── UnixHelpers.cs ├── UnixUtilCompletion.cs ├── UtilCompleterInitializer.cs └── ZshUtilCompleter.cs ├── OnStart.ps1 ├── README.md ├── build.ps1 ├── completions.gif └── tests ├── Completion.Tests.ps1 └── bash-completer /.ci/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build-$(System.PullRequest.PullRequestNumber)-$(Date:yyyyMMdd)$(Rev:.rr) 2 | trigger: 3 | # Batch merge builds together while a merge build is running 4 | batch: true 5 | branches: 6 | include: 7 | - main 8 | pr: 9 | branches: 10 | include: 11 | - main 12 | 13 | resources: 14 | repositories: 15 | - repository: ComplianceRepo 16 | type: github 17 | endpoint: ComplianceGHRepo 18 | name: PowerShell/compliance 19 | 20 | stages: 21 | - stage: Build 22 | displayName: Build Completers Package 23 | jobs: 24 | - job: BuildPkg 25 | displayName: Build Package 26 | pool: 27 | vmImage: windows-2019 28 | steps: 29 | - pwsh: | 30 | $(Build.SourcesDirectory)/build.ps1 -Configuration Release 31 | displayName: Build 32 | 33 | - pwsh: | 34 | $env:BUILD_OUTPUT_PATH = "$(Build.SourcesDirectory)/out" 35 | $env:SIGNED_OUTPUT_PATH = "$(Build.SourcesDirectory)/signed" 36 | (Get-Item -ea ignore "$(Build.SourcesDirectory)/signed") || (New-Item -ItemType Directory -Path "$(Build.SourcesDirectory)/signed") 37 | Write-Verbose "BUILD_OUTPUT_PATH- $env:BUILD_OUTPUT_PATH" -Verbose 38 | Write-Verbose "SIGNED_OUTPUT_PATH- $env:SIGNED_OUTPUT_PATH" -Verbose 39 | Copy-Item "${env:BUILD_OUTPUT_PATH}/*" $env:SIGNED_OUTPUT_PATH -Recurse -Force -Verbose 40 | displayName: Build Signing Placeholder 41 | 42 | - pwsh: | 43 | $repoName = [guid]::newguid().ToString("N") 44 | $moduleName = "Microsoft.PowerShell.UnixTabCompletion" 45 | try { 46 | Register-PSRepository -Name $repoName -SourceLocation '$(System.ArtifactsDirectory)' -ErrorAction Ignore 47 | Publish-Module -Repository $repoName -Path "$(Build.SourcesDirectory)/signed/${moduleName}" 48 | $nupkg = Get-ChildItem '$(System.ArtifactsDirectory)/*.nupkg' 49 | $nupkgName = $nupkg.Name 50 | $nupkgPath = $nupkg.FullName 51 | Write-Host "##vso[artifact.upload containerfolder=$nupkgName;artifactname=$nupkgName;]$nupkgPath" 52 | } 53 | finally { 54 | Unregister-PSRepository -Name $repoName -ErrorAction SilentlyContinue 55 | } 56 | Get-ChildItem -rec '$(System.ArtifactsDirectory)' | Write-Verbose -Verbose 57 | Get-ChildItem -rec -file | Write-Verbose -Verbose 58 | displayName: Create module package 59 | 60 | 61 | - stage: Compliance 62 | displayName: Compliance 63 | dependsOn: Build 64 | jobs: 65 | - job: ComplianceJob 66 | pool: 67 | vmImage: Windows-latest 68 | steps: 69 | - checkout: self 70 | - checkout: ComplianceRepo 71 | - template: ci-compliance.yml@ComplianceRepo 72 | 73 | - stage: Test 74 | displayName: Test Package 75 | jobs: 76 | - template: test.yml 77 | parameters: 78 | jobName: TestPkgUbuntu18 79 | displayName: PowerShell Core on Ubuntu 18.04 80 | imageName: ubuntu-latest 81 | 82 | - template: test.yml 83 | parameters: 84 | jobName: TestPkgWinMacOS 85 | displayName: PowerShell Core on macOS 86 | imageName: macOS-latest 87 | -------------------------------------------------------------------------------- /.ci/releaseBuild.yml: -------------------------------------------------------------------------------- 1 | name: Microsoft.PowerShell.UnixTabCompletion-Release-$(Build.BuildId) 2 | trigger: 3 | batch: true 4 | branches: 5 | include: 6 | - main 7 | pr: none 8 | 9 | variables: 10 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 11 | POWERSHELL_TELEMETRY_OPTOUT: 1 12 | 13 | resources: 14 | repositories: 15 | - repository: ComplianceRepo 16 | type: github 17 | endpoint: ComplianceGHRepo 18 | name: PowerShell/compliance 19 | ref: master 20 | 21 | stages: 22 | - stage: Build 23 | displayName: Build 24 | pool: 25 | name: 1ES 26 | demands: 27 | - ImageOverride -equals PSMMS2019-Secure 28 | jobs: 29 | - job: Build_Job 30 | displayName: Build Microsoft.PowerShell.UnixTabCompletion 31 | variables: 32 | - group: ESRP 33 | steps: 34 | - checkout: self 35 | 36 | - pwsh: | 37 | Set-Location "$(Build.SourcesDirectory)/UnixCompleters" 38 | Get-ChildItem -Recurse -File -Name | Write-Verbose -Verbose 39 | ./build.ps1 -Configuration Release 40 | displayName: Execute build 41 | 42 | - pwsh: | 43 | $repoName = "UnixCompleters" 44 | $signSrcPath = "$(Build.SourcesDirectory)/${repoName}" 45 | Set-Location "$signSrcPath" 46 | $moduleName = "Microsoft.PowerShell.UnixTabCompletion" 47 | $moduleInfo = Import-PowerShellDataFile -Path "${moduleName}.psd1" 48 | $moduleVersion = $moduleInfo.ModuleVersion.ToString() 49 | $signSrcPath = "$(Build.SourcesDirectory)/${repoName}/out/${moduleName}/${moduleVersion}" 50 | 51 | # Set signing src path variable 52 | $vstsCommandString = "vso[task.setvariable variable=signSrcPath]${signSrcPath}" 53 | Write-Host "##$vstsCommandString" 54 | 55 | $signOutPath = "$(Build.SourcesDirectory)/${repoName}/signed/${moduleName}/${moduleVersion}" 56 | $null = New-Item -ItemType Directory -Path $signOutPath 57 | # Set signing out path variable 58 | $vstsCommandString = "vso[task.setvariable variable=signOutPath]${signOutPath}" 59 | Write-Host "##$vstsCommandString" 60 | 61 | # Set path variable for guardian codesign validation 62 | $vstsCommandString = "vso[task.setvariable variable=GDN_CODESIGN_TARGETDIRECTORY]${signOutPath}" 63 | Write-Host "##$vstsCommandString" 64 | 65 | displayName: Setup variables for signing 66 | 67 | - checkout: ComplianceRepo 68 | 69 | - template: EsrpSign.yml@ComplianceRepo 70 | parameters: 71 | # the folder which contains the binaries to sign 72 | buildOutputPath: $(signSrcPath) 73 | # the location to put the signed output 74 | signOutputPath: $(signOutPath) 75 | # the certificate ID to use 76 | certificateId: "CP-230012" 77 | # the file pattern to use, comma separated 78 | pattern: '*.psm1,*.psd1,*.ps1xml,*.ps1,*.dll' 79 | 80 | - template: Sbom.yml@ComplianceRepo 81 | parameters: 82 | BuildDropPath: $(signOutPath) 83 | Build_Repository_Uri: 'https://github.com/powershell/UnixCompleters' 84 | 85 | - pwsh: | 86 | $src = $env:signSrcPath 87 | $dst = $env:signOutPath 88 | Set-Location "$src" 89 | Get-ChildItem -Recu -File 90 | $moduleName = "Microsoft.PowerShell.UnixTabCompletion" 91 | $moduleInfo = Import-PowerShellDataFile -Path "${moduleName}.psd1" 92 | $moduleVersion = $moduleInfo.ModuleVersion.ToString() 93 | $noSignFiles = "zcomplete.sh","LICENSE.txt" 94 | foreach($file in $noSignFiles) { 95 | copy-item (Join-Path $src $file) (Join-Path $dst $file) 96 | } 97 | displayName: Copy unsigned files 98 | 99 | - pwsh: | 100 | $repoName = [guid]::newguid().ToString("N") 101 | $moduleName = "Microsoft.PowerShell.UnixTabCompletion" 102 | try { 103 | Register-PSRepository -Name $repoName -SourceLocation '$(System.ArtifactsDirectory)' -ErrorAction Ignore 104 | Publish-Module -Repository $repoName -Path "$(Build.SourcesDirectory)/UnixCompleters/signed/${moduleName}" 105 | $nupkg = Get-ChildItem '$(System.ArtifactsDirectory)/*.nupkg' 106 | $nupkgName = $nupkg.Name 107 | $nupkgPath = $nupkg.FullName 108 | Write-Host "##vso[artifact.upload containerfolder=$nupkgName;artifactname=$nupkgName;]$nupkgPath" 109 | } 110 | finally { 111 | Unregister-PSRepository -Name $repoName -ErrorAction SilentlyContinue 112 | } 113 | Get-ChildItem -rec '$(System.ArtifactsDirectory)' | Write-Verbose -Verbose 114 | Get-ChildItem -rec -file | Write-Verbose -Verbose 115 | displayName: Create signed module nupkg 116 | 117 | - publish: "$(signSrcPath)" 118 | artifact: build 119 | displayName: Publish build 120 | 121 | - stage: compliance 122 | displayName: Compliance 123 | dependsOn: Build 124 | jobs: 125 | - job: Compliance_Job 126 | pool: 127 | name: 1ES 128 | steps: 129 | - checkout: self 130 | - checkout: ComplianceRepo 131 | - download: current 132 | artifact: build 133 | 134 | - pwsh: | 135 | Get-ChildItem -Path "$(Pipeline.Workspace)\build" -Recurse 136 | displayName: Capture downloaded artifacts 137 | 138 | - template: script-module-compliance.yml@ComplianceRepo 139 | parameters: 140 | # component-governance 141 | sourceScanPath: '$(Build.SourcesDirectory)' 142 | # credscan 143 | suppressionsFile: '' 144 | # TermCheck 145 | optionsRulesDBPath: '' 146 | optionsFTPath: '' 147 | # tsa-upload 148 | codeBaseName: 'UnixCompleters_20220407' 149 | # selections 150 | APIScan: false # set to false when not using Windows APIs. 151 | 152 | -------------------------------------------------------------------------------- /.ci/test.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | jobName: TestPkgWin 3 | imageName: windows-2019 4 | displayName: PowerShell Core on Windows 5 | powershellExecutable: pwsh 6 | 7 | jobs: 8 | - job: ${{ parameters.jobName }} 9 | pool: 10 | vmImage: ${{ parameters.imageName }} 11 | displayName: ${{ parameters.displayName }} 12 | steps: 13 | - ${{ parameters.powershellExecutable }}: | 14 | Install-module Pester -Force 15 | displayName: Install dependencies - Pester 16 | timeoutInMinutes: 10 17 | 18 | - ${{ parameters.powershellExecutable }}: | 19 | Install-Module -Name "PSScriptAnalyzer" -RequiredVersion 1.20.0 -Force 20 | displayName: Install dependencies - ScriptAnalyzer 21 | timeoutInMinutes: 10 22 | 23 | - ${{ parameters.powershellExecutable }}: | 24 | if ( $IsLinux ) { 25 | sudo apt update -y 26 | sudo apt install -y bash-completion 27 | } 28 | else { 29 | brew install bash-completion 30 | } 31 | displayName: Install dependencies - BashCompletion 32 | timeoutInMinutes: 10 33 | 34 | - task: DownloadBuildArtifacts@0 35 | displayName: 'Download artifacts' 36 | inputs: 37 | buildType: current 38 | downloadType: specific 39 | itemPattern: '**/*.nupkg' 40 | downloadPath: '$(System.ArtifactsDirectory)' 41 | 42 | - ${{ parameters.powershellExecutable }}: | 43 | $repoName = [guid]::newguid().ToString("N") 44 | $moduleName = "Microsoft.PowerShell.UnixTabCompletion" 45 | Get-ChildItem -rec -file | Write-Verbose -Verbose 46 | try { 47 | Register-PSRepository -Name $repoName -SourceLocation '$(System.ArtifactsDirectory)' -ErrorAction Ignore 48 | New-Item -ItemType Directory out 49 | Save-Module -Repository $repoName -Name $moduleName -Path out -AcceptLicense -Force 50 | } 51 | finally { 52 | Unregister-PSRepository -Name $repoName -ErrorAction SilentlyContinue 53 | } 54 | displayName: Extract product artifact 55 | timeoutInMinutes: 10 56 | 57 | - ${{ parameters.powershellExecutable }}: | 58 | $moduleName = "Microsoft.PowerShell.UnixTabCompletion" 59 | Import-Module ./out/${moduleName} 60 | ./build.ps1 -test 61 | displayName: Execute functional tests 62 | errorActionPreference: continue 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | bin/ 3 | obj/ 4 | *.dll 5 | *.pdb 6 | packages/ 7 | testResults.xml -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "WARNING01": "*********************************************************************************", 12 | "WARNING02": "The C# extension was unable to automatically decode projects in the current", 13 | "WARNING03": "workspace to create a runnable launch.json file. A template launch.json file has", 14 | "WARNING04": "been created as a placeholder.", 15 | "WARNING05": "", 16 | "WARNING06": "If OmniSharp is currently unable to load your project, you can attempt to resolve", 17 | "WARNING07": "this by restoring any missing project dependencies (example: run 'dotnet restore')", 18 | "WARNING08": "and by fixing any reported errors from building the projects in your workspace.", 19 | "WARNING09": "If this allows OmniSharp to now load your project then --", 20 | "WARNING10": " * Delete this file", 21 | "WARNING11": " * Open the Visual Studio Code command palette (View->Command Palette)", 22 | "WARNING12": " * run the command: '.NET: Generate Assets for Build and Debug'.", 23 | "WARNING13": "", 24 | "WARNING14": "If your project requires a more complex launch configuration, you may wish to delete", 25 | "WARNING15": "this configuration and pick a different template using the 'Add Configuration...'", 26 | "WARNING16": "button at the bottom of this file.", 27 | "WARNING17": "*********************************************************************************", 28 | "preLaunchTask": "build", 29 | "program": "${workspaceFolder}/bin/Debug//.dll", 30 | "args": [], 31 | "cwd": "${workspaceFolder}", 32 | "console": "internalConsole", 33 | "stopAtEntry": false 34 | }, 35 | { 36 | "name": ".NET Core Attach", 37 | "type": "coreclr", 38 | "request": "attach", 39 | "processId": "${command:pickProcess}" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Robert Holt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'Microsoft.PowerShell.UnixTabCompletion' 3 | # 4 | 5 | @{ 6 | 7 | # Script module or binary module file associated with this manifest. 8 | RootModule = 'Microsoft.PowerShell.UnixTabCompletion.dll' 9 | 10 | # Version number of this module. 11 | ModuleVersion = '0.5.0' 12 | 13 | # Supported PSEditions 14 | CompatiblePSEditions = 'Core' 15 | 16 | # ID used to uniquely identify this module 17 | GUID = '042bff5f-9644-43ef-8f4e-d8b8ed5a1f97' 18 | 19 | # Author of this module 20 | Author = 'Microsoft' 21 | 22 | # Company or vendor of this module 23 | CompanyName = 'Microsoft' 24 | 25 | # Copyright statement for this module 26 | Copyright = '© Microsoft' 27 | 28 | # Description of the functionality provided by this module 29 | Description = 'Get parameter completion for native Unix utilities. Requires zsh or bash.' 30 | 31 | # Minimum version of the PowerShell engine required by this module 32 | PowerShellVersion = '7.0' 33 | 34 | # Name of the PowerShell host required by this module 35 | # PowerShellHostName = '' 36 | 37 | # Minimum version of the PowerShell host required by this module 38 | # PowerShellHostVersion = '' 39 | 40 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 41 | # DotNetFrameworkVersion = '' 42 | 43 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 44 | # CLRVersion = '' 45 | 46 | # Processor architecture (None, X86, Amd64) required by this module 47 | # ProcessorArchitecture = '' 48 | 49 | # Modules that must be imported into the global environment prior to importing this module 50 | # RequiredModules = @() 51 | 52 | # Assemblies that must be loaded prior to importing this module 53 | # RequiredAssemblies = @() 54 | 55 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 56 | ScriptsToProcess = @("OnStart.ps1") 57 | 58 | # Type files (.ps1xml) to be loaded when importing this module 59 | # TypesToProcess = @() 60 | 61 | # Format files (.ps1xml) to be loaded when importing this module 62 | # FormatsToProcess = @() 63 | 64 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 65 | # NestedModules = @() 66 | 67 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 68 | FunctionsToExport = @() 69 | 70 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 71 | CmdletsToExport = @( 72 | 'Import-PSUnixTabCompletion', 73 | 'Remove-PSUnixTabCompletion', 74 | 'Set-PSUnixTabCompletion', 75 | 'Get-PSUnixTabCompletion' 76 | ) 77 | 78 | # Variables to export from this module 79 | VariablesToExport = @() 80 | 81 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 82 | AliasesToExport = @() 83 | 84 | # DSC resources to export from this module 85 | # DscResourcesToExport = @() 86 | 87 | # List of all modules packaged with this module 88 | # ModuleList = @() 89 | 90 | # List of all files packaged with this module 91 | # FileList = @() 92 | 93 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 94 | PrivateData = @{ 95 | 96 | PSData = @{ 97 | 98 | # Tags applied to this module. These help with module discovery in online galleries. 99 | # Tags = @() 100 | 101 | # A URL to the license for this module. 102 | LicenseUri = 'https://raw.githubusercontent.com/PowerShell/UnixCompleters/master/LICENSE' 103 | 104 | # A URL to the main website for this project. 105 | ProjectUri = 'https://github.com/PowerShell/UnixCompleters' 106 | 107 | # A URL to an icon representing this module. 108 | # IconUri = '' 109 | 110 | # ReleaseNotes of this module 111 | # ReleaseNotes = '' 112 | 113 | # Prerelease string of this module 114 | # Prerelease = '' 115 | 116 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 117 | RequireLicenseAcceptance = $true 118 | 119 | # External dependent modules of this module 120 | # ExternalModuleDependencies = @() 121 | 122 | } # End of PSData hashtable 123 | 124 | } # End of PrivateData hashtable 125 | 126 | # HelpInfo URI of this module 127 | # HelpInfoURI = '' 128 | 129 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 130 | # DefaultCommandPrefix = '' 131 | 132 | } 133 | -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion/BashUtilCompleter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Management.Automation; 8 | using System.Management.Automation.Language; 9 | using System.Runtime.InteropServices; 10 | using System.Text; 11 | 12 | namespace Microsoft.PowerShell.UnixTabCompletion 13 | { 14 | public class BashUtilCompleter : IUnixUtilCompleter 15 | { 16 | 17 | private string _completionScript = "/usr/share/bash-completion/bash_completion"; 18 | public string CompletionScript 19 | { 20 | get { return _completionScript; } 21 | set { _completionScript = value; } 22 | } 23 | private static readonly string s_resolveCompleterCommandTemplate = string.Join("; ", new [] 24 | { 25 | "-lc \". {0} 2>/dev/null", 26 | "__load_completion {1} 2>/dev/null", 27 | "complete -p {1} 2>/dev/null | sed -E 's/^complete.*-F ([^ ]+).*$/\\1/'\"" 28 | }); 29 | 30 | private readonly Dictionary _commandCompletionFunctions; 31 | 32 | private readonly string _bashPath; 33 | 34 | public BashUtilCompleter(string bashPath, string completionScript) 35 | { 36 | _bashPath = bashPath; 37 | if (!string.IsNullOrEmpty(completionScript)) 38 | { 39 | _completionScript = completionScript; 40 | } 41 | _commandCompletionFunctions = new Dictionary(); 42 | } 43 | 44 | public IEnumerable FindCompletableCommands() 45 | { 46 | return UnixHelpers.NativeUtilNames; 47 | } 48 | 49 | public IEnumerable CompleteCommand( 50 | string command, 51 | string wordToComplete, 52 | CommandAst commandAst, 53 | int cursorPosition) 54 | { 55 | string completerFunction = ResolveCommandCompleterFunction(command); 56 | 57 | int cursorWordIndex = 0; 58 | string previousWord = commandAst.CommandElements[0].Extent.Text; 59 | for (int i = 1; i < commandAst.CommandElements.Count; i++) 60 | { 61 | IScriptExtent elementExtent = commandAst.CommandElements[i].Extent; 62 | 63 | if (cursorPosition < elementExtent.EndColumnNumber) 64 | { 65 | previousWord = commandAst.CommandElements[i - 1].Extent.Text; 66 | cursorWordIndex = i; 67 | break; 68 | } 69 | 70 | if (cursorPosition == elementExtent.EndColumnNumber) 71 | { 72 | previousWord = elementExtent.Text; 73 | cursorWordIndex = i + 1; 74 | break; 75 | } 76 | 77 | if (cursorPosition < elementExtent.StartColumnNumber) 78 | { 79 | previousWord = commandAst.CommandElements[i - 1].Extent.Text; 80 | cursorWordIndex = i; 81 | break; 82 | } 83 | 84 | if (i == commandAst.CommandElements.Count - 1 && cursorPosition > elementExtent.EndColumnNumber) 85 | { 86 | previousWord = elementExtent.Text; 87 | cursorWordIndex = i + 1; 88 | break; 89 | } 90 | } 91 | 92 | string commandLine; 93 | string bashWordArray; 94 | 95 | if (cursorWordIndex > 0) 96 | { 97 | commandLine = "'" + commandAst.Extent.Text + "'"; 98 | 99 | // Handle a case like '/mnt/c/Program Files'/ where the slash is outside the string 100 | IScriptExtent currentExtent = commandAst.CommandElements[cursorWordIndex].Extent; // The presumed slash-prefixed string 101 | IScriptExtent previousExtent = commandAst.CommandElements[cursorWordIndex - 1].Extent; // The string argument 102 | if (currentExtent.Text.StartsWith("/") && currentExtent.StartColumnNumber == previousExtent.EndColumnNumber) 103 | { 104 | commandLine = commandLine.Replace(previousExtent.Text + currentExtent.Text, wordToComplete); 105 | bashWordArray = BuildCompWordsBashArrayString(commandAst.Extent.Text, replaceAt: cursorPosition, replacementWord: wordToComplete); 106 | } 107 | else 108 | { 109 | bashWordArray = BuildCompWordsBashArrayString(commandAst.Extent.Text); 110 | } 111 | } 112 | else if (cursorPosition > commandAst.Extent.Text.Length) 113 | { 114 | cursorWordIndex++; 115 | commandLine = "'" + commandAst.Extent.Text + " '"; 116 | bashWordArray = new StringBuilder(64) 117 | .Append("('").Append(commandAst.Extent.Text).Append("' '')") 118 | .ToString(); 119 | } 120 | else 121 | { 122 | commandLine = "'" + commandAst.Extent.Text + "'"; 123 | bashWordArray = new StringBuilder(32) 124 | .Append("('").Append(wordToComplete).Append("')") 125 | .ToString(); 126 | } 127 | 128 | string completionCommand = BuildCompletionCommand( 129 | command, 130 | COMP_LINE: commandLine, 131 | COMP_WORDS: bashWordArray, 132 | COMP_CWORD: cursorWordIndex, 133 | COMP_POINT: cursorPosition, 134 | completerFunction, 135 | wordToComplete, 136 | previousWord); 137 | 138 | List completionResults = InvokeBashWithArguments(completionCommand) 139 | .Split('\n') 140 | .Distinct(StringComparer.Ordinal) 141 | .ToList(); 142 | 143 | completionResults.Sort(StringComparer.Ordinal); 144 | 145 | string previousCompletion = null; 146 | foreach (string completionResult in completionResults) 147 | { 148 | if (string.IsNullOrEmpty(completionResult)) 149 | { 150 | continue; 151 | } 152 | 153 | // int equalsIndex = wordToComplete.IndexOf('='); 154 | int equalsIndex = wordToComplete.IndexOf(' '); 155 | 156 | string completionText; 157 | string listItemText; 158 | if (equalsIndex >= 0) 159 | { 160 | completionText = wordToComplete.Substring(0, equalsIndex) + completionResult; 161 | listItemText = completionResult; 162 | } 163 | else 164 | { 165 | completionText = completionResult; 166 | listItemText = completionText; 167 | } 168 | 169 | if (completionText.Equals(previousCompletion)) 170 | { 171 | listItemText += " "; 172 | } 173 | 174 | previousCompletion = completionText; 175 | 176 | yield return new CompletionResult( 177 | completionText, 178 | listItemText, 179 | CompletionResultType.ParameterName, 180 | completionText); 181 | } 182 | } 183 | 184 | private string ResolveCommandCompleterFunction(string commandName) 185 | { 186 | if (string.IsNullOrEmpty(commandName)) 187 | { 188 | throw new ArgumentException(nameof(commandName)); 189 | } 190 | 191 | string completerFunction; 192 | if (_commandCompletionFunctions.TryGetValue(commandName, out completerFunction)) 193 | { 194 | return completerFunction; 195 | } 196 | 197 | string resolveCompleterInvocation = string.Format(s_resolveCompleterCommandTemplate, _completionScript, commandName); 198 | completerFunction = InvokeBashWithArguments(resolveCompleterInvocation).Trim(); 199 | _commandCompletionFunctions[commandName] = completerFunction; 200 | 201 | return completerFunction; 202 | } 203 | 204 | private string InvokeBashWithArguments(string argumentString) 205 | { 206 | using (var bashProc = new Process()) 207 | { 208 | bashProc.StartInfo.FileName = this._bashPath; 209 | bashProc.StartInfo.Arguments = argumentString; 210 | bashProc.StartInfo.UseShellExecute = false; 211 | bashProc.StartInfo.RedirectStandardOutput = true; 212 | bashProc.Start(); 213 | 214 | return bashProc.StandardOutput.ReadToEnd(); 215 | } 216 | } 217 | 218 | private static string EscapeCompletionResult(string completionResult) 219 | { 220 | completionResult = completionResult.Trim(); 221 | 222 | if (!completionResult.Contains(' ')) 223 | { 224 | return completionResult; 225 | } 226 | 227 | return "'" + completionResult.Replace("'", "''") + "'"; 228 | } 229 | 230 | 231 | private string BuildCompWordsBashArrayString( 232 | string line, 233 | int replaceAt = -1, 234 | string replacementWord = null) 235 | { 236 | // Build a bash array of line components, like "('ls' '-a')" 237 | 238 | string[] lineElements = line.Split(); 239 | 240 | int approximateLength = 0; 241 | foreach (string element in lineElements) 242 | { 243 | approximateLength += lineElements.Length + 2; 244 | } 245 | 246 | var sb = new StringBuilder(approximateLength); 247 | 248 | sb.Append('(') 249 | .Append('\'') 250 | .Append(lineElements[0].Replace("'", "\\'")) 251 | .Append('\''); 252 | 253 | if (replaceAt < 1) 254 | { 255 | for (int i = 1; i < lineElements.Length; i++) 256 | { 257 | sb.Append(' ') 258 | .Append('\'') 259 | .Append(lineElements[i].Replace("'", "\\'")) 260 | .Append('\''); 261 | } 262 | } 263 | else 264 | { 265 | for (int i = 1; i < lineElements.Length; i++) 266 | { 267 | if (i == replaceAt - 1) 268 | { 269 | continue; 270 | } 271 | 272 | if (i == replaceAt) 273 | { 274 | sb.Append(' ').Append(replacementWord); 275 | continue; 276 | } 277 | 278 | sb.Append(' ') 279 | .Append('\'') 280 | .Append(lineElements[i].Replace("'", "\\'")) 281 | .Append('\''); 282 | } 283 | } 284 | 285 | sb.Append(')'); 286 | 287 | return sb.ToString(); 288 | } 289 | 290 | private string BuildCompletionCommand( 291 | string command, 292 | string COMP_LINE, 293 | string COMP_WORDS, 294 | int COMP_CWORD, 295 | int COMP_POINT, 296 | string completionFunction, 297 | string wordToComplete, 298 | string previousWord) 299 | { 300 | return new StringBuilder(512) 301 | .Append("-lc \". ") 302 | .Append(_completionScript) 303 | .Append(" 2>/dev/null; ") 304 | .Append("__load_completion ").Append(command).Append(" 2>/dev/null; ") 305 | .Append("COMP_LINE=").Append(COMP_LINE).Append("; ") 306 | .Append("COMP_WORDS=").Append(COMP_WORDS).Append("; ") 307 | .Append("COMP_CWORD=").Append(COMP_CWORD).Append("; ") 308 | .Append("COMP_POINT=").Append(COMP_POINT).Append("; ") 309 | .Append("bind 'set completion-ignore-case on' 2>/dev/null; ") 310 | .Append(completionFunction) 311 | .Append(" '").Append(command).Append("'") 312 | .Append(" '").Append(wordToComplete).Append("'") 313 | .Append(" '").Append(previousWord).Append("' 2>/dev/null; ") 314 | .Append("IFS=$'\\n'; ") 315 | .Append("echo \"\"\"${COMPREPLY[*]}\"\"\"\"") 316 | .ToString(); 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion/Commands/GetPSUnixUtilCompletersCompleterCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace Microsoft.PowerShell.UnixTabCompletion.Commands 4 | { 5 | /// 6 | ///Retrieve the current unix completer. 7 | /// 8 | [Cmdlet(VerbsCommon.Get, Utils.ModuleName)] 9 | public class GetUnixTabCompletionCommand : PSCmdlet 10 | { 11 | protected override void EndProcessing() 12 | { 13 | WriteObject(CompleterGlobals.UnixUtilCompleter); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion/Commands/ImportPSUnixUtilCompletersCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Management.Automation; 4 | 5 | namespace Microsoft.PowerShell.UnixTabCompletion.Commands 6 | { 7 | [Cmdlet(VerbsData.Import, Utils.ModuleName)] 8 | public class ImportUnixTabCompletionCommand : PSCmdlet 9 | { 10 | protected override void EndProcessing() 11 | { 12 | // Do nothing here; this command does its job by autoloading 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion/Commands/RemovePSUnixUtilCompletersCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation; 3 | 4 | namespace Microsoft.PowerShell.UnixTabCompletion.Commands 5 | { 6 | [Cmdlet(VerbsCommon.Remove, Utils.ModuleName)] 7 | public class RemoveUnixTabCompletionCommand : PSCmdlet 8 | { 9 | protected override void EndProcessing() 10 | { 11 | InvokeCommand.InvokeScript("Remove-Module -Name Microsoft.PowerShell.UnixTabCompletion -Scope All -Force"); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion/Commands/SetPSUnixUtilCompletersCompleterCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation; 3 | 4 | namespace Microsoft.PowerShell.UnixTabCompletion.Commands 5 | { 6 | [Cmdlet(VerbsCommon.Set, Utils.ModuleName)] 7 | public class SetUnixTabCompletionCommand : Cmdlet 8 | { 9 | [ValidateNotNull()] 10 | [Parameter(Position = 0, Mandatory = true, ParameterSetName = "Completer")] 11 | public IUnixUtilCompleter Completer { get; set; } 12 | 13 | [ValidateNotNullOrEmpty()] 14 | [Parameter(Position = 0, Mandatory = true, ParameterSetName = "Shell")] 15 | public string Shell { get; set; } 16 | 17 | [ValidateNotNullOrEmpty()] 18 | [Parameter(Position = 0, Mandatory = true, ParameterSetName = "ShellType")] 19 | [Parameter(Position = 1, ParameterSetName = "Shell")] 20 | public ShellType ShellType { get; set; } 21 | 22 | // should this be a dynamic parameter? 23 | [Parameter(ParameterSetName = "ShellType")] 24 | public string CompletionScript { get; set; } 25 | 26 | protected override void EndProcessing() 27 | { 28 | if (Completer == null) 29 | { 30 | string shellName = Shell ?? ShellType.ToString().ToLower(); 31 | 32 | if (!UnixHelpers.TryFindShell(shellName, out string shellPath, out ShellType shellType)) 33 | { 34 | ThrowTerminatingError( 35 | new ErrorRecord( 36 | new ItemNotFoundException($"Unable to find shell '{shellName}'"), 37 | "CompletionShellNotFound", 38 | ErrorCategory.ObjectNotFound, 39 | shellName)); 40 | return; 41 | } 42 | 43 | // Allow a scenario where a shell is named one thing but behaves as another 44 | // and the user has manually specified what shell it behaves as 45 | if (ShellType != ShellType.None) 46 | { 47 | shellType = ShellType; 48 | } 49 | 50 | switch (shellType) 51 | { 52 | case ShellType.Zsh: 53 | Completer = new ZshUtilCompleter(shellPath); 54 | break; 55 | 56 | case ShellType.Bash: 57 | Completer = new BashUtilCompleter(shellPath, CompletionScript); 58 | break; 59 | 60 | default: 61 | ThrowTerminatingError( 62 | new ErrorRecord( 63 | new ArgumentException($"Unable to create a completer for shell type '{shellType}'"), 64 | "InvalidCompletionShellType", 65 | ErrorCategory.InvalidArgument, 66 | shellType)); 67 | return; 68 | } 69 | } 70 | 71 | CompleterGlobals.UnixUtilCompleter = Completer; 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion/Commands/Utils.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.PowerShell.UnixTabCompletion.Commands 2 | { 3 | internal static class Utils 4 | { 5 | public const string ModuleName = "PSUnixTabCompletion"; 6 | } 7 | } -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion/IUnixUtilCompleter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Management.Automation; 3 | using System.Management.Automation.Language; 4 | 5 | namespace Microsoft.PowerShell.UnixTabCompletion 6 | { 7 | /// 8 | /// Provides completions for native Unix commands. 9 | /// 10 | public interface IUnixUtilCompleter 11 | { 12 | /// 13 | /// Gets the list of commands this completer can generate completions for. 14 | /// 15 | /// The names of all commands this completer can generate completions for. 16 | IEnumerable FindCompletableCommands(); 17 | 18 | /// 19 | /// Complete a given Unix command. 20 | /// 21 | /// 22 | /// The name of the command to complete. 23 | /// Guaranteed to be one of the strings output by FindCompletableCommand(). 24 | /// 25 | /// The current word to complete. 26 | /// The whole command AST undergoing completion. 27 | /// The offset of the cursor from the start of input. 28 | /// A list of completions for the current word. 29 | IEnumerable CompleteCommand( 30 | string command, 31 | string wordToComplete, 32 | CommandAst commandAst, 33 | int cursorPosition); 34 | 35 | public string Name 36 | { 37 | get { return this.GetType().Name; } 38 | private set { } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion/Microsoft.PowerShell.UnixTabCompletion.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion/UnixHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace Microsoft.PowerShell.UnixTabCompletion 7 | { 8 | internal static class UnixHelpers 9 | { 10 | private readonly static IReadOnlyList s_nativeUtilDirs = new [] 11 | { 12 | "/usr/local/sbin", 13 | "/usr/local/bin", 14 | "/usr/sbin", 15 | "/usr/bin", 16 | "/sbin", 17 | "/bin" 18 | }; 19 | 20 | private readonly static IReadOnlyDictionary s_shells = new Dictionary() 21 | { 22 | { "zsh", ShellType.Zsh }, 23 | { "bash", ShellType.Bash }, 24 | }; 25 | 26 | private readonly static Lazy> s_nativeUtilNamesLazy = 27 | new Lazy>(GetNativeUtilNames); 28 | 29 | internal static IReadOnlyList NativeUtilDirs => s_nativeUtilDirs; 30 | 31 | internal static IReadOnlyList NativeUtilNames => s_nativeUtilNamesLazy.Value; 32 | 33 | internal static bool TryFindShell(string shellName, out string shellPath, out ShellType shellType) 34 | { 35 | // No shell name provided 36 | if (string.IsNullOrEmpty(shellName)) 37 | { 38 | shellPath = null; 39 | shellType = ShellType.None; 40 | return false; 41 | } 42 | 43 | // Look for absolute path to a shell 44 | if (Path.IsPathRooted(shellName) 45 | && s_shells.TryGetValue(Path.GetFileName(shellName), out shellType) 46 | && File.Exists(shellName)) 47 | { 48 | shellPath = shellName; 49 | return true; 50 | } 51 | 52 | // Now assume the shell is just a command name, and confirm we recognize it 53 | if (!s_shells.TryGetValue(shellName, out shellType)) 54 | { 55 | shellPath = null; 56 | return false; 57 | } 58 | 59 | return TryFindShellByName(shellName, out shellPath); 60 | } 61 | 62 | internal static bool TryFindFallbackShell(out string foundShell, out ShellType shellType) 63 | { 64 | foreach (KeyValuePair shell in s_shells) 65 | { 66 | if (TryFindShellByName(shell.Key, out foundShell)) 67 | { 68 | shellType = shell.Value; 69 | return true; 70 | } 71 | } 72 | 73 | foundShell = null; 74 | shellType = ShellType.None; 75 | return false; 76 | } 77 | 78 | private static bool TryFindShellByName(string shellName, out string foundShellPath) 79 | { 80 | foreach (string utilDir in UnixHelpers.NativeUtilDirs) 81 | { 82 | string shellPath = Path.Combine(utilDir, shellName); 83 | if (File.Exists(shellPath)) 84 | { 85 | foundShellPath = shellPath; 86 | return true; 87 | } 88 | } 89 | 90 | foundShellPath = null; 91 | return false; 92 | } 93 | 94 | 95 | private static IReadOnlyList GetNativeUtilNames() 96 | { 97 | var commandSet = new HashSet(StringComparer.Ordinal); 98 | foreach (string utilDir in s_nativeUtilDirs) 99 | { 100 | if (Directory.Exists(utilDir)) 101 | { 102 | foreach (string utilPath in Directory.GetFiles(utilDir)) 103 | { 104 | if (IsExecutable(utilPath)) 105 | { 106 | commandSet.Add(Path.GetFileName(utilPath)); 107 | } 108 | } 109 | } 110 | } 111 | var commandList = new List(commandSet); 112 | return commandList; 113 | } 114 | 115 | private static bool IsExecutable(string path) 116 | { 117 | return access(path, X_OK) != -1; 118 | } 119 | 120 | private const int X_OK = 0x01; 121 | 122 | [DllImport("libc")] 123 | private static extern int access(string pathname, int mode); 124 | } 125 | } -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion/UnixUtilCompletion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Management.Automation; 5 | using System.Management.Automation.Language; 6 | using System.Text; 7 | 8 | namespace Microsoft.PowerShell.UnixTabCompletion 9 | { 10 | public static class UnixUtilCompletion 11 | { 12 | private static string s_fullTypeName = typeof(UnixUtilCompletion).FullName; 13 | 14 | public static IEnumerable CompleteCommand( 15 | string command, 16 | string wordToComplete, 17 | CommandAst commandAst, 18 | int cursorPosition) 19 | { 20 | if (CompleterGlobals.UnixUtilCompleter == null) 21 | { 22 | return Enumerable.Empty(); 23 | } 24 | 25 | return CompleterGlobals.UnixUtilCompleter.CompleteCommand(command, wordToComplete, commandAst, cursorPosition); 26 | } 27 | 28 | internal static ScriptBlock CreateInvocationScriptBlock(string command) 29 | { 30 | string script = new StringBuilder(256) 31 | .Append("param($wordToComplete,$commandAst,$cursorPosition)[") 32 | .Append(s_fullTypeName) 33 | .Append("]::") 34 | .Append(nameof(CompleteCommand)) 35 | .Append("('") 36 | .Append(command) 37 | .Append("',$wordToComplete,$commandAst,$cursorPosition)") 38 | .ToString(); 39 | 40 | return ScriptBlock.Create(script); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion/UtilCompleterInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Management.Automation; 5 | using System.Management.Automation.Runspaces; 6 | using System.Reflection; 7 | using System.Runtime.InteropServices; 8 | using System.Text; 9 | using System.Threading; 10 | 11 | namespace Microsoft.PowerShell.UnixTabCompletion 12 | { 13 | public enum ShellType 14 | { 15 | None = 0, 16 | Zsh, 17 | Bash, 18 | } 19 | 20 | internal static class CompleterGlobals 21 | { 22 | private readonly static PropertyInfo s_executionContextProperty = typeof(Runspace).GetProperty("ExecutionContext", BindingFlags.NonPublic | BindingFlags.Instance); 23 | 24 | private readonly static PropertyInfo s_nativeArgumentCompletersProperty = s_executionContextProperty.PropertyType.GetProperty("NativeArgumentCompleters", BindingFlags.NonPublic | BindingFlags.Instance); 25 | 26 | private static Dictionary s_nativeArgumentCompleterTable; 27 | 28 | internal static IEnumerable CompletedCommands { get; set; } 29 | 30 | internal static Dictionary NativeArgumentCompleterTable 31 | { 32 | get 33 | { 34 | if (s_nativeArgumentCompleterTable == null) 35 | { 36 | object executionContext = s_executionContextProperty.GetValue(Runspace.DefaultRunspace); 37 | 38 | var completerTable = (Dictionary)s_nativeArgumentCompletersProperty.GetValue(executionContext); 39 | 40 | if (completerTable == null) 41 | { 42 | completerTable = new Dictionary(StringComparer.OrdinalIgnoreCase); 43 | s_nativeArgumentCompletersProperty.SetValue(executionContext, completerTable); 44 | } 45 | 46 | s_nativeArgumentCompleterTable = completerTable; 47 | } 48 | 49 | return s_nativeArgumentCompleterTable; 50 | } 51 | } 52 | 53 | internal static IUnixUtilCompleter UnixUtilCompleter { get; set; } 54 | } 55 | 56 | public class UtilCompleterInitializer : IModuleAssemblyInitializer 57 | { 58 | private const string SHELL_PREFERENCE_VARNAME = "COMPLETION_SHELL_PREFERENCE"; 59 | private const string BASH_COMPLETION_VARNAME = "BASH_COMPLETION"; 60 | 61 | public void OnImport() 62 | { 63 | string preferredCompletionShell = Environment.GetEnvironmentVariable(SHELL_PREFERENCE_VARNAME); 64 | string completionScript = Environment.GetEnvironmentVariable(BASH_COMPLETION_VARNAME); 65 | 66 | ShellType shellType = ShellType.None; 67 | string shellExePath; 68 | if ((string.IsNullOrEmpty(preferredCompletionShell) || !UnixHelpers.TryFindShell(preferredCompletionShell, out shellExePath, out shellType)) 69 | && !UnixHelpers.TryFindFallbackShell(out shellExePath, out shellType)) 70 | { 71 | WriteError("Unable to find shell to provide unix utility completions"); 72 | return; 73 | } 74 | 75 | IUnixUtilCompleter utilCompleter; 76 | switch (shellType) 77 | { 78 | case ShellType.Bash: 79 | utilCompleter = new BashUtilCompleter(shellExePath, completionScript); 80 | break; 81 | 82 | case ShellType.Zsh: 83 | utilCompleter = new ZshUtilCompleter(shellExePath); 84 | break; 85 | 86 | default: 87 | WriteError("Unable to find shell to provide unix utility completions"); 88 | return; 89 | } 90 | 91 | IEnumerable utilsToComplete = utilCompleter.FindCompletableCommands(); 92 | 93 | CompleterGlobals.CompletedCommands = utilsToComplete; 94 | CompleterGlobals.UnixUtilCompleter = utilCompleter; 95 | 96 | RegisterCompletersForCommands(utilsToComplete); 97 | } 98 | 99 | private void RegisterCompletersForCommands(IEnumerable commands) 100 | { 101 | foreach (string command in commands) 102 | { 103 | CompleterGlobals.NativeArgumentCompleterTable[command] = UnixUtilCompletion.CreateInvocationScriptBlock(command); 104 | } 105 | } 106 | 107 | private void WriteError(string errorMessage) 108 | { 109 | using (var pwsh = System.Management.Automation.PowerShell.Create()) 110 | { 111 | pwsh.AddCommand("Write-Error") 112 | .AddParameter("Message", errorMessage) 113 | .Invoke(); 114 | } 115 | } 116 | 117 | private static void OnRunspaceAvailable(object sender, RunspaceAvailabilityEventArgs args) 118 | { 119 | if (args.RunspaceAvailability != RunspaceAvailability.Available) 120 | { 121 | return; 122 | } 123 | 124 | var runspace = (Runspace)sender; 125 | 126 | runspace.SessionStateProxy.InvokeCommand.InvokeScript("Write-Host 'Hello'"); 127 | runspace.AvailabilityChanged -= OnRunspaceAvailable; 128 | } 129 | } 130 | 131 | public class UtilCompleterCleanup : IModuleAssemblyCleanup 132 | { 133 | public void OnRemove(PSModuleInfo psModuleInfo) 134 | { 135 | foreach (string completedCommand in CompleterGlobals.CompletedCommands) 136 | { 137 | CompleterGlobals.NativeArgumentCompleterTable.Remove(completedCommand); 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Microsoft.PowerShell.UnixTabCompletion/ZshUtilCompleter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Management.Automation; 6 | using System.Management.Automation.Language; 7 | using System.Reflection; 8 | using System.Text; 9 | 10 | namespace Microsoft.PowerShell.UnixTabCompletion 11 | { 12 | public class ZshUtilCompleter : IUnixUtilCompleter 13 | { 14 | private static readonly string s_completionScriptPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "zcomplete.sh"); 15 | 16 | private readonly string _zshPath; 17 | 18 | private readonly HashSet _seenCompletions; 19 | 20 | public ZshUtilCompleter(string zshPath) 21 | { 22 | _zshPath = zshPath; 23 | _seenCompletions = new HashSet(StringComparer.OrdinalIgnoreCase); 24 | } 25 | 26 | public IEnumerable FindCompletableCommands() 27 | { 28 | return UnixHelpers.NativeUtilNames; 29 | } 30 | 31 | public IEnumerable CompleteCommand( 32 | string command, 33 | string wordToComplete, 34 | CommandAst commandAst, 35 | int cursorPosition) 36 | { 37 | string zshArgs = CreateZshCompletionArgs(command, wordToComplete, commandAst, cursorPosition - commandAst.Extent.StartOffset); 38 | _seenCompletions.Clear(); 39 | foreach (string result in InvokeWithZsh(zshArgs).Split('\n')) 40 | { 41 | if (!TryGetCompletionFromResult(result, out string completionText, out string toolTip)) 42 | { 43 | continue; 44 | } 45 | 46 | string listItemText = completionText; 47 | 48 | // Deal with case sensitivity 49 | while (!_seenCompletions.Add(listItemText)) 50 | { 51 | listItemText += " "; 52 | } 53 | 54 | yield return new CompletionResult( 55 | completionText, 56 | listItemText, 57 | CompletionResultType.ParameterName, 58 | toolTip); 59 | } 60 | } 61 | 62 | private bool TryGetCompletionFromResult(string result, out string completionText, out string toolTip) 63 | { 64 | // The completer script sometimes has a bug 65 | // where it returns odd strings with VT100 escapes in it, 66 | // so just filter those out. 67 | if (string.IsNullOrEmpty(result) || result.Contains("\u001B[")) 68 | { 69 | completionText = null; 70 | toolTip = null; 71 | return false; 72 | } 73 | 74 | int spaceIndex = result.IndexOf(' '); 75 | 76 | if (spaceIndex < 0) 77 | { 78 | completionText = result.Trim(); 79 | toolTip = completionText; 80 | return true; 81 | } 82 | 83 | completionText = result.Substring(0, spaceIndex); 84 | 85 | int dashIndex = result.IndexOf("-- ", spaceIndex); 86 | 87 | toolTip = dashIndex < spaceIndex 88 | ? completionText 89 | : result.Substring(spaceIndex + 3); 90 | return true; 91 | } 92 | 93 | private string InvokeWithZsh(string arguments) 94 | { 95 | using (var zshProc = new Process()) 96 | { 97 | zshProc.StartInfo.RedirectStandardOutput = true; 98 | zshProc.StartInfo.RedirectStandardError = true; 99 | zshProc.StartInfo.FileName = this._zshPath; 100 | zshProc.StartInfo.Arguments = arguments; 101 | 102 | zshProc.Start(); 103 | 104 | return zshProc.StandardOutput.ReadToEnd(); 105 | } 106 | } 107 | 108 | private static string CreateZshCompletionArgs( 109 | string command, 110 | string wordToComplete, 111 | CommandAst commandAst, 112 | int cursorPosition) 113 | { 114 | string completionText; 115 | if (cursorPosition == commandAst.Extent.Text.Length) 116 | { 117 | completionText = commandAst.Extent.Text; 118 | } 119 | else if (cursorPosition > commandAst.Extent.Text.Length) 120 | { 121 | completionText = commandAst.Extent.Text + " "; 122 | } 123 | else 124 | { 125 | completionText = commandAst.Extent.Text.Substring(0, cursorPosition); 126 | } 127 | 128 | return new StringBuilder(s_completionScriptPath.Length + commandAst.Extent.Text.Length) 129 | .Append('"').Append(s_completionScriptPath).Append("\" ") 130 | .Append('"').Append(completionText.Replace("\"", "\"\"\"")).Append("\"") 131 | .ToString(); 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /OnStart.ps1: -------------------------------------------------------------------------------- 1 | # Informative messages to let users know that completers have been registered 2 | 3 | Write-Verbose "Registering UNIX native tab completion" 4 | 5 | # We don't have access to the module at load time, since loading occurs last 6 | # Instead we set up a one-time event to set the OnRemove scriptblock once the module has been loaded 7 | $null = Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -MaxTriggerCount 1 -Action { 8 | $m = Get-Module Microsoft.PowerShell.UnixTabCompletion 9 | $m.OnRemove = { 10 | Write-Verbose "Deregistering UNIX native tab completion" 11 | } 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft.PowerShell.UnixTabCompletion 2 | 3 | PowerShell parameter completers for native commands on Linux and macOS. 4 | 5 | This module uses completers supplied in traditional Unix shells 6 | to complete native utility parameters in PowerShell. 7 | 8 | ![Completions with apt example](completions.gif) 9 | 10 | Currently, this module supports completions from zsh and bash. 11 | By default it will look for zsh and then bash to run completions 12 | (since zsh's completions seem to be generally better). 13 | 14 | ## Basic usage 15 | 16 | To enable unix utility completions, 17 | install this module and add the following to your profile: 18 | 19 | ```powershell 20 | Import-Module Microsoft.PowerShell.UnixTabCompletion 21 | ``` 22 | 23 | There is also an alternate command, `Import-PSUnixTabCompletion`, 24 | that has the same functionality but is discoverable by command completion. 25 | 26 | This will register argument completers for all native commands 27 | found in the usual Unix util directories. 28 | 29 | Given the nature of native completion results, 30 | you may find this works best with PSReadLine's MenuComplete mode: 31 | 32 | ```powershell 33 | Import-Module PSUnixTabCompletion 34 | 35 | Set-PSReadLineKeyHandler -Key Tab -Function MenuComplete 36 | ``` 37 | 38 | ## Further configuration 39 | 40 | If you wish to set a preferred shell, you can do so by setting an environment variable: 41 | 42 | ```powershell 43 | $env:COMPLETION_SHELL_PREFERENCE = 'bash' 44 | 45 | # OR 46 | 47 | $env:COMPLETION_SHELL_PREFERENCE = '/bin/bash' 48 | 49 | Import-Module PSUnixTabCompletion 50 | ``` 51 | 52 | Note that you must do this before you load the module, 53 | and that setting it after loading will have no effect. 54 | 55 | If you want to change the completer after loading, 56 | you can do so from PowerShell like so: 57 | 58 | ```powershell 59 | Set-PSUnixTabCompletion -ShellType Zsh 60 | 61 | # Or if you have a shell installed to a particular path 62 | Set-PSUnixTabCompletion -Shell "/bin/zsh" 63 | 64 | # You can even write your own utility completer by implementing `IUnixUtilCompleter` 65 | $myCompleter = [MyCompleter]::new() 66 | Set-PSUnixTabCompletion -Completer $myCompleter 67 | ``` 68 | 69 | You can retrieve the current configuration with the `Get-PSUnixTabCompletion` cmdlet: 70 | 71 | ```powershell 72 | PS> Get-PSUnixTabCompletion 73 | 74 | Name 75 | ---- 76 | ZshUtilCompleter 77 | ``` 78 | 79 | ## Supporting different versions of Bash 80 | 81 | `bash` may have different tab completion depending on the version and system. 82 | To provide greater flexibility, setting `bash` tab completion via `Set-PSUnixTabCompletion` supports an additional parameter. 83 | If your completion script is in an alternative location, 84 | you may provide the location of the completion script as a parameter to `Set-PSUnixTabCompletion`. 85 | By default, this value is set to `/usr/share/bash-completion/bash_completion` which should work for most Linux systems. 86 | This is roughly equivalent to the `source /usr/share/bash-completion/bash_completion` which may be needed by MacOS. 87 | 88 | ```powershell 89 | Set-PSUnixTabCompletion -ShellType Bash -CompletionScript /usr/local/etc/bash_completion 90 | ``` 91 | 92 | When the shell is set to bash, `Get-PSUnixTabCompletion` will return the completion script. 93 | 94 | ```powershell 95 | [UnixCompleters-1|main↑0↓0•0+1?2] 🐚> Set-PSUnixTabCompletion -ShellType Bash -CompletionScript /usr/local/etc/bash_completion 96 | [UnixCompleters-1|main↑0↓0•0+1?2] 🐚> Get-PSUnixTabCompletion 97 | 98 | CompletionScript Name 99 | ---------------- ---- 100 | /usr/local/etc/bash_completion BashUtilCompleter 101 | ``` 102 | 103 | ## Unregistering UNIX util completions 104 | 105 | The Microsoft.PowerShell.UnixTabCompletion module will unregister completers 106 | for all the commands it registered completers for 107 | when removed: 108 | 109 | ```powershell 110 | Remove-Module Microsoft.PowerShell.PSUnixTabCompletion 111 | ``` 112 | 113 | As with loading, there is also a convenience command provided for this: 114 | 115 | ```powershell 116 | Remove-PSUnixTabCompletion 117 | ``` 118 | 119 | ## Building the module yourself 120 | 121 | Microsoft.PowerShell.UnixTabCompletion comes with a PowerShell build script, 122 | which you can invoke to build the module yourself with: 123 | 124 | ```powershell 125 | ./build.ps1 -Clean 126 | ``` 127 | 128 | This will output the built module to `out/Microsoft.PowerShell.UnixTabCompletion`. 129 | 130 | ## Credits 131 | 132 | All the zsh completions provided by this module are made possible 133 | by the work of [@Valodim](https://github.com/Valodim)'s zsh completion project, 134 | [zsh-capture-completion](https://github.com/Valodim/zsh-capture-completion), 135 | which this module invokes to get completion results. 136 | 137 | The bash completions provided by this module are adapted from the work 138 | done by [@mikebattista](https://github.com/mikebattista) for his 139 | [PowerShell-WSL-interop](https://github.com/mikebattista/PowerShell-WSL-Interop) PowerShell module. 140 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | #requires -Version 6.0 2 | 3 | [CmdletBinding(DefaultParameterSetName = 'Build')] 4 | param( 5 | [Parameter(ParameterSetName = 'Build')] 6 | [ValidateSet('Debug', 'Release')] 7 | [string] 8 | $Configuration = 'Debug', 9 | 10 | [Parameter(ParameterSetName = 'Build')] 11 | [switch] 12 | $Clean, 13 | 14 | [Parameter(ParameterSetName = 'Package')] 15 | [switch] 16 | $Package, 17 | 18 | [Parameter(ParameterSetName = 'Package')] 19 | [switch] 20 | $Signed, 21 | 22 | [Parameter(ParameterSetName = 'Test')] 23 | [switch] 24 | $Build, 25 | 26 | [Parameter(ParameterSetName = 'Test')] 27 | [switch] 28 | $Test 29 | ) 30 | 31 | $ErrorActionPreference = 'Stop' 32 | 33 | $script:ModuleName = 'Microsoft.PowerShell.UnixTabCompletion' 34 | $script:ModuleVersion = (Import-PowerShellDataFile -Path "${PSScriptRoot}/${ModuleName}.psd1").ModuleVersion 35 | $script:OutDir = "${PSScriptRoot}/out" 36 | $script:ModuleBase = "${PSScriptRoot}/out/${script:ModuleName}" 37 | $script:OutModuleDir = "${ModuleBase}/${script:ModuleVersion}" 38 | $script:SrcDir = "$PSScriptRoot/${ModuleName}" 39 | $script:Framework = 'netstandard2.1' 40 | $script:ZshCompleterScriptLocation = "${script:OutModuleDir}/zcomplete.sh" 41 | 42 | $script:Artifacts = @{ 43 | "OnStart.ps1" = "OnStart.ps1" 44 | "${script:ModuleName}.psd1" = "${script:ModuleName}.psd1" 45 | "${ModuleName}/bin/$Configuration/${script:Framework}/${ModuleName}.dll" = "${ModuleName}.dll" 46 | "LICENSE" = "LICENSE.txt" 47 | } 48 | if ( $Configuration -eq 'Debug' ) { 49 | ${script:Artifacts}["${ModuleName}/bin/$Configuration/${script:Framework}/${ModuleName}.pdb"] = "${ModuleName}.pdb" 50 | } 51 | 52 | function Exec([scriptblock]$sb, [switch]$IgnoreExitcode) 53 | { 54 | $backupEAP = $script:ErrorActionPreference 55 | $script:ErrorActionPreference = "Continue" 56 | try 57 | { 58 | & $sb 59 | # note, if $sb doesn't have a native invocation, $LASTEXITCODE will 60 | # point to the obsolete value 61 | if ($LASTEXITCODE -ne 0 -and -not $IgnoreExitcode) 62 | { 63 | throw "Execution of {$sb} failed with exit code $LASTEXITCODE" 64 | } 65 | } 66 | finally 67 | { 68 | $script:ErrorActionPreference = $backupEAP 69 | } 70 | } 71 | 72 | if ($PSCmdlet.ParameterSetName -eq 'Build' -or $Build) 73 | { 74 | try 75 | { 76 | $null = Get-Command dotnet -ErrorAction Stop 77 | } 78 | catch 79 | { 80 | throw 'Unable to find dotnet executable' 81 | } 82 | 83 | if ($Clean) 84 | { 85 | foreach ($path in $script:OutDir,"${script:SrcDir}/bin","${script:SrcDir}/obj") 86 | { 87 | if (Test-Path -Path $path) 88 | { 89 | Remove-Item -Force -Recurse -Path $path -ErrorAction Stop 90 | } 91 | } 92 | } 93 | 94 | Push-Location $script:SrcDir 95 | try 96 | { 97 | Exec { dotnet build --configuration $Configuration } 98 | } 99 | finally 100 | { 101 | Pop-Location 102 | } 103 | 104 | New-Item -ItemType Directory -Path $script:OutModuleDir -ErrorAction SilentlyContinue 105 | 106 | foreach ($artifactEntry in $script:Artifacts.GetEnumerator()) 107 | { 108 | Copy-Item -Path $artifactEntry.Key -Destination (Join-Path $script:OutModuleDir $artifactEntry.Value) -ErrorAction Stop 109 | } 110 | 111 | # We need the zsh completer script to drive zsh completions 112 | Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/Valodim/zsh-capture-completion/master/capture.zsh' -OutFile $script:ZshCompleterScriptLocation 113 | } 114 | 115 | if ($Test) { 116 | $pwsh = (Get-Process -Id $PID).Path 117 | $testPath = "$PSScriptRoot/tests" 118 | & $pwsh -noprofile -c "Import-Module Pester; Invoke-Pester -output Detailed -CI '$testPath'" 119 | } 120 | 121 | # if we're signed, the files must be in 'signed' rather than 'out' directory 122 | if ($Package) { 123 | $packagePath = "$PSScriptRoot/packages" 124 | $repoName = [guid]::NewGuid().ToString("N") 125 | if ( $Signed ) { 126 | $moduleLocation = "$psScriptRoot/signed/${script:ModuleName}/${script:ModuleVersion}" 127 | } 128 | else { 129 | $moduleLocation = ${script:ModuleBase} 130 | } 131 | if (-not (Test-Path $packagePath)) { 132 | $null = New-Item -ItemType Directory -Path $packagePath 133 | } 134 | try { 135 | Register-PSRepository -Name $repoName -SourceLocation $packagePath -PublishLocation $packagePath -InstallationPolicy Trusted 136 | Publish-Module -Path "${moduleLocation}" -Repository $repoName 137 | } 138 | finally { 139 | Unregister-PSRepository -Name $repoName 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /completions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PowerShell/UnixCompleters/24dceabc0cbea8a2f5742063db81a910674e0e0a/completions.gif -------------------------------------------------------------------------------- /tests/Completion.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Microsoft.PowerShell.UnixTabCompletion completion tests' { 2 | BeforeDiscovery { 3 | Import-Module "$PSScriptRoot/../out/Microsoft.PowerShell.UnixTabCompletion" 4 | $zsh = Get-Command -ErrorAction Ignore zsh 5 | $bsh = Get-Command -ErrorAction Ignore /bin/bash 6 | $skipZsh = $null -eq $zsh ? $true : $false 7 | $skipBsh = $null -eq $bsh ? $true : $false 8 | if ( $skipZsh ) { $zsh = Get-Command Write-OutPut } 9 | if ( $skipBsh ) { $bsh = Get-Command Write-Output } 10 | } 11 | 12 | Context "Script Analyzer" { 13 | It "There should be no script analyzer violations" { 14 | $result = Invoke-ScriptAnalyzer -Recurse -Path "$PSScriptRoot/.." 15 | $result | Should -BeNullOrEmpty 16 | } 17 | } 18 | 19 | Context "Bash completions" { 20 | BeforeDiscovery { 21 | if ( $skipBsh ) { 22 | $completionTestCases = @( 23 | @{ InStr = 'gzip --'; CursorPos = 7; Suggestions = "n/a" } 24 | @{ InStr = 'dd i'; CursorPos = 4; Suggestions = "n/a" } 25 | ) 26 | return 27 | } 28 | if (Test-Path /etc/bash_completion) { 29 | $bashCompletionScript = "/etc/bash_completion" 30 | } 31 | else { 32 | $bashCompletionScript = "/usr/local/etc/bash_completion" 33 | } 34 | $bcomp = "$PSScriptRoot/bash-completer" 35 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] 36 | $completionTestCases = @( 37 | @{ InStr = 'gzip --'; CursorPos = 7; Suggestions = (& $bsh -c "$bcomp $bashCompletionScript 'gzip --'") } 38 | @{ InStr = 'dd i'; CursorPos = 4; Suggestions = (& $bsh -c "$bcomp $bashCompletionScript 'dd i'") } 39 | ) 40 | Set-PSUnixTabCompletion -ShellType Bash -CompletionScript $bashCompletionScript 41 | } 42 | 43 | It "Completes correctly" -foreach $completionTestCases -skip:$skipBsh { 44 | # param($InStr, $CursorPos, $Suggestions) 45 | 46 | $result = TabExpansion2 -inputScript $InStr -cursorColumn $CursorPos 47 | 48 | foreach ($s in $Suggestions) 49 | { 50 | $result.CompletionMatches.CompletionText | Should -Contain $s 51 | } 52 | } 53 | } 54 | 55 | Context "Zsh completions" { 56 | BeforeDiscovery { 57 | if ( $skipZsh ) { 58 | $completionTestCases = @( 59 | @{ InStr = 'ls -a'; CursorPos = 5; Suggestions = "n/a" } 60 | @{ InStr = 'grep --'; CursorPos = 7; Suggestions = "n/a" } 61 | @{ InStr = 'dd i'; CursorPos = 4; Suggestions = "n/a" } 62 | @{ InStr = 'cat -'; CursorPos = 5; Suggestions = "n/a" } 63 | @{ InStr = 'ps au'; CursorPos = 5; Suggestions = "n/a" } 64 | ) 65 | return 66 | } 67 | $moduleVersion = (Get-Module Microsoft.PowerShell.UnixTabCompletion).Version.ToString() 68 | $zcomp = "$PSScriptRoot/../out/Microsoft.PowerShell.UnixTabCompletion/${moduleVersion}/zcomplete.sh" 69 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] 70 | $completionTestCases = @( 71 | @{ InStr = 'ls -a'; CursorPos = 5; Suggestions = (& $zsh $zcomp 'ls -a').where({"$_" -match ' -- '}).foreach({"$_".Split(' ')[0]}) } 72 | @{ InStr = 'grep --'; CursorPos = 7; Suggestions = (& $zsh $zcomp 'grep --').where({"$_" -match ' -- '}).foreach({"$_".Split(' ')[0]}) } 73 | @{ InStr = 'dd i'; CursorPos = 4; Suggestions = (& $zsh $zcomp 'dd i').where({"$_" -match ' -- '}).foreach({"$_".Split(' ')[0]}) } 74 | @{ InStr = 'cat -'; CursorPos = 5; Suggestions = (& $zsh $zcomp 'cat -').where({"$_" -match ' -- '}).foreach({"$_".Split(' ')[0]}) } 75 | @{ InStr = 'ps au'; CursorPos = 5; Suggestions = (& $zsh $zcomp 'ps au').where({"$_" -match ' -- '}).foreach({"$_".Split(' ')[0]}) } 76 | ) 77 | Set-PSUnixTabCompletion -ShellType Zsh 78 | } 79 | 80 | It "Completes '' correctly" -foreach $completionTestCases -skip:$skipZsh { 81 | # param($InStr, $CursorPos, $Suggestions) 82 | 83 | $result = TabExpansion2 -inputScript $InStr -cursorColumn $CursorPos 84 | 85 | foreach ($s in $Suggestions) 86 | { 87 | $result.CompletionMatches.CompletionText | Should -Contain $s 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/bash-completer: -------------------------------------------------------------------------------- 1 | #!/bin/bash -l 2 | 3 | FindCompletionFunction() { 4 | complete -p "$1" 2>/dev/null | awk '{print $(NF-1)}' 5 | } 6 | 7 | if [ $# != 2 ]; then 8 | echo "need completion script and line to complete" 9 | exit 1 10 | fi 11 | # set up variables for completions 12 | # the completion script 13 | completionScript=$1 14 | if [ ! -f $completionScript ]; then 15 | echo "$completionScript not found" 16 | exit 1 17 | fi 18 | shift 19 | declare -a COMPREPLY 20 | COMP_LINE=$* 21 | COMP_POINT=${#COMP_LINE} 22 | eval set -- "$@" 23 | COMP_WORDS=($@) 24 | [[ "${COMP_LINE[@]: -1}" = " " ]] && COMP_WORDS+=('') 25 | COMP_CWORD=$(( ${#COMP_WORDS[@]} - 1 )) 26 | 27 | #echo ">>${COMP_LINE}::" 28 | #echo ">>${COMP_POINT}::" 29 | #echo "%%${#COMP_WORDS[@]}%%" >&2 30 | #echo "^^${COMP_CWORD[@]}^^" >&2 31 | #printf '**%s**\n' "${COMP_WORDS[@]}" >&2 32 | 33 | # determine the completer 34 | #source /usr/local/etc/bash_completion 35 | #source /usr/share/bash-completion/bash_completion 36 | source $completionScript 37 | completion=$(FindCompletionFunction ${COMP_WORDS[0]}) 38 | #echo ">>${completion}<<" 39 | 40 | # not found, try to load it 41 | if [[ -z $completion ]] 42 | then 43 | _completion_loader "$1" 2>/dev/null 44 | # try to find it again 45 | completion=$(FindCompletionFunction ${COMP_WORDS[0]}) 46 | fi 47 | 48 | # ensure completion was detected 49 | if [[ -z $completion ]] 50 | then 51 | exit 1 52 | fi 53 | 54 | # execute completion function 55 | "$completion" 56 | #set -x 57 | #echo ">> $completion" >&2 58 | # print completions to stdout 59 | #echo "${COMPREPLY[@]}"|sort -u 60 | #'s/.*\(--[-A-Za-z0-9]\{1,\}=\{0,1\}\).*/\1/p' 61 | #'s/.*\(--[-A-Za-z0-9]\{1,\}\).*/\1/p' 62 | if [[ ${#COMPREPLY[@]} = 1 && ${COMPREPLY[0]} = "--help" ]] 63 | then 64 | COMPREPLY=($("${COMP_WORDS[0]}" "${COMPREPLY[0]}" 2>&1|sed -ne 's/.*\(--[-A-Za-z0-9]\{1,\}=\{0,1\}\).*/\1/p' |sort -u)) 65 | fi 66 | # try again to get completions 67 | if [ ${#COMPREPLY} -eq 0 ] 68 | then 69 | COMPREPLY=($(compgen -W '$(_parse_help ${COMP_WORDS[0]})' -- "${COMP_WORDS[1]}")) 70 | fi 71 | printf '%s\n' "${COMPREPLY[@]}"|sort -u 72 | --------------------------------------------------------------------------------