Mike
September 26, 2019
Een veelgehoorde vraag van Windows-ontwikkelaars is “waarom heeft Windows nog geen <INSERT FAVORITE LINUX COMMAND HERE>
?”. Of ze nu verlangen naar een krachtige pager zoals less
of vertrouwde commando’s willen gebruiken zoals grep
of sed
, Windows-ontwikkelaars verlangen naar eenvoudige toegang tot deze commando’s als onderdeel van hun kernworkflow.
Het Windows Subsystem for Linux (WSL) was hier een enorme stap voorwaarts, waardoor ontwikkelaars Linux commando’s vanuit Windows konden aanroepen door ze te proxen via wsl.exe
(bijv. wsl ls
). Hoewel dit een aanzienlijke verbetering is, schiet de ervaring op verschillende manieren tekort:
- Het toevoegen van commando’s met
wsl
is vervelend en onnatuurlijk - Windows-paden die als argumenten worden doorgegeven, worden vaak niet opgelost doordat backslashes worden geïnterpreteerd als escape-tekens in plaats van mapplaatscheidingstekens
- Windows-paden die als argumenten worden doorgegeven, worden vaak niet opgelost doordat ze niet worden vertaald naar het juiste koppelpunt binnen WSL
- Standaardparameters gedefinieerd in WSL login profielen met aliassen en omgevingsvariabelen worden niet gerespecteerd
- Linux path completion wordt niet ondersteund
- Command completion wordt niet ondersteund
- Argument completion wordt niet ondersteund
Het resultaat van deze tekortkomingen is dat Linux commando’s aanvoelen als tweederangsburgers dan Windows en zijn ze moeilijker te gebruiken dan ze zouden moeten zijn. Om een commando aan te laten voelen als een Windows commando, moeten we deze problemen oplossen.
PowerShell Functie Wrappers
We kunnen de noodzaak om commando’s te prefixen met wsl
wegnemen, de vertaling van Windows paden naar WSL paden afhandelen, en command completion ondersteunen met PowerShell functie-wrappers. De basisvereisten voor de wrappers zijn:
- Er moet één functie-wrapper per Linux-commando zijn met dezelfde naam als het commando
- De wrapper moet Windows-paden herkennen die als argumenten worden doorgegeven en deze vertalen naar WSL-paden
- De wrapper moet
wsl
aanroepen met het corresponderende Linux-commando, waarbij alle pijplijn-input wordt doorgegeven en alle commandoregel-argumenten die aan de functie zijn doorgegeven, worden doorgegeven
Omdat dit sjabloon op elk commando kan worden toegepast, kunnen we de definitie van deze wrappers abstraheren en ze dynamisch genereren uit een lijst van te importeren commando’s.
# The commands to import.$commands = "awk", "emacs", "grep", "head", "less", "ls", "man", "sed", "seq", "ssh", "tail", "vim"# Register a function for each command.$commands | ForEach-Object { Invoke-Expression @"Remove-Alias $_ -Force -ErrorAction Ignorefunction global:$_() { for (`$i = 0; `$i -lt `$args.Count; `$i++) { # If a path is absolute with a qualifier (e.g. C:), run it through wslpath to map it to the appropriate mount point. if (Split-Path `$args -IsAbsolute -ErrorAction Ignore) { `$args = Format-WslArgument (wsl.exe wslpath (`$args -replace "\\", "/")) # If a path is relative, the current working directory will be translated to an appropriate mount point, so just format it. } elseif (Test-Path `$args -ErrorAction Ignore) { `$args = Format-WslArgument (`$args -replace "\\", "/") } } if (`$input.MoveNext()) { `$input.Reset() `$input | wsl.exe $_ (`$args -split ' ') } else { wsl.exe $_ (`$args -split ' ') }}"@}
De $command
lijst definieert de te importeren commando’s. Vervolgens genereren we dynamisch de functie-wrapper voor elk met behulp van het Invoke-Expression
commando (waarbij we eerst alle aliassen verwijderen die in conflict zouden komen met de functie).
De functie loopt door de commandoregel-argumenten, identificeert Windows-paden met behulp van de Split-Path
en Test-Path
commando’s, en converteert die paden vervolgens naar WSL-paden. We voeren de paden door een helperfunctie die we later zullen definiëren, Format-WslArgument
genaamd, die speciale tekens zoals spaties en haakjes escaped die anders verkeerd zouden worden geïnterpreteerd.
Ten slotte geven we de invoer van de pijplijn en eventuele opdrachtregelargumenten door aan wsl
.
Met deze functie-wrappers op hun plaats, kunnen we nu onze favoriete Linux commando’s op een meer natuurlijke manier aanroepen zonder dat we ze hoeven te prefixen met wsl
of ons zorgen hoeven te maken over hoe Windows-paden worden vertaald naar WSL-paden:
man bash
less -i $profile.CurrentUserAllHosts
ls -Al C:\Windows\ | less
grep -Ein error *.log
tail -f *.log
Een startset van commando’s wordt hier getoond, maar je kunt voor elk Linux-commando een wrapper genereren door het simpelweg aan de lijst toe te voegen. Als u deze code toevoegt aan uw PowerShell-profiel, staan deze commando’s u in elke PowerShell-sessie ter beschikking, net als native commando’s!
Standaardparameters
Het is gebruikelijk in Linux om aliassen en/of omgevingsvariabelen te definiëren binnen inlogprofielen om standaardparameters in te stellen voor commando’s die u vaak gebruikt (bijv. alias ls=ls -AFh
of export LESS=-i
). Een van de nadelen van proxying via een niet-interactieve shell via wsl.exe
is dat login profielen niet geladen worden, dus deze standaard parameters zijn niet beschikbaar (bijv. ls
binnen WSL en wsl ls
zouden zich anders gedragen met de hierboven gedefinieerde alias).
PowerShell biedt $PSDefaultParameterValues
, een standaard mechanisme om standaard parameterwaarden te definiëren, maar alleen voor cmdlets en geavanceerde functies. Onze functie-wrappers omzetten in geavanceerde functies is mogelijk, maar brengt complicaties met zich mee (PowerShell matcht bijvoorbeeld gedeeltelijke parameternamen (zoals -a
voor -ArgumentList
), wat conflicteert met Linux commando’s die gedeeltelijke namen als argumenten accepteren), en de syntax voor het definiëren van standaard waarden zou minder dan ideaal zijn voor dit scenario (de naam van een parameter in de sleutel voor het definiëren van de standaard argumenten is nodig, in plaats van alleen de naam van het commando).
Met een kleine wijziging in onze functie-wrappers kunnen we een model introduceren dat lijkt op $PSDefaultParameterValues
en standaardparameters voor Linux-commando’s mogelijk maken!
function global:$_() { … `$defaultArgs = ((`$WslDefaultParameterValues.$_ -split ' '), "") if (`$input.MoveNext()) { `$input.Reset() `$input | wsl.exe $_ `$defaultArgs (`$args -split ' ') } else { wsl.exe $_ `$defaultArgs (`$args -split ' ') }}
Door $WslDefaultParameterValues
door te geven in de opdrachtregel die we sturen via wsl.exe
, kun je nu statements als hieronder toevoegen aan je PowerShell profiel om standaard parameters in te stellen!
$WslDefaultParameterValues = "-E"$WslDefaultParameterValues = "-i"$WslDefaultParameterValues = "-AFh --group-directories-first"
Omdat dit is gemodelleerd naar $PSDefaultParameterValues
, kunt u deze eenvoudig tijdelijk uitschakelen door de "Disabled"
sleutel op $true
te zetten. Een aparte hashtabel heeft als bijkomend voordeel dat $WslDefaultParameterValues
apart van $PSDefaultParameterValues
kan worden uitgeschakeld.
Argument Completion
PowerShell stelt u in staat om argument completers te registreren met het Register-ArgumentCompleter
commando. Bash heeft krachtige programmeerbare aanvulmogelijkheden. Met WSL kun je bash aanroepen vanuit PowerShell. Als we argumentcompleteers kunnen registreren voor onze PowerShell functie-wrappers en bash kunnen aanroepen om de completies te genereren, kunnen we rijke argumentcompleteers krijgen met dezelfde betrouwbaarheid als in bash zelf!
# Register an ArgumentCompleter that shims bash's programmable completion.Register-ArgumentCompleter -CommandName $commands -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) # Map the command to the appropriate bash completion function. $F = switch ($commandAst.CommandElements.Value) { {$_ -in "awk", "grep", "head", "less", "ls", "sed", "seq", "tail"} { "_longopt" break } "man" { "_man" break } "ssh" { "_ssh" break } Default { "_minimal" break } } # Populate bash programmable completion variables. $COMP_LINE = "`"$commandAst`"" $COMP_WORDS = "('$($commandAst.CommandElements.Extent.Text -join "' '")')" -replace "''", "'" for ($i = 1; $i -lt $commandAst.CommandElements.Count; $i++) { $extent = $commandAst.CommandElements.Extent if ($cursorPosition -lt $extent.EndColumnNumber) { # The cursor is in the middle of a word to complete. $previousWord = $commandAst.CommandElements.Extent.Text $COMP_CWORD = $i break } elseif ($cursorPosition -eq $extent.EndColumnNumber) { # The cursor is immediately after the current word. $previousWord = $extent.Text $COMP_CWORD = $i + 1 break } elseif ($cursorPosition -lt $extent.StartColumnNumber) { # The cursor is within whitespace between the previous and current words. $previousWord = $commandAst.CommandElements.Extent.Text $COMP_CWORD = $i break } elseif ($i -eq $commandAst.CommandElements.Count - 1 -and $cursorPosition -gt $extent.EndColumnNumber) { # The cursor is within whitespace at the end of the line. $previousWord = $extent.Text $COMP_CWORD = $i + 1 break } } # Repopulate bash programmable completion variables for scenarios like '/mnt/c/Program Files'/<TAB> where <TAB> should continue completing the quoted path. $currentExtent = $commandAst.CommandElements.Extent $previousExtent = $commandAst.CommandElements.Extent if ($currentExtent.Text -like "/*" -and $currentExtent.StartColumnNumber -eq $previousExtent.EndColumnNumber) { $COMP_LINE = $COMP_LINE -replace "$($previousExtent.Text)$($currentExtent.Text)", $wordToComplete $COMP_WORDS = $COMP_WORDS -replace "$($previousExtent.Text) '$($currentExtent.Text)'", $wordToComplete $previousWord = $commandAst.CommandElements.Extent.Text $COMP_CWORD -= 1 } # Build the command to pass to WSL. $command = $commandAst.CommandElements.Value $bashCompletion = ". /usr/share/bash-completion/bash_completion 2> /dev/null" $commandCompletion = ". /usr/share/bash-completion/completions/$command 2> /dev/null" $COMPINPUT = "COMP_LINE=$COMP_LINE; COMP_WORDS=$COMP_WORDS; COMP_CWORD=$COMP_CWORD; COMP_POINT=$cursorPosition" $COMPGEN = "bind `"set completion-ignore-case on`" 2> /dev/null; $F `"$command`" `"$wordToComplete`" `"$previousWord`" 2> /dev/null" $COMPREPLY = "IFS=`$'\n'; echo `"`${COMPREPLY}`"" $commandLine = "$bashCompletion; $commandCompletion; $COMPINPUT; $COMPGEN; $COMPREPLY" -split ' ' # Invoke bash completion and return CompletionResults. $previousCompletionText = "" (wsl.exe $commandLine) -split '\n' | Sort-Object -Unique -CaseSensitive | ForEach-Object { if ($wordToComplete -match "(.*=).*") { $completionText = Format-WslArgument ($Matches + $_) $true $listItemText = $_ } else { $completionText = Format-WslArgument $_ $true $listItemText = $completionText } if ($completionText -eq $previousCompletionText) { # Differentiate completions that differ only by case otherwise PowerShell will view them as duplicate. $listItemText += ' ' } $previousCompletionText = $completionText ::new($completionText, $listItemText, 'ParameterName', $completionText) }}# Helper function to escape characters in arguments passed to WSL that would otherwise be misinterpreted.function global:Format-WslArgument($arg, $interactive) { if ($interactive -and $arg.Contains(" ")) { return "'$arg'" } else { return ($arg -replace " ", "\ ") -replace "()", ('\$1', '`$1') }}
De code is een beetje ingewikkeld zonder een goed begrip van bash internals, maar in principe:
- We registreren de argument completer voor al onze functie wrappers door de
$commands
lijst door te geven aan de-CommandName
parameter vanRegister-ArgumentCompleter
- We mappen elk commando aan de shell functie die bash gebruikt om ervoor te voltooien (
$F
die is vernoemd naarcomplete -F <FUNCTION>
die wordt gebruikt om voltooiingsspecificaties in bash te definiëren) - We converteren PowerShell’s
$wordToComplete
$commandAst
, en$cursorPosition
argumenten in het formaat dat verwacht wordt door bash completion functies volgens de bash programmeerbare completion spec - We bouwen een opdrachtregel die we kunnen doorgeven aan
wsl.exe
die ervoor zorgt dat de completion omgeving correct is ingesteld, de juiste voltooiingsfunctie aanroept, en vervolgens een tekenreeks met de voltooiingsresultaten uitvoert, gescheiden door nieuwe regels - Wij roepen dan
wsl
aan met de commandoregel, splitsen de uitvoerstekenreeks op de nieuwe regelscheiding, en genereren danCompletionResults
voor elk, sorteren ze, en escapen tekens zoals spaties en haakjes die anders verkeerd geïnterpreteerd zouden worden
Het eindresultaat hiervan is dat onze Linux commando wrappers nu exact dezelfde completering zullen gebruiken als bash gebruikt! Bijvoorbeeld:
ssh -c <TAB> -J <TAB> -m <TAB> -O <TAB> -o <TAB> -Q <TAB> -w <TAB> -b <TAB>
Elke voltooiing geeft waarden die specifiek zijn voor het argument ervoor, waarbij configuratiegegevens zoals bekende hosts worden ingelezen vanuit WSL!
<TAB>
zal door de opties lopen. <Ctrl + Space>
toont alle beschikbare opties.
Daarnaast, omdat bash completion nu de leiding heeft, kunt u Linux-paden direct binnen PowerShell oplossen!
less /etc/<TAB>
-
ls /usr/share/<TAB>
vim ~/.bash<TAB>
In gevallen waarin bash completion geen resultaten oplevert, valt PowerShell terug op de standaard completering die Windows-paden zal oplossen, waardoor u in feite zowel Linux-paden als Windows-paden naar believen kunt oplossen.
Conclusie
Met PowerShell en WSL kunnen we Linux commando’s in Windows integreren, net alsof het native applicaties zijn. Je hoeft niet op zoek te gaan naar Win32 builds van Linux programma’s of je workflow te onderbreken om in een Linux shell te komen. Installeer WSL, stel uw PowerShell profiel in, en maak een lijst van de commando’s die u wilt importeren! De rijke argumentcompletering die hier wordt getoond van zowel commando-opties als Linux- en Windows-bestandspaden is een ervaring die zelfs native Windows-commando’s vandaag de dag niet bieden.
De volledige broncode die hierboven is beschreven, evenals aanvullende richtlijnen voor het opnemen ervan in uw workflow, is beschikbaar op https://github.com/mikebattista/PowerShell-WSL-Interop.
Welke Linux-commando’s vindt u het handigst? Welke andere delen van je ontwikkelaarsworkflow mis je op Windows?
Laat het ons weten in de reacties hieronder of op GitHub!
Mike Battista
Senior Program Manager, Windows Developer Platform
Volg