├── .github ├── CODEOWNERS └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── LICENSE ├── README.md ├── WslInterop.Publish.ps1 ├── WslInterop.Tests.ps1 ├── WslInterop.psd1 └── WslInterop.psm1 /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mikebattista -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mike Battista 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerShell WSL Interop 2 | 3 | The [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/about) enables calling Linux commands directly within PowerShell via `wsl.exe` (e.g. `wsl ls`). While more convenient than a full context switch into WSL, it has the following limitations: 4 | 5 | * Prefixing commands with `wsl` is tedious and unnatural 6 | * Windows paths passed as arguments don't often resolve due to backslashes being interpreted as escape characters rather than directory separators 7 | * Windows paths passed as arguments don't often resolve due to not being translated to the appropriate mount point within WSL 8 | * Arguments with special characters (e.g. regular expressions) are often misinterpreted without unnatural embedded quotes or escape sequences 9 | * Default parameters defined in WSL login profiles with aliases and environment variables aren’t honored 10 | * Linux path completion is not supported 11 | * Command completion is not supported 12 | * Argument completion is not supported 13 | 14 | The `Import-WslCommand` function addresses these issues in the following ways: 15 | 16 | * By creating PowerShell function wrappers for commands, prefixing them with `wsl` is no longer necessary 17 | * By identifying path arguments and converting them to WSL paths, path resolution is natural and intuitive as it translates seamlessly between Windows and WSL paths 18 | * By formatting arguments with special characters, arguments like regular expressions can be provided naturally 19 | * Default parameters are supported by `$WslDefaultParameterValues` similar to `$PSDefaultParameterValues` 20 | * Environment variables are supported by `$WslEnvironmentVariables` 21 | * Command completion is enabled by PowerShell's command completion 22 | * Argument completion is enabled by registering an `ArgumentCompleter` that shims bash's programmable completion 23 | 24 | The commands can receive both pipeline input as well as their corresponding arguments just as if they were native to Windows. 25 | 26 | Additionally, they will honor any default parameters defined in a hash table called `$WslDefaultParameterValues` similar to `$PSDefaultParameterValues`. For example: 27 | 28 | ```powershell 29 | $WslDefaultParameterValues["grep"] = "-E" 30 | $WslDefaultParameterValues["less"] = "-i" 31 | $WslDefaultParameterValues["ls"] = "-AFh --group-directories-first" 32 | ``` 33 | 34 | If you use aliases or environment variables within your login profiles to set default parameters for commands, define a hash table called `$WslDefaultParameterValues` within 35 | your PowerShell profile and populate it as above for a similar experience. 36 | 37 | Environment variables can also be set in a hash table called `$WslEnvironmentVariables` using the pattern `$WslEnvironmentVariables[""] = ""`. 38 | 39 | The import of these functions replaces any PowerShell aliases that conflict with the commands. 40 | 41 | ## Usage 42 | 43 | * Install [PowerShell Core](https://github.com/powershell/powershell#get-powershell) 44 | * Install the [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install-win10) 45 | * Ensure the `bash-completion` package is installed with `sudo apt install bash-completion` or equivalent command 46 | * Install the WslInterop module with `Install-Module WslInterop` 47 | * Import commands with `Import-WslCommand` 48 | * `Import-WslCommand "apt", "awk", "emacs", "find", "grep", "head", "less", "ls", "man", "sed", "seq", "ssh", "sudo", "tail", "touch", "vim"` for example 49 | * Add this to your [profile](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles) for persistent access 50 | * (Optionally) Define a [hash table](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_hash_tables?view=powershell-6#creating-hash-tables) called `$WslDefaultParameterValues` and set default arguments for commands using the following patterns: 51 | * `$WslDefaultParameterValues[""] = ""` 52 | * `` will be passed as default arguments to `` 53 | * `$WslDefaultParameterValues[""] = { }` 54 | * `` will be executed at runtime to determine the default arguments for `` 55 | * `$WslDefaultParameterValues["-d"] = ""` 56 | * The distribution WSL uses will be changed by setting `wsl -d ` 57 | * `$WslDefaultParameterValues["-u"] = ""` 58 | * The username WSL uses will be changed by setting `wsl -u ` 59 | * `$WslDefaultParameterValues["--shell-type"] = ""` 60 | * The shell type WSL uses will be changed by setting `wsl --shell-type ` where `` is either `standard`, `login`, or `none` 61 | * (Optionally) Define a [hash table](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_hash_tables?view=powershell-6#creating-hash-tables) called `$WslEnvironmentVariables` and set environment variables using the pattern `$WslEnvironmentVariables[""] = ""` or use [WSLENV](https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/) 62 | 63 | ## Known Issues 64 | 65 | * Windows PowerShell is not supported. [PowerShell Core](https://github.com/powershell/powershell#get-powershell) is required. 66 | -------------------------------------------------------------------------------- /WslInterop.Publish.ps1: -------------------------------------------------------------------------------- 1 | $module = Test-ModuleManifest .\*.psd1 2 | $moduleName = $module.Name 3 | $moduleVersion = $module.Version.ToString() 4 | $modulePath = "$Env:TEMP\$(New-Guid)\$moduleName" 5 | 6 | [version]::new($moduleVersion).CompareTo([version]::new((Find-PSResource $moduleName).Version)) | Should -Be 1 7 | 8 | New-Item $modulePath -ItemType Directory -Force | Out-Null 9 | Copy-Item .\* $modulePath -Recurse -Exclude .github, *.Publish.ps1, *.Tests.ps1 10 | Get-ChildItem $modulePath -Force | Select-Object -ExpandProperty Name | Should -BeExactly "LICENSE", "README.md", "WslInterop.psd1", "WslInterop.psm1" 11 | 12 | Publish-PSResource -Path $modulePath -Repository PSGallery -ApiKey $args[0] -Confirm 13 | 14 | Remove-Item $modulePath -Recurse -Force -ErrorAction Ignore -------------------------------------------------------------------------------- /WslInterop.Tests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\WslInterop.psd1 -Force 2 | 3 | Describe "Import-WslCommand" { 4 | It "Creates function wrappers and removes any conflicting aliases." -TestCases @( 5 | @{command = 'awk'}, 6 | @{command = 'emacs'}, 7 | @{command = 'grep'}, 8 | @{command = 'head'}, 9 | @{command = 'less'}, 10 | @{command = 'ls'}, 11 | @{command = 'man'}, 12 | @{command = 'sed'}, 13 | @{command = 'seq'}, 14 | @{command = 'ssh'}, 15 | @{command = 'tail'}, 16 | @{command = 'vim'} 17 | ) { 18 | param([string]$command) 19 | 20 | Set-Alias $command help -Scope Global -Force -ErrorAction Ignore 21 | 22 | Import-WslCommand $command 23 | 24 | Get-Command $command | Select-Object -ExpandProperty CommandType | Should -BeExactly "Function" 25 | } 26 | 27 | It "Enables calling commands with arbitrary arguments." -TestCases @( 28 | @{command = 'seq'; arguments = '0 10' -split ' '; expectedResult = '0 1 2 3 4 5 6 7 8 9 10' -split ' '}, 29 | @{command = 'seq'; arguments = '0 2 10' -split ' '; expectedResult = '0 2 4 6 8 10' -split ' '}, 30 | @{command = 'seq'; arguments = '-s - 0 2 10' -split ' '; expectedResult = '0-2-4-6-8-10' -split ' '} 31 | ) { 32 | param([string]$command, [string[]]$arguments, [string[]]$expectedResult) 33 | 34 | Import-WslCommand $command 35 | 36 | & $command @arguments | Should -BeExactly $expectedResult 37 | } 38 | 39 | It "Enables calling commands with default arguments." -TestCases @( 40 | @{command = 'seq'; arguments = '0 10' -split ' '; expectedResult = '0-1-2-3-4-5-6-7-8-9-10'}, 41 | @{command = 'seq'; arguments = '0 2 10' -split ' '; expectedResult = '0-2-4-6-8-10'}, 42 | @{command = 'seq'; arguments = '-s : 0 2 10' -split ' '; expectedResult = '0:2:4:6:8:10'} 43 | ) { 44 | param([string]$command, [string[]]$arguments, [string]$expectedResult) 45 | 46 | Import-WslCommand $command 47 | 48 | Set-Variable WslDefaultParameterValues @{seq = "-s -"} -Scope Global 49 | 50 | & $command @arguments | Should -BeExactly $expectedResult 51 | 52 | Remove-Variable WslDefaultParameterValues -Scope Global 53 | } 54 | 55 | It "Enables calling commands that honor environment variables." -TestCases @( 56 | @{command = 'grep'; arguments = 'input' -split ' '; expectedResult = '1'} 57 | ) { 58 | param([string]$command, [string[]]$arguments, [string]$expectedResult) 59 | 60 | Import-WslCommand $command 61 | 62 | Set-Variable WslEnvironmentVariables @{GREP_OPTIONS = "-c"} -Scope Global 63 | 64 | "input" | & $command @arguments 2> $null | Should -BeExactly $expectedResult 65 | 66 | Remove-Variable WslEnvironmentVariables -Scope Global 67 | } 68 | 69 | It "Enables resolving Windows paths." -TestCases @( 70 | @{command = 'ls'; arguments = 'C:\Windows'; failureResult = 'ls: cannot access ''C:Windows''*'}, 71 | @{command = 'ls'; arguments = 'C:\Windows'; failureResult = 'ls: cannot access ''C:/Windows''*'}, 72 | @{command = 'ls'; arguments = 'C:\Win*'; failureResult = 'ls: cannot access ''C:Win*''*'}, 73 | @{command = 'ls'; arguments = 'C:\Win*'; failureResult = 'ls: cannot access ''C:/Win*''*'}, 74 | @{command = 'ls'; arguments = '/mnt/c/Program Files (x86)'; failureResult = 'ls: cannot access ''/mnt/c/Program''*'} 75 | @{command = 'ls'; arguments = '.\.github'; failureResult = 'ls: cannot access ''..github''*'}, 76 | @{command = 'ls'; arguments = '.githu*'; failureResult = 'ls: cannot access ''.githu*''*'} 77 | @{command = 'ls'; arguments = '.githu?'; failureResult = 'ls: cannot access ''.githu?''*'} 78 | @{command = 'ls'; arguments = '.githu[abc]'; failureResult = 'ls: cannot access ''.githu[abc]''*'} 79 | @{command = 'ls'; arguments = '.githu[a/b]'; failureResult = 'Test-Path : Cannot retrieve the dynamic parameters for the cmdlet. The specified wildcard character pattern is not valid*'} 80 | @{command = 'ls'; arguments = '.githu[a\b]'; failureResult = 'Test-Path : Cannot retrieve the dynamic parameters for the cmdlet. The specified wildcard character pattern is not valid*'} 81 | ) { 82 | param([string]$command, [string[]]$arguments, [string]$failureResult) 83 | 84 | Import-WslCommand $command 85 | 86 | & $command @arguments 2>&1 | Should -Not -BeLike $failureResult 87 | } 88 | } 89 | 90 | Describe "Format-WslArgument" { 91 | It "Escapes special characters in when interactive is ." -TestCases @( 92 | @{arg = '/mnt/c/Windows'; interactive = $true; expectedResult = '/mnt/c/Windows'} 93 | @{arg = '/mnt/c/Windows'; interactive = $false; expectedResult = '/mnt/c/Windows'} 94 | @{arg = '/mnt/c/Windows '; interactive = $true; expectedResult = '/mnt/c/Windows'} 95 | @{arg = '/mnt/c/Windows '; interactive = $false; expectedResult = '/mnt/c/Windows'} 96 | @{arg = '/mnt/c/Program Files (x86)'; interactive = $true; expectedResult = '''/mnt/c/Program Files (x86)'''} 97 | @{arg = '/mnt/c/Program Files (x86)'; interactive = $false; expectedResult = '/mnt/c/Program\ Files\ \(x86\)'} 98 | @{arg = '/mnt/c/Program Files (x86) '; interactive = $true; expectedResult = '''/mnt/c/Program Files (x86)'''} 99 | @{arg = '/mnt/c/Program Files (x86) '; interactive = $false; expectedResult = '/mnt/c/Program\ Files\ \(x86\)'} 100 | @{arg = './Windows'; interactive = $true; expectedResult = './Windows'} 101 | @{arg = './Windows'; interactive = $false; expectedResult = './Windows'} 102 | @{arg = './Windows '; interactive = $true; expectedResult = './Windows'} 103 | @{arg = './Windows '; interactive = $false; expectedResult = './Windows'} 104 | @{arg = './Program Files (x86)'; interactive = $true; expectedResult = '''./Program Files (x86)'''} 105 | @{arg = './Program Files (x86)'; interactive = $false; expectedResult = './Program\ Files\ \(x86\)'} 106 | @{arg = './Program Files (x86) '; interactive = $true; expectedResult = '''./Program Files (x86)'''} 107 | @{arg = './Program Files (x86) '; interactive = $false; expectedResult = './Program\ Files\ \(x86\)'} 108 | @{arg = '~/.bashrc'; interactive = $true; expectedResult = '~/.bashrc'} 109 | @{arg = '~/.bashrc'; interactive = $false; expectedResult = '~/.bashrc'} 110 | @{arg = '~/.bashrc '; interactive = $true; expectedResult = '~/.bashrc'} 111 | @{arg = '~/.bashrc '; interactive = $false; expectedResult = '~/.bashrc'} 112 | @{arg = '/usr/share/bash-completion/bash_completion'; interactive = $true; expectedResult = '/usr/share/bash-completion/bash_completion'} 113 | @{arg = '/usr/share/bash-completion/bash_completion'; interactive = $false; expectedResult = '/usr/share/bash-completion/bash_completion'} 114 | @{arg = '/usr/share/bash-completion/bash_completion '; interactive = $true; expectedResult = '/usr/share/bash-completion/bash_completion'} 115 | @{arg = '/usr/share/bash-completion/bash_completion '; interactive = $false; expectedResult = '/usr/share/bash-completion/bash_completion'} 116 | @{arg = 's/;/\n/g'; interactive = $true; expectedResult = 's/`;/\n/g'} 117 | @{arg = 's/;/\n/g'; interactive = $false; expectedResult = 's/\;/\\n/g'} 118 | @{arg = '"s/;/\n/g"'; interactive = $true; expectedResult = '"s/;/\n/g"'} 119 | @{arg = '"s/;/\n/g"'; interactive = $false; expectedResult = '"s/;/\n/g"'} 120 | @{arg = '''s/;/\n/g'''; interactive = $true; expectedResult = '''s/;/\n/g'''} 121 | @{arg = '''s/;/\n/g'''; interactive = $false; expectedResult = '''s/;/\n/g'''} 122 | @{arg = '^(a|b)\w+\1'; interactive = $true; expectedResult = '^`(a`|b`)\w+\1'} 123 | @{arg = '^(a|b)\w+\1'; interactive = $false; expectedResult = '^\(a\|b\)\\w+\\1'} 124 | @{arg = '"^(a|b)\w+\1"'; interactive = $true; expectedResult = '"^(a|b)\w+\1"'} 125 | @{arg = '"^(a|b)\w+\1"'; interactive = $false; expectedResult = '"^(a|b)\w+\1"'} 126 | @{arg = '''^(a|b)\w+\1'''; interactive = $true; expectedResult = '''^(a|b)\w+\1'''} 127 | @{arg = '''^(a|b)\w+\1'''; interactive = $false; expectedResult = '''^(a|b)\w+\1'''} 128 | @{arg = '[aeiou]{2,}'; interactive = $true; expectedResult = '[aeiou]`{2`,`}'} 129 | @{arg = '[aeiou]{2,}'; interactive = $false; expectedResult = '[aeiou]\{2\,\}'} 130 | @{arg = '[[:digit:]]{2,}'; interactive = $true; expectedResult = '[[:digit:]]`{2`,`}'} 131 | @{arg = '[[:digit:]]{2,}'; interactive = $false; expectedResult = '[[:digit:]]\{2\,\}'} 132 | @{arg = '^foo(.*?)bar$'; interactive = $true; expectedResult = '^foo`(.*?`)bar$'} 133 | @{arg = '^foo(.*?)bar$'; interactive = $false; expectedResult = '^foo\(.*?\)bar$'} 134 | @{arg = '\^foo\.\*\?bar\$'; interactive = $true; expectedResult = '\^foo\.\*\?bar\$'} 135 | @{arg = '\^foo\.\*\?bar\$'; interactive = $false; expectedResult = '\\^foo\\.\\*\\?bar\\$'} 136 | @{arg = '\\\\\w'; interactive = $true; expectedResult = '\\\\\w'} 137 | @{arg = '\\\\\w'; interactive = $false; expectedResult = '\\\\\\\\\\w'} 138 | @{arg = '\\\\([^\\]+)'; interactive = $true; expectedResult = '\\\\`([^\\]+`)'} 139 | @{arg = '\\\\([^\\]+)'; interactive = $false; expectedResult = '\\\\\\\\\([^\\\\]+\)'} 140 | @{arg = '(\\\\[^\\]+)'; interactive = $true; expectedResult = '`(\\\\[^\\]+`)'} 141 | @{arg = '(\\\\[^\\]+)'; interactive = $false; expectedResult = '\(\\\\\\\\[^\\\\]+\)'} 142 | @{arg = '\(\)'; interactive = $true; expectedResult = '\`(\`)'} 143 | @{arg = '\(\)'; interactive = $false; expectedResult = '\\\(\\\)'} 144 | @{arg = '\a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z'; interactive = $true; expectedResult = '\a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z'} 145 | @{arg = '\a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z'; interactive = $false; expectedResult = '\\a\\b\\c\\d\\e\\f\\g\\h\\i\\j\\k\\l\\m\\n\\o\\p\\q\\r\\s\\t\\u\\v\\w\\x\\y\\z'} 146 | @{arg = '\A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z'; interactive = $true; expectedResult = '\A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z'} 147 | @{arg = '\A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z'; interactive = $false; expectedResult = '\\A\\B\\C\\D\\E\\F\\G\\H\\I\\J\\K\\L\\M\\N\\O\\P\\Q\\R\\S\\T\\U\\V\\W\\X\\Y\\Z'} 148 | @{arg = '\0\1\2\3\4\5\6\7\8\9'; interactive = $true; expectedResult = '\0\1\2\3\4\5\6\7\8\9'} 149 | @{arg = '\0\1\2\3\4\5\6\7\8\9'; interactive = $false; expectedResult = '\\0\\1\\2\\3\\4\\5\\6\\7\\8\\9'} 150 | ) { 151 | param([string]$arg, [bool]$interactive, [string]$expectedResult) 152 | 153 | Format-WslArgument $arg $interactive | Should -BeExactly $expectedResult 154 | } 155 | } -------------------------------------------------------------------------------- /WslInterop.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'WslInterop' 3 | # 4 | # Generated by: Mike Battista 5 | # 6 | # Generated on: 9/29/2019 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = '.\WslInterop.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '0.4.1' 16 | 17 | # Supported PSEditions 18 | CompatiblePSEditions = 'Core' 19 | 20 | # ID used to uniquely identify this module 21 | GUID = 'b3b24408-16df-432e-8587-45c230e9b8c2' 22 | 23 | # Author of this module 24 | Author = 'Mike Battista' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Microsoft' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) Mike Battista. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Integrate Linux commands into Windows with PowerShell and the Windows Subsystem for Linux (WSL).' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | PowerShellVersion = '6.0' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # 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. 72 | FunctionsToExport = 'Import-WslCommand', 'Format-WslArgument' 73 | 74 | # 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. 75 | CmdletsToExport = @() 76 | 77 | # Variables to export from this module 78 | VariablesToExport = 'WslCompletionFunctions' 79 | 80 | # 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. 81 | AliasesToExport = @() 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | # FileList = @() 91 | 92 | # 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. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | # Tags = @() 99 | 100 | # A URL to the license for this module. 101 | LicenseUri = 'https://github.com/mikebattista/PowerShell-WSL-Interop/blob/master/LICENSE' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/mikebattista/PowerShell-WSL-Interop' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | # ReleaseNotes = '' 111 | 112 | # Prerelease string of this module 113 | # Prerelease = '' 114 | 115 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 116 | RequireLicenseAcceptance = $false 117 | 118 | # External dependent modules of this module 119 | # ExternalModuleDependencies = @() 120 | 121 | } # End of PSData hashtable 122 | 123 | } # End of PrivateData hashtable 124 | 125 | # HelpInfo URI of this module 126 | # HelpInfoURI = '' 127 | 128 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 129 | # DefaultCommandPrefix = '' 130 | 131 | } -------------------------------------------------------------------------------- /WslInterop.psm1: -------------------------------------------------------------------------------- 1 | function global:Import-WslCommand() { 2 | <# 3 | .SYNOPSIS 4 | Import Linux commands into the session as PowerShell functions with argument completion. 5 | 6 | .DESCRIPTION 7 | WSL enables calling Linux commands directly within PowerShell via wsl.exe (e.g. wsl ls). While more convenient 8 | than a full context switch into WSL, it has the following limitations: 9 | 10 | * Prefixing commands with wsl is tedious and unnatural 11 | * Windows paths passed as arguments don't often resolve due to backslashes being interpreted as escape characters rather than directory separators 12 | * Windows paths passed as arguments don't often resolve due to not being translated to the appropriate mount point within WSL 13 | * Arguments with special characters (e.g. regular expressions) are often misinterpreted without unnatural embedded quotes or escape sequences 14 | * Default parameters defined in WSL login profiles with aliases and environment variables aren’t honored 15 | * Linux path completion is not supported 16 | * Command completion is not supported 17 | * Argument completion is not supported 18 | 19 | This function addresses these issues in the following ways: 20 | 21 | * By creating PowerShell function wrappers for commands, prefixing them with wsl is no longer necessary 22 | * By identifying path arguments and converting them to WSL paths, path resolution is natural and intuitive as it translates seamlessly between Windows and WSL paths 23 | * By formatting arguments with special characters, arguments like regular expressions can be provided naturally 24 | * Default parameters are supported by $WslDefaultParameterValues similar to $PSDefaultParameterValues 25 | * Environment variables are supported by $WslEnvironmentVariables 26 | * Command completion is enabled by PowerShell's command completion 27 | * Argument completion is enabled by registering an ArgumentCompleter that shims bash's programmable completion 28 | 29 | The commands can receive both pipeline input as well as their corresponding arguments just as if they were native to Windows. 30 | 31 | Additionally, they will honor any default parameters defined in a hash table called $WslDefaultParameterValues similar to $PSDefaultParameterValues. For example: 32 | 33 | * $WslDefaultParameterValues["grep"] = "-E" 34 | * $WslDefaultParameterValues["less"] = "-i" 35 | * $WslDefaultParameterValues["ls"] = "-AFh --group-directories-first" 36 | 37 | If you use aliases or environment variables within your login profiles to set default parameters for commands, define a hash table called $WslDefaultParameterValues within 38 | your PowerShell profile and populate it as above for a similar experience. 39 | 40 | Environment variables can also be set in a hash table called $WslEnvironmentVariables using the pattern $WslEnvironmentVariables[""] = "". 41 | 42 | The import of these functions replaces any PowerShell aliases that conflict with the commands. 43 | 44 | .PARAMETER Command 45 | Specifies the commands to import. 46 | 47 | .EXAMPLE 48 | Import-WslCommand "awk", "emacs", "grep", "head", "less", "ls", "man", "sed", "seq", "ssh", "tail", "vim" 49 | #> 50 | 51 | [CmdletBinding()] 52 | Param( 53 | [Parameter(Mandatory = $true)] 54 | [ValidateNotNullOrEmpty()] 55 | [string[]]$Command 56 | ) 57 | 58 | # Register a function for each command. 59 | $Command | ForEach-Object { Invoke-Expression @" 60 | Remove-Alias $_ -Scope Global -Force -ErrorAction Ignore 61 | function global:$_() { 62 | # Translate path arguments and format special characters. 63 | for (`$i = 0; `$i -lt `$args.Count; `$i++) { 64 | if (`$null -eq `$args[`$i]) { 65 | continue 66 | } 67 | 68 | # If a path is fully qualified (e.g. C:) and not a UNC path, run it through wslpath to map it to the appropriate mount point. 69 | if ([System.IO.Path]::IsPathFullyQualified(`$args[`$i]) -and -not (`$args[`$i] -like '\\*')) { 70 | `$args[`$i] = Format-WslArgument (wsl.exe wslpath (`$args[`$i] -replace "\\", "/")) 71 | # If a path is relative, the current working directory will be translated to an appropriate mount point, so just format it. 72 | # Avoid invalid wildcard pattern errors by using -LiteralPath for invalid patterns. 73 | } elseif (`$args[`$i] -match '\[[^\[]*[/\\]') { 74 | if (Test-Path -LiteralPath `$args[`$i] -ErrorAction Ignore) { 75 | `$args[`$i] = Format-WslArgument (`$args[`$i] -replace "\\", "/") 76 | } else { 77 | `$args[`$i] = Format-WslArgument `$args[`$i] 78 | } 79 | # If a path is relative, the current working directory will be translated to an appropriate mount point, so just format it. 80 | # The path doesn't contain an invalid wildcard pattern, so use -Path to support wildcards. 81 | } elseif (Test-Path `$args[`$i] -ErrorAction Ignore) { 82 | `$args[`$i] = Format-WslArgument (`$args[`$i] -replace "\\", "/") 83 | # Otherwise, format special characters. 84 | } else { 85 | `$args[`$i] = Format-WslArgument `$args[`$i] 86 | } 87 | } 88 | 89 | # Build the command to pass to WSL. 90 | `$distribution = ("-d `$(`$WslDefaultParameterValues."-d")", "")[`$WslDefaultParameterValues."-d" -eq `$null] 91 | `$username = ("-u `$(`$WslDefaultParameterValues."-u")", "")[`$WslDefaultParameterValues."-u" -eq `$null] 92 | `$shelltype = ("--shell-type `$(`$WslDefaultParameterValues."--shell-type")", "")[`$WslDefaultParameterValues."--shell-type" -eq `$null] 93 | `$environmentVariables = ((`$WslEnvironmentVariables.Keys | ForEach-Object { "`$_='`$(`$WslEnvironmentVariables."`$_")'" }), "")[`$WslEnvironmentVariables.Count -eq 0] 94 | `$defaultArgs = (`$WslDefaultParameterValues."$_", "")[`$WslDefaultParameterValues."$_" -eq `$null -or `$WslDefaultParameterValues.Disabled -eq `$true] 95 | if (`$defaultArgs -is [scriptblock]) { `$defaultArgs = . `$defaultArgs } 96 | 97 | `$commandLine = "" 98 | if (`$distribution -ne "") { 99 | `$commandLine += "`$distribution " 100 | } 101 | if (`$username -ne "") { 102 | `$commandLine += "`$username " 103 | } 104 | if (`$shelltype -ne "") { 105 | `$commandLine += "`$shelltype " 106 | } 107 | if (`$environmentVariables -ne "") { 108 | `$commandLine += "`$environmentVariables " 109 | } 110 | `$commandLine += "$_ " 111 | if (`$defaultArgs -ne "") { 112 | `$commandLine += "`$defaultArgs " 113 | } 114 | `$commandLine += "`$args" 115 | `$commandLine = "`$commandLine".Trim() -split ' ' 116 | 117 | # Invoke the command. 118 | if (`$input.MoveNext()) { 119 | `$input.Reset() 120 | `$input | wsl.exe `$commandLine 121 | } else { 122 | wsl.exe `$commandLine 123 | } 124 | } 125 | "@ 126 | } 127 | 128 | # Register an ArgumentCompleter that shims bash's programmable completion. 129 | Register-ArgumentCompleter -CommandName $Command -ScriptBlock { 130 | param($wordToComplete, $commandAst, $cursorPosition) 131 | 132 | # Identify the command. 133 | $command = $commandAst.CommandElements[0].Value 134 | 135 | # Initialize the bash completion function cache. 136 | $WslCompletionFunctionsCache = "$Env:APPDATA\PowerShell WSL Interop\WslCompletionFunctions" 137 | if ($null -eq $global:WslCompletionFunctions) { 138 | if (Test-Path $WslCompletionFunctionsCache) { 139 | $global:WslCompletionFunctions = Import-Clixml $WslCompletionFunctionsCache 140 | } else { 141 | $global:WslCompletionFunctions = @{} 142 | } 143 | } 144 | 145 | # Map the command to the appropriate bash completion function. 146 | if (-not $global:WslCompletionFunctions.Contains($command)) { 147 | # Try to find the completion function. 148 | $global:WslCompletionFunctions[$command] = wsl.exe bash -c ". /usr/share/bash-completion/bash_completion 2> /dev/null; __load_completion $command 2> /dev/null; complete -p $command 2> /dev/null | sed -E 's/^complete.*-F ([^ ]+).*`$/\1/'" 149 | 150 | # If we can't find a completion function, resort to the default completion function. 151 | if ($null -eq $global:WslCompletionFunctions[$command] -or $global:WslCompletionFunctions[$command] -like "complete*") { 152 | $global:WslCompletionFunctions["-D"] = wsl.exe bash -c ". /usr/share/bash-completion/bash_completion 2> /dev/null; complete -p -D 2> /dev/null | sed -E 's/^complete.*-F ([^ ]+).*`$/\1/'" 153 | 154 | # If the default completion function was overridden, use that. 155 | if ($global:WslCompletionFunctions["-D"] -ne "_completion_loader") { 156 | $global:WslCompletionFunctions[$command] = $global:WslCompletionFunctions["-D"] 157 | # Otherwise, resort to _minimal which will return Linux file paths. 158 | } else { 159 | $global:WslCompletionFunctions[$command] = "_minimal" 160 | } 161 | } 162 | 163 | # Update the bash completion function cache. 164 | New-Item $WslCompletionFunctionsCache -Force | Out-Null 165 | $global:WslCompletionFunctions | Export-Clixml $WslCompletionFunctionsCache 166 | } 167 | 168 | # Populate bash programmable completion variables. 169 | $COMP_LINE = "`"$($commandAst.Extent.Text.PadRight($cursorPosition))`"" 170 | $COMP_WORDS = "('$($commandAst.CommandElements.Extent.Text -join "' '")')" -replace "''", "'" 171 | $previousWord = $commandAst.CommandElements[0].Value 172 | $COMP_CWORD = 1 173 | $COMP_POINT = $cursorPosition - $commandAst.Extent.StartOffset 174 | for ($i = 1; $i -lt $commandAst.CommandElements.Count; $i++) { 175 | $extent = $commandAst.CommandElements[$i].Extent 176 | if ($cursorPosition -lt $extent.EndColumnNumber) { 177 | # The cursor is in the middle of a word to complete. 178 | $previousWord = $commandAst.CommandElements[$i - 1].Extent.Text 179 | $COMP_CWORD = $i 180 | break 181 | } elseif ($cursorPosition -eq $extent.EndColumnNumber) { 182 | # The cursor is immediately after the current word. 183 | $previousWord = $extent.Text 184 | $COMP_CWORD = $i + 1 185 | break 186 | } elseif ($cursorPosition -lt $extent.StartColumnNumber) { 187 | # The cursor is within whitespace between the previous and current words. 188 | $previousWord = $commandAst.CommandElements[$i - 1].Extent.Text 189 | $COMP_CWORD = $i 190 | break 191 | } elseif ($i -eq $commandAst.CommandElements.Count - 1 -and $cursorPosition -gt $extent.EndColumnNumber) { 192 | # The cursor is within whitespace at the end of the line. 193 | $previousWord = $extent.Text 194 | $COMP_CWORD = $i + 1 195 | break 196 | } 197 | } 198 | 199 | # Repopulate bash programmable completion variables for scenarios like '/mnt/c/Program Files'/ where should continue completing the quoted path. 200 | $currentExtent = $commandAst.CommandElements[$COMP_CWORD].Extent 201 | $previousExtent = $commandAst.CommandElements[$COMP_CWORD - 1].Extent 202 | if ($currentExtent.Text -like "/*" -and $currentExtent.StartColumnNumber -eq $previousExtent.EndColumnNumber) { 203 | $COMP_LINE = $COMP_LINE -replace "$($previousExtent.Text)$($currentExtent.Text)", $wordToComplete 204 | $COMP_WORDS = $COMP_WORDS -replace "$($previousExtent.Text) '$($currentExtent.Text)'", $wordToComplete 205 | $previousWord = $commandAst.CommandElements[$COMP_CWORD - 2].Extent.Text 206 | $COMP_CWORD -= 1 207 | } 208 | 209 | # Build the command to pass to WSL. 210 | $bashCompletion = ". /usr/share/bash-completion/bash_completion 2> /dev/null" 211 | $commandCompletion = "__load_completion $command 2> /dev/null" 212 | $COMPINPUT = "COMP_LINE=$COMP_LINE; COMP_WORDS=$COMP_WORDS; COMP_CWORD=$COMP_CWORD; COMP_POINT=$COMP_POINT" 213 | $COMPGEN = "bind `"set completion-ignore-case on`" 2> /dev/null; $($WslCompletionFunctions[$command]) `"$command`" `"$wordToComplete`" `"$previousWord`" 2> /dev/null" 214 | $COMPREPLY = "IFS=`$'\n'; echo `"`${COMPREPLY[*]}`"" 215 | $commandLine = "$bashCompletion; $commandCompletion; $COMPINPUT; $COMPGEN; $COMPREPLY" 216 | 217 | # Invoke bash completion. 218 | $completions = ($commandLine | wsl.exe bash -s) 219 | 220 | # If no results were returned by an overridden default completion function, emulate `-o default` by resorting to _minimal. 221 | if ($completions -eq "" -and $WslCompletionFunctions[$command] -eq $global:WslCompletionFunctions["-D"]) { 222 | $completions = ($commandLine -replace $WslCompletionFunctions[$command], "_minimal" | wsl.exe bash -s) 223 | } 224 | 225 | # Return CompletionResults. 226 | $previousCompletionText = "" 227 | $completions -split '\n' | 228 | Sort-Object -Unique -CaseSensitive | 229 | ForEach-Object { 230 | if ($_ -eq "") { 231 | continue 232 | } 233 | 234 | if ($wordToComplete -match "(.*=).*") { 235 | $completionText = Format-WslArgument ($Matches[1] + $_) $true 236 | $listItemText = $_ 237 | } else { 238 | $completionText = Format-WslArgument $_ $true 239 | $listItemText = $completionText 240 | } 241 | 242 | if ($completionText -eq $previousCompletionText) { 243 | # Differentiate completions that differ only by case otherwise PowerShell will view them as duplicate. 244 | $listItemText += ' ' 245 | } 246 | 247 | $previousCompletionText = $completionText 248 | [System.Management.Automation.CompletionResult]::new($completionText, $listItemText, 'ParameterName', $completionText) 249 | } 250 | } 251 | } 252 | 253 | function global:Format-WslArgument([string]$arg, [bool]$interactive) { 254 | <# 255 | .SYNOPSIS 256 | Format arguments passed to WSL to prevent them from being misinterpreted. 257 | 258 | Wrapping arguments in quotes can interfere with user intent for scenarios like 259 | pathname expansion and variable substitution, so instead just escape special characters, 260 | optimizing for the most common scenarios (e.g. paths and regular expressions). 261 | 262 | Automatic formatting can be bypassed by embedding quotes within arguments (e.g. "'s/;/\n/g'") 263 | in which case the embedded quotes will be passed to WSL as part of the argument and standard 264 | quoting rules will be applied. 265 | #> 266 | 267 | $arg = $arg.Trim() 268 | 269 | if ($arg -like "[""']*[""']") { 270 | return $arg 271 | } 272 | 273 | if ($interactive) { 274 | $arg = (($arg -replace '([ ,(){}|&;])', '`$1'), "'$arg'")[$arg.Contains(" ")] 275 | } else { 276 | $arg = $arg -replace '(\\\\|\\[ ,(){}|&;])', '\\$1' 277 | 278 | while ($arg -match '([^\\](\\\\)*)([ ,(){}|&;])') { 279 | $arg = $arg -replace '([^\\](\\\\)*)([ ,(){}|&;])', '$1\$3' 280 | } 281 | $arg = $arg -replace '^((\\\\)*)([ ,(){}|&;])', '$1\$3' 282 | 283 | $arg = $arg -replace '(\\[a-zA-Z0-9.*+?^$])', '\$1' 284 | } 285 | 286 | return $arg 287 | } --------------------------------------------------------------------------------