Integreren van Linux-commando's in Windows met behulp van PowerShell en WSL

Een veelgestelde vraag van Windows-ontwikkelaars: "Waarom is hier nog steeds geen <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?». Будь то мощное пролистывание less of vertrouwde tools grep of sed, Windows-ontwikkelaars willen eenvoudig toegang hebben tot deze commando's in hun dagelijks werk.

Windows Subsystem for Linux (WSL) heeft hierin enorme stappen gezet. Het maakt het mogelijk om Linux-commando's vanuit Windows aan te roepen, door ze te proxen via wsl.exe (bijvoorbeeld, wsl ls). Hoewel dit een aanzienlijke verbetering is, heeft deze optie verschillende nadelen.

  • De wijdverspreide integratie wsl is moeizaam en onnatuurlijk.
  • Windows-paden in argumenten werken niet altijd, omdat de omgekeerde schuine strepen worden geïnterpreteerd als escape-tekens in plaats van als pad scheidingstekens.
  • Windows-paden in argumenten worden niet omgezet in de bijbehorende mount-punt in WSL.
  • Standaardparameters in WSL-profielen met aliassen en omgevingsvariabelen worden niet in aanmerking genomen.
  • Er is geen ondersteuning voor het aanvullen van Linux-paden.
  • Er is geen ondersteuning voor het aanvullen van commando's.
  • Er is geen ondersteuning voor het aanvullen van argumenten.

Als gevolg hiervan worden Linux-commando's onder Windows beschouwd als tweederangs burgers - en zijn ze moeilijker te gebruiken dan native commando's. Om hun rechten gelijk te trekken, moeten de genoemde problemen worden opgelost.

PowerShell-functie-shells

Met PowerShell-functie-shells kunnen we autocompletion voor commando's toevoegen en de noodzaak voor prefixes elimineren wsl, door Windows-paden naar WSL-paden te vertalen. De belangrijkste vereisten voor de shells zijn:

  • Voor elk Linux-commando moet er één functie-shell met dezelfde naam zijn.
  • De shell moet Windows-paden herkennen die als argumenten zijn doorgegeven en deze omzetten naar WSL-paden.
  • De shell moet aanroepen wsl met het bijbehorende Linux-commando op elke invoer van de pipeline en alle commandoregelargumenten doorgeven die aan de functie zijn doorgegeven.

Aangezien dit sjabloon op elk commando kan worden toegepast, kunnen we de definitie van deze shells abstraheren en ze dynamisch genereren vanuit een lijst van commando's voor import.

# 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 bepaalt de commando's voor import. Vervolgens genereren we dynamisch een functie-wrapper voor elk van hen, met behulp van het commando Invoke-Expression (door eerst eventuele aliassen te verwijderen die conflicteren met de functie).

De functie doorloopt de commandoregelargumenten, herkent Windows-paden met de commando's Split-Path en Test-Path, en vervolgens deze paden omzet naar WSL-paden. We voeren de paden door een hulpfunctie Format-WslArgument, die we later zullen definiëren. Het escapen van speciale tekens, zoals spaties en haakjes, die anders verkeerd geïnterpreteerd zouden worden.

Ten slotte geven we wsl de invoer van de pijplijn en eventuele opdrachtregelargumenten door.

Met zulke wrappers kun je je favoriete Linux-commando's natuurlijker aanroepen, zonder een prefix toe te voegen wsl en je geen zorgen te maken over hoe paden worden omgezet:

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

Hier is een basisset van commando's, maar je kunt een wrapper voor elk Linux-commando maken door het gewoon aan de lijst toe te voegen. Als je deze code in je profiel PowerShell plaatst, zijn deze commando's beschikbaar in elke PowerShell-sessie, net als de native commando's!

Standaardparameters

In Linux is het gebruikelijk om aliassen en/of omgevingsvariabelen in profielen (loginprofiel) te definiëren, waarbij standaardparameters voor vaak gebruikte commando's worden ingesteld (bijvoorbeeld, alias ls=ls -AFh of export LESS=-i). Een van de nadelen van proxy-werking via een niet-interactieve shell wsl.exe is dat profielen niet worden geladen, waardoor deze standaardparameters niet beschikbaar zijn (d.w.z. ls in WSL en wsl ls zullen 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 van onze shells geavanceerde functies maken, maar dit voegt complicaties toe (klopt, PowerShell koppelt gedeeltelijke parameternaam (bijvoorbeeld, -a koppelt aan -ArgumentList), die conflicteren met Linux-commando's die gedeeltelijke namen als argumenten accepteren), en de syntaxis voor het definiëren van standaardwaarden is niet de meest geschikte (voor het definiëren van standaardargumenten is de parameternaam in de sleutel vereist, niet alleen de naam van het commando).

Echter, met een kleine aanpassing van onze shells kunnen we een model implementeren dat lijkt op $PSDefaultParameterValues, en standaardparameters voor Linux-commando's opnemen!

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 ' ')
    }
}

Door te geven $WslDefaultParameterValues in de opdrachtregel sturen we parameters door via wsl.exe. Hieronder staat hoe u instructies aan het PowerShell-profiel kunt toevoegen om standaardparameters in te stellen. Nu kunnen we dit doen!

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

Aangezien parameters worden gemodelleerd na $PSDefaultParameterValues, kunt u ze gemakkelijk uitschakelen voor een bepaalde tijd door de sleutel "Disabled" op de waarde van $true. Een bijkomend voordeel van een aparte hashtabel is de mogelijkheid om uit te schakelen $WslDefaultParameterValues afzonderlijk van $PSDefaultParameterValues.

Automatisch aanvullen van argumenten

PowerShell stelt gebruikers in staat om argumentenaanvullers te registreren met het commando Register-ArgumentCompleter. In Bash zijn er krachtige programmeermogelijkheden voor automatisch aanvullen. WSL stelt ons in staat om bash vanuit PowerShell aan te roepen. Als we argumentenaanvullers voor onze PowerShell-functies kunnen registreren en bash kunnen aanroepen om aanvullingen te genereren, hebben we volledige automatische aanvulling van argumenten met dezelfde precisie 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[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 inzicht in enkele interne bash-functies, maar grotendeels doen we het volgende:

  • We registreren de argumentaanvuller voor al onze functieomslagen door een lijst door te geven $commands aan de parameter -CommandName voor Register-ArgumentCompleter.
  • We koppelen elke opdracht aan de shellfunctie die bash gebruikt voor automatisch aanvullen (om de specificaties voor automatisch aanvullen in bash te bepalen, wordt $F, een afkorting van complete -F <FUNCTION>).
  • We transformeren PowerShell-argumenten $wordToComplete, $commandAst en $cursorPosition in het formaat dat door de functies voor automatisch aanvullen in bash wordt verwacht volgens de specificaties van programmeervriendelijk automatisch aanvullen bash.
  • We stellen de opdrachtregel samen om door te geven aan wsl.exe, die zorgt voor de juiste omgevingsconfiguratie, roept de juiste functie voor automatisch aanvullen aan en levert resultaten met regelafbrekingen.
  • Vervolgens roepen we wsl met de opdrachtregel aan, splitsen we de uitvoer op in regels en genereren we voor elke CompletionResults, sorteren ze en escapen we symbolen zoals spaties en haakjes die anders verkeerd geïnterpreteerd zouden worden.

Uiteindelijk zullen onze Linux-opdrachtenshells exact dezelfde automatisch aanvulling gebruiken als in bash! Bijvoorbeeld:

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

Elke autocompletion biedt waarden die specifiek zijn voor het vorige argument, door configuratiegegevens te lezen, zoals bekende hosts, vanuit WSL!

<TAB> zal parameters cyclisch doorlopen. <Ctrl + пробел> toont alle beschikbare opties.

Bovendien, omdat we nu autocompletion met bash hebben, kun je Linux-paden direct in PowerShell autocompleten!

  • less /etc/
  • ls /usr/share/
  • vim ~/ .bash

In gevallen waarin bash-autocompletion geen resultaten oplevert, valt PowerShell terug op het standaard systeem met Windows-paden. Zo kun je in de praktijk zowel Linux- als Windows-paden naar eigen inzicht tegelijkertijd gebruiken.

Conclusie

Met PowerShell en WSL kunnen we Linux-commando's in Windows integreren als native applicaties. Er is geen noodzaak om Win32-builds of Linux-hulpmiddelen te zoeken of je workflow te onderbreken door naar de Linux-shell te schakelen. Gewoon WSL installeren, configureer je PowerShell-profiel en geef de commando's op die je wilt importeren! Rijke autocompletion voor commando-parameters en paden naar bestanden in Linux en Windows is functionaliteit die vandaag de dag zelfs niet aanwezig is in de native Windows-commando's.

De volledige broncode, zoals hierboven beschreven, evenals extra aanbevelingen voor het integreren in je workflow, zijn beschikbaar hier.

Welke Linux-commando's vind je het nuttigst? Wat mis je verder tijdens het werken in Windows? Laat het weten in de reacties of op GitHub!

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster