Integratie van Linux-opdrachten in Windows met behulp van PowerShell en WSL

Een typische vraag van Windows-ontwikkelaars: “Waarom is er nog steeds geen <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Of het nu een krachtige swipe is less of bekende hulpmiddelen grep of sedwillen Windows-ontwikkelaars in hun dagelijkse werk gemakkelijk toegang hebben tot deze opdrachten.

Windows-subsysteem voor Linux (WSL) heeft op dit vlak een grote stap voorwaarts gemaakt. Hiermee kun je Linux-opdrachten vanuit Windows aanroepen door ze via een proxy te versturen wsl.exe (bijvoorbeeld wsl ls). Hoewel dit een aanzienlijke verbetering is, heeft deze optie verschillende nadelen.

  • Alomtegenwoordige toevoeging wsl vervelend en onnatuurlijk.
  • Windows-paden in argumenten werken niet altijd omdat backslashes worden geïnterpreteerd als escape-tekens in plaats van mapscheidingstekens.
  • Windows-paden in argumenten worden niet vertaald naar het overeenkomstige koppelpunt in WSL.
  • Standaardinstellingen worden niet gerespecteerd in WSL-profielen met aliassen en omgevingsvariabelen.
  • Voltooiing van Linux-paden wordt niet ondersteund.
  • Het voltooien van opdrachten wordt niet ondersteund.
  • Het voltooien van argumenten wordt niet ondersteund.

Als gevolg hiervan worden Linux-commando's onder Windows behandeld als tweederangsburgers en zijn ze moeilijker te gebruiken dan native commando's. Om hun rechten gelijk te maken, is het noodzakelijk om de genoemde problemen op te lossen.

PowerShell-functiewrappers

Met PowerShell-functiewrappers kunnen we opdrachtvoltooiing toevoegen en de noodzaak voor voorvoegsels elimineren wsl, Windows-paden vertalen naar WSL-paden. Basisvereisten voor schelpen:

  • Voor elke Linux-opdracht moet er één functie-wrapper met dezelfde naam zijn.
  • De shell moet de als argumenten doorgegeven Windows-paden herkennen en deze naar WSL-paden converteren.
  • De shell zou moeten bellen wsl met de juiste Linux-opdracht naar elke pijplijninvoer en het doorgeven van opdrachtregelargumenten die aan de functie zijn doorgegeven.

Omdat dit patroon op elke opdracht kan worden toegepast, kunnen we de definitie van deze wrappers abstraheren en deze dynamisch genereren uit een lijst met te importeren opdrachten.

# 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 Ignore
function 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[`$i] -IsAbsolute -ErrorAction Ignore) {
            `$args[`$i] = Format-WslArgument (wsl.exe wslpath (`$args[`$i] -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[`$i] -ErrorAction Ignore) {
            `$args[`$i] = Format-WslArgument (`$args[`$i] -replace "", "/")
        }
    }
 
    if (`$input.MoveNext()) {
        `$input.Reset()
        `$input | wsl.exe $_ (`$args -split ' ')
    } else {
        wsl.exe $_ (`$args -split ' ')
    }
}
"@
}

Lijst $command definieert importopdrachten. Vervolgens genereren we dynamisch een functie-wrapper voor elk van hen met behulp van de opdracht Invoke-Expression (door eerst alle aliassen te verwijderen die in conflict zouden kunnen komen met de functie).

De functie herhaalt de argumenten op de opdrachtregel en bepaalt Windows-paden met behulp van opdrachten Split-Path и Test-Pathen converteert deze paden vervolgens naar WSL-paden. We laten de paden door een helperfunctie lopen Format-WslArgument, die we later zullen definiëren. Het ontsnapt aan speciale tekens zoals spaties en haakjes die anders verkeerd zouden worden geïnterpreteerd.

Tenslotte brengen wij het over wsl pijplijninvoer en eventuele opdrachtregelargumenten.

Met deze wrappers kun je je favoriete Linux-commando's op een meer natuurlijke manier oproepen zonder een voorvoegsel toe te voegen wsl en zonder u zorgen te hoeven maken over hoe de paden worden geconverteerd:

  • man bash
  • less -i $profile.CurrentUserAllHosts
  • ls -Al C:Windows | less
  • grep -Ein error *.log
  • tail -f *.log

De basisset met opdrachten wordt hier weergegeven, maar u kunt voor elke Linux-opdracht een shell maken door deze eenvoudigweg aan de lijst toe te voegen. Als u deze code toevoegt aan uw profiel PowerShell, deze opdrachten zijn voor u beschikbaar in elke PowerShell-sessie, net als native opdrachten!

Standaard instellingen

In Linux is het gebruikelijk om aliassen en/of omgevingsvariabelen te definiëren in inlogprofielen, waarbij standaardparameters worden ingesteld voor veelgebruikte opdrachten (bijvoorbeeld alias ls=ls -AFh of export LESS=-i). Een van de nadelen van proxying via een niet-interactieve shell wsl.exe - dat de profielen niet zijn geladen, waardoor deze opties niet standaard beschikbaar zijn (bijv. ls in WSL en wsl ls zal zich anders gedragen met de hierboven gedefinieerde alias).

PowerShell biedt $PSDefaultParameterValues, een standaardmechanisme voor het definiëren van standaardparameters, maar alleen voor cmdlets en geavanceerde functies. Natuurlijk kunnen we geavanceerde functies van onze shells maken, maar dit introduceert onnodige complicaties (PowerShell correleert bijvoorbeeld gedeeltelijke parameternamen (bijvoorbeeld -a correleert met -ArgumentList), wat in conflict zal komen met Linux-opdrachten die gedeeltelijke namen als argumenten gebruiken), en de syntaxis voor het definiëren van standaardwaarden zal niet de meest geschikte zijn (standaardargumenten vereisen de naam van de parameter in de sleutel, niet alleen de opdrachtnaam) .

Met een kleine aanpassing aan onze shells kunnen we echter een model implementeren dat vergelijkbaar is met $PSDefaultParameterValuesen schakel standaardopties voor Linux-opdrachten in!

function global:$_() {
    …
 
    `$defaultArgs = ((`$WslDefaultParameterValues.$_ -split ' '), "")[`$WslDefaultParameterValues.Disabled -eq `$true]
    if (`$input.MoveNext()) {
        `$input.Reset()
        `$input | wsl.exe $_ `$defaultArgs (`$args -split ' ')
    } else {
        wsl.exe $_ `$defaultArgs (`$args -split ' ')
    }
}

Passeren $WslDefaultParameterValues naar de opdrachtregel sturen we parameters via wsl.exe. Hieronder ziet u hoe u instructies aan uw PowerShell-profiel toevoegt om de standaardinstellingen te configureren. Nu kunnen we het!

$WslDefaultParameterValues["grep"] = "-E"
$WslDefaultParameterValues["less"] = "-i"
$WslDefaultParameterValues["ls"] = "-AFh --group-directories-first"

Omdat de parameters zijn gemodelleerd $PSDefaultParameterValues, dat kan het is gemakkelijk om ze uit te schakelen tijdelijk door het installeren van de sleutel "Disabled" in betekenis $true. Een bijkomend voordeel van een aparte hashtabel is de mogelijkheid om deze uit te schakelen $WslDefaultParameterValues afzonderlijk van $PSDefaultParameterValues.

Voltooiing van de argumentatie

Met PowerShell kunt u argumenttrailers registreren met behulp van de opdracht Register-ArgumentCompleter. Bash is krachtig programmeerbare tools voor automatisch aanvullen. Met WSL kunt u bash aanroepen vanuit PowerShell. Als we argumentvoltooiingen voor onze PowerShell-functiewrappers kunnen registreren en bash kunnen aanroepen om de voltooiingen te genereren, krijgen we volledige argumentvoltooiing met dezelfde precisie als 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[0].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[$i].Extent
        if ($cursorPosition -lt $extent.EndColumnNumber) {
            # The cursor is in the middle of a word to complete.
            $previousWord = $commandAst.CommandElements[$i - 1].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[$i - 1].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[$COMP_CWORD].Extent
    $previousExtent = $commandAst.CommandElements[$COMP_CWORD - 1].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[$COMP_CWORD - 2].Extent.Text
        $COMP_CWORD -= 1
    }
 
    # Build the command to pass to WSL.
    $command = $commandAst.CommandElements[0].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[1] + $_) $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
        [System.Management.Automation.CompletionResult]::new($completionText, $listItemText, 'ParameterName', $completionText)
    }
}
 
# Helper function to escape characters in arguments passed to WSL that would otherwise be misinterpreted.
function global:Format-WslArgument([string]$arg, [bool]$interactive) {
    if ($interactive -and $arg.Contains(" ")) {
        return "'$arg'"
    } else {
        return ($arg -replace " ", " ") -replace "([()|])", ('$1', '`$1')[$interactive]
    }
}

De code is een beetje compact zonder enkele interne functies van bash te begrijpen, maar wat we in principe doen is dit:

  • Een argumentcompleter registreren voor al onze functiewrappers door een lijst door te geven $commands in parameters -CommandName voor Register-ArgumentCompleter.
  • We wijzen elke opdracht toe aan de shell-functie die bash gebruikt voor automatisch aanvullen (om specificaties voor automatisch aanvullen te definiëren, gebruikt bash $F, afkorting voor complete -F <FUNCTION>).
  • PowerShell-argumenten converteren $wordToComplete, $commandAst и $cursorPosition in het formaat dat wordt verwacht door de automatische aanvullingsfuncties van bash volgens de specificaties programmeerbare automatische aanvulling bashen.
  • We stellen een opdrachtregel samen waarnaar u wilt overbrengen wsl.exe, dat ervoor zorgt dat de omgeving correct is ingesteld, roept de juiste functie voor automatisch aanvullen aan en voert de resultaten regel voor regel uit.
  • Dan bellen wij wsl met de opdrachtregel scheiden we de uitvoer door regelscheidingstekens en genereren we voor elk CompletionResults, door ze te sorteren en tekens zoals spaties en haakjes te laten ontsnappen die anders verkeerd zouden worden geïnterpreteerd.

Als gevolg hiervan zullen onze Linux-opdrachtshells exact dezelfde automatische aanvulling gebruiken als bash! Bijvoorbeeld:

  • ssh -c <TAB> -J <TAB> -m <TAB> -O <TAB> -o <TAB> -Q <TAB> -w <TAB> -b <TAB>

Elke automatische aanvulling levert waarden op die specifiek zijn voor het vorige argument, waarbij configuratiegegevens zoals bekende hosts van WSL worden gelezen!

<TAB> loopt door de parameters. <Ctrl + пробел> toont alle beschikbare opties.

En omdat we nu bash-autoaanvulling hebben, kun je Linux-paden rechtstreeks in PowerShell automatisch aanvullen!

  • less /etc/<TAB>
  • ls /usr/share/<TAB>
  • vim ~/.bash<TAB>

In gevallen waarin automatische aanvulling van bash geen resultaten oplevert, keert PowerShell terug naar de standaard Windows-paden van het systeem. In de praktijk kunt u dus naar eigen goeddunken beide paden tegelijkertijd gebruiken.

Conclusie

Met behulp van PowerShell en WSL kunnen we Linux-opdrachten als native applicaties in Windows integreren. Het is niet nodig om naar Win32-builds of Linux-hulpprogramma's te zoeken of uw workflow te onderbreken door naar een Linux-shell te gaan. Zojuist WSL installeren, configureren PowerShell-profiel и vermeld de opdrachten die u wilt importeren! Rijke automatische aanvulling voor Linux- en Windows-opdrachtparameters en bestandspaden is functionaliteit die tegenwoordig niet eens beschikbaar is in native Windows-opdrachten.

De volledige broncode die hierboven is beschreven, evenals aanvullende richtlijnen voor het opnemen ervan in uw workflow, is beschikbaar hier.

Welke Linux-opdrachten vind je het nuttigst? Welke andere veel voorkomende dingen ontbreken bij het werken in Windows? Schrijf in de reacties of op GitHub!

Bron: www.habr.com

Voeg een reactie