Avatar

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 van Register-ArgumentCompleter
  • We mappen elk commando aan de shell functie die bash gebruikt om ervoor te voltooien ($F die is vernoemd naar complete -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 dan CompletionResults 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!

Avatar
Mike Battista

Senior Program Manager, Windows Developer Platform

Volg

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *