Integrering av Linux-kommandoer i Windows ved hjelp av PowerShell og WSL

Et typisk spørsmål fra Windows-utviklere: "Hvorfor er det fortsatt nei <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Enten det er et kraftig sveip less eller kjente verktøy grep eller sed, vil Windows-utviklere ha enkel tilgang til disse kommandoene i det daglige arbeidet.

Windows-undersystem for Linux (WSL) har tatt et stort skritt fremover i denne forbindelse. Den lar deg ringe Linux-kommandoer fra Windows ved å sende dem via proxy wsl.exe (f.eks. wsl ls). Selv om dette er en betydelig forbedring, lider dette alternativet av en rekke ulemper.

  • Allestedsnærværende tillegg wsl kjedelig og unaturlig.
  • Windows-baner i argumenter fungerer ikke alltid fordi omvendte skråstreker tolkes som escape-tegn i stedet for katalogskilletegn.
  • Windows-baner i argumenter blir ikke oversatt til det tilsvarende monteringspunktet i WSL.
  • Standardinnstillinger respekteres ikke i WSL-profiler med aliaser og miljøvariabler.
  • Fullføring av Linux-bane støttes ikke.
  • Kommandofullføring støttes ikke.
  • Argumentfullføring støttes ikke.

Som et resultat blir Linux-kommandoer behandlet som annenrangs borgere under Windows – og er vanskeligere å bruke enn opprinnelige kommandoer. For å utjevne rettighetene deres, er det nødvendig å løse de oppførte problemene.

PowerShell-funksjonsinnpakninger

Med PowerShell-funksjonspakker kan vi legge til kommandofullføring og eliminere behovet for prefikser wsl, oversetter Windows-baner til WSL-baner. Grunnleggende krav til skjell:

  • For hver Linux-kommando må det være en funksjonsomslag med samme navn.
  • Skallet må gjenkjenne Windows-banene som sendes som argumenter og konvertere dem til WSL-baner.
  • Skallet skal ringe wsl med den riktige Linux-kommandoen til en hvilken som helst pipeline-inngang og sende eventuelle kommandolinjeargumenter som sendes til funksjonen.

Siden dette mønsteret kan brukes på en hvilken som helst kommando, kan vi abstrahere definisjonen av disse wrapperne og generere dem dynamisk fra en liste over kommandoer som 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 deretter dynamisk en funksjonsinnpakning for hver av dem ved å bruke kommandoen Invoke-Expression (ved først å fjerne eventuelle aliaser som ville komme i konflikt med funksjonen).

Funksjonen itererer over kommandolinjeargumenter, bestemmer Windows-baner ved hjelp av kommandoer Split-Path и Test-Pathog konverterer deretter disse banene til WSL-baner. Vi kjører stiene gjennom en hjelpefunksjon Format-WslArgument, som vi vil definere senere. Den unnslipper spesialtegn som mellomrom og parenteser som ellers ville blitt feiltolket.

Til slutt formidler vi wsl pipeline-inndata og eventuelle kommandolinjeargumenter.

Med disse innpakningene kan du kalle dine favoritt Linux-kommandoer på en mer naturlig måte uten å legge til et prefiks wsl og uten å bekymre deg for hvordan banene konverteres:

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

Det grunnleggende settet med kommandoer vises her, men du kan lage et skall for enhver Linux-kommando ved å legge det til i listen. Hvis du legger til denne koden i din profil PowerShell, disse kommandoene vil være tilgjengelige for deg i hver PowerShell-økt, akkurat som native kommandoer!

Standard instillinger

I Linux er det vanlig å definere aliaser og/eller miljøvariabler i påloggingsprofiler, og sette standardparametere for ofte brukte kommandoer (f.eks. alias ls=ls -AFh eller export LESS=-i). En av ulempene med proxying gjennom et ikke-interaktivt skall wsl.exe - at profilene ikke er lastet inn, så disse alternativene er ikke tilgjengelige som standard (dvs. ls i WSL og wsl ls vil oppføre seg annerledes med aliaset definert ovenfor).

PowerShell gir $PSDefaultParameterValues, en standardmekanisme for å definere standardparametere, men bare for cmdlets og avanserte funksjoner. Selvfølgelig kan vi lage avanserte funksjoner ut av skallene våre, men dette introduserer unødvendige komplikasjoner (for eksempel korrelerer PowerShell delvise parameternavn (for eksempel, -a korrelerer med -ArgumentList), som vil komme i konflikt med Linux-kommandoer som tar delnavn som argumenter), og syntaksen for å definere standardverdier vil ikke være den mest passende (å definere standardargumenter krever parameternavnet i nøkkelen, ikke bare kommandonavnet).

Imidlertid, med en liten modifikasjon av skallene våre, kan vi implementere en modell som ligner på $PSDefaultParameterValues, og aktiver standardalternativer 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 ' ')
    }
}

Passering $WslDefaultParameterValues til kommandolinjen sender vi parametere via wsl.exe. Følgende viser hvordan du legger til instruksjoner til PowerShell-profilen din for å konfigurere standardinnstillinger. Nå kan vi gjøre det!

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

Siden parametrene er modellert etter $PSDefaultParameterValues, Du kan det er enkelt å deaktivere dem midlertidig ved å installere nøkkelen "Disabled" til mening $true. En ekstra fordel med en separat hash-tabell er muligheten til å deaktivere $WslDefaultParameterValues separat fra $PSDefaultParameterValues.

Argumentfullføring

PowerShell lar deg registrere argumenttrailere ved å bruke kommandoen Register-ArgumentCompleter. Bash har kraftig programmerbare autofullføringsverktøy. WSL lar deg ringe bash fra PowerShell. Hvis vi kan registrere argumentfullføringer for våre PowerShell-funksjonspakker og kalle bash for å generere fullføringene, får vi full argumentfullføring med samme presisjon 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 litt tett uten å forstå noen av bashs interne funksjoner, men i utgangspunktet er det vi gjør dette:

  • Registrere en argumentfullfører for alle funksjonspakkerne våre ved å sende en liste $commands i parameter -CommandName for Register-ArgumentCompleter.
  • Vi tilordner hver kommando til skallfunksjonen som bash bruker for autofullføring (for å definere spesifikasjoner for autofullføring, bruker bash $F, forkortelse for complete -F <FUNCTION>).
  • Konvertering av PowerShell-argumenter $wordToComplete, $commandAst и $cursorPosition inn i formatet som forventes av bashs autofullføringsfunksjoner i henhold til spesifikasjonene programmerbar autofullføring bash.
  • Vi lager en kommandolinje for å overføre til wsl.exe, som sikrer at miljøet er riktig konfigurert, kaller opp den riktige autofullføringsfunksjonen og sender ut resultatene linje for linje.
  • Så ringer vi wsl med kommandolinjen skiller vi utdataene med linjeskillere og genererer for hver CompletionResults, sortere dem og unnslippe tegn som mellomrom og parenteser som ellers ville blitt feiltolket.

Som et resultat vil våre Linux-kommandoskall bruke nøyaktig samme autofullføring som bash! For eksempel:

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

Hver autofullføring gir verdier som er spesifikke for det forrige argumentet, og leser konfigurasjonsdata som kjente verter fra WSL!

<TAB> vil gå gjennom parametrene. <Ctrl + пробел> vil vise alle tilgjengelige alternativer.

I tillegg, siden vi nå har bash-autofullføring, kan du autofullføre Linux-baner direkte i PowerShell!

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

I tilfeller der bash-autofullføring ikke gir noen resultater, går PowerShell tilbake til systemets standard Windows-baner. Dermed kan du i praksis bruke begge veier samtidig etter eget skjønn.

Konklusjon

Ved å bruke PowerShell og WSL kan vi integrere Linux-kommandoer i Windows som native applikasjoner. Det er ikke nødvendig å søke etter Win32-bygg eller Linux-verktøy eller avbryte arbeidsflyten din ved å gå til et Linux-skall. Bare installer WSL, konfigurere PowerShell-profil и liste opp kommandoene du vil importere! Rik autofullføring for Linux- og Windows-kommandoparametere og filbaner er funksjonalitet som ikke engang er tilgjengelig i opprinnelige Windows-kommandoer i dag.

Den fullstendige kildekoden beskrevet ovenfor, samt ytterligere retningslinjer for å inkludere den i arbeidsflyten din, er tilgjengelig her.

Hvilke Linux-kommandoer synes du er mest nyttige? Hvilke andre vanlige ting mangler når du arbeider i Windows? Skriv i kommentarfeltet eller på GitHub!

Kilde: www.habr.com

Legg til en kommentar