Integrering af Linux-kommandoer i Windows ved hjælp af PowerShell og WSL

Et typisk spørgsmål fra Windows-udviklere: “Hvorfor er der stadig ingen <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Uanset om det er et kraftigt swipe less eller velkendte værktøjer grep eller sed, Windows-udviklere ønsker nem adgang til disse kommandoer i deres daglige arbejde.

Windows-undersystem til Linux (WSL) har taget et stort skridt fremad i denne henseende. Det giver dig mulighed for at kalde Linux-kommandoer fra Windows ved at proxye dem igennem wsl.exe (Fx wsl ls). Selvom dette er en væsentlig forbedring, lider denne mulighed af en række ulemper.

  • Allestedsnærværende tilføjelse wsl kedeligt og unaturligt.
  • Windows-stier i argumenter virker ikke altid, fordi omvendte skråstreg fortolkes som escape-tegn snarere end mappeseparatorer.
  • Windows-stier i argumenter oversættes ikke til det tilsvarende monteringspunkt i WSL.
  • Standardindstillingerne respekteres ikke i WSL-profiler med aliaser og miljøvariabler.
  • Fuldførelse af Linux-sti understøttes ikke.
  • Kommandofuldførelse er ikke understøttet.
  • Argumentfuldførelse er ikke understøttet.

Som følge heraf behandles Linux-kommandoer som andenrangsborgere under Windows - og er sværere at bruge end oprindelige kommandoer. For at udligne deres rettigheder er det nødvendigt at løse de anførte problemer.

PowerShell-funktionsindpakninger

Med PowerShell-funktionsindpakninger kan vi tilføje kommandofuldførelse og eliminere behovet for præfikser wsl, oversætter Windows-stier til WSL-stier. Grundlæggende krav til skaller:

  • For hver Linux-kommando skal der være en funktionsindpakning med samme navn.
  • Skallen skal genkende de Windows-stier, der sendes som argumenter, og konvertere dem til WSL-stier.
  • Skallen burde kalde wsl med den passende Linux-kommando til enhver pipeline-input og videregivelse af eventuelle kommandolinjeargumenter, der er sendt til funktionen.

Da dette mønster kan anvendes på enhver kommando, kan vi abstrahere definitionen af ​​disse wrappers og dynamisk generere dem fra en liste over kommandoer, der skal importeres.

# 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 ' ')
    }
}
"@
}

liste $command definerer importkommandoer. Vi genererer derefter dynamisk en funktionsindpakning for hver af dem ved hjælp af kommandoen Invoke-Expression (ved først at fjerne eventuelle aliaser, der ville være i konflikt med funktionen).

Funktionen itererer over kommandolinjeargumenter, bestemmer Windows-stier ved hjælp af kommandoer Split-Path и Test-Pathog konverterer derefter disse stier til WSL-stier. Vi kører stierne gennem en hjælperfunktion Format-WslArgument, som vi vil definere senere. Det undslipper specialtegn såsom mellemrum og parenteser, der ellers ville blive misfortolket.

Til sidst formidler vi wsl pipeline-input og eventuelle kommandolinjeargumenter.

Med disse wrappers kan du kalde dine foretrukne Linux-kommandoer på en mere naturlig måde uden at tilføje et præfiks wsl og uden at bekymre dig om, hvordan stierne konverteres:

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

Det grundlæggende sæt af kommandoer er vist her, men du kan oprette en shell til enhver Linux-kommando ved blot at tilføje den til listen. Hvis du tilføjer denne kode til din profil PowerShell, disse kommandoer vil være tilgængelige for dig i hver PowerShell-session, ligesom native kommandoer!

Standardindstillinger

I Linux er det almindeligt at definere aliaser og/eller miljøvariabler i login-profiler ved at indstille standardparametre for ofte brugte kommandoer (f.eks. alias ls=ls -AFh eller export LESS=-i). En af ulemperne ved proxying gennem en ikke-interaktiv shell wsl.exe - at profilerne ikke er indlæst, så disse muligheder er ikke tilgængelige som standard (dvs. ls i WSL og wsl ls vil opføre sig anderledes med det ovenfor definerede alias).

PowerShell giver $PSDefaultParameterValues, en standardmekanisme til at definere standardparametre, men kun for cmdlets og avancerede funktioner. Selvfølgelig kan vi lave avancerede funktioner ud af vores skaller, men dette introducerer unødvendige komplikationer (f.eks. korrelerer PowerShell delvise parameternavne (f.eks. -a korrelerer med -ArgumentList), som vil være i konflikt med Linux-kommandoer, der tager delnavne som argumenter), og syntaksen til at definere standardværdier vil ikke være den mest passende (standardargumenter kræver navnet på parameteren i nøglen, ikke kun kommandonavnet) .

Men med en lille ændring af vores skaller, kan vi implementere en model svarende til $PSDefaultParameterValues, og aktiver standardindstillinger for Linux-kommandoer!

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

Bestået $WslDefaultParameterValues til kommandolinjen sender vi parametre via wsl.exe. Det følgende viser, hvordan du tilføjer instruktioner til din PowerShell-profil for at konfigurere standardindstillinger. Nu kan vi gøre det!

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

Da parametrene er modelleret efter $PSDefaultParameterValuesdu kan det er nemt at deaktivere dem midlertidigt ved at installere nøglen "Disabled" i betydning $true. En yderligere fordel ved en separat hash-tabel er muligheden for at deaktivere $WslDefaultParameterValues adskilt fra $PSDefaultParameterValues.

Argumentafslutning

PowerShell giver dig mulighed for at registrere argumenttrailere ved hjælp af kommandoen Register-ArgumentCompleter. Bash har kraftfuld programmerbare værktøjer til automatisk fuldførelse. WSL giver dig mulighed for at ringe til bash fra PowerShell. Hvis vi kan registrere argumentafslutninger for vores PowerShell-funktionsindpakninger og kalde bash for at generere afslutningerne, får vi fuld argumentafslutning med samme præcision som bash selv!

# 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]
    }
}

Koden er en smule tæt uden at forstå nogle af bashs interne funktioner, men dybest set, hvad vi gør er dette:

  • Registrering af en argumentkompletterer for alle vores funktionsindpakninger ved at sende en liste $commands i parameter -CommandName for Register-ArgumentCompleter.
  • Vi knytter hver kommando til shell-funktionen, som bash bruger til autofuldførelse (for at definere autofuldførelsesspecifikationer, bruger bash $F, forkortelse for complete -F <FUNCTION>).
  • Konvertering af PowerShell-argumenter $wordToComplete, $commandAst и $cursorPosition i det format, der forventes af bashs autofuldførelsesfunktioner i henhold til specifikationerne programmerbar autofuldførelse bash.
  • Vi komponerer en kommandolinje til at overføre til wsl.exe, som sikrer, at miljøet er konfigureret korrekt, kalder den passende autofuldførelsesfunktion og udsender resultaterne linje for linje.
  • Så ringer vi wsl med kommandolinjen adskiller vi output med linjeseparatorer og genererer for hver CompletionResults, sortering af dem og undslippende tegn såsom mellemrum og parenteser, der ellers ville blive misfortolket.

Som et resultat vil vores Linux-kommandoskaller bruge nøjagtig den samme autofuldførelse som bash! For eksempel:

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

Hver autofuldførelse leverer værdier, der er specifikke for det forrige argument, læser konfigurationsdata såsom kendte værter fra WSL!

<TAB> vil cykle gennem parametrene. <Ctrl + пробел> vil vise alle tilgængelige muligheder.

Plus, da vi nu har bash-autofuldførelse, kan du autofuldføre Linux-stier direkte i PowerShell!

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

I tilfælde, hvor bash-autofuldførelse ikke giver nogen resultater, vender PowerShell tilbage til systemets standard Windows-stier. I praksis kan du således bruge begge veje samtidigt efter eget skøn.

Konklusion

Ved hjælp af PowerShell og WSL kan vi integrere Linux-kommandoer i Windows som native applikationer. Der er ingen grund til at søge efter Win32-builds eller Linux-værktøjer eller afbryde din arbejdsgang ved at gå til en Linux-skal. Lige installere WSL, konfigurere PowerShell-profil и liste de kommandoer, du vil importere! Rig autofuldførelse for Linux- og Windows-kommandoparametre og filstier er funktionalitet, der ikke engang er tilgængelig i oprindelige Windows-kommandoer i dag.

Den fulde kildekode, der er beskrevet ovenfor, samt yderligere retningslinjer for inkorporering af den i din arbejdsgang, er tilgængelig her.

Hvilke Linux-kommandoer finder du mest nyttige? Hvilke andre almindelige ting mangler, når du arbejder i Windows? Skriv i kommentarfeltet eller på GitHub!

Kilde: www.habr.com

Tilføj en kommentar