Integrera Linux-kommandon i Windows med PowerShell och WSL

En typisk fråga från Windows-utvecklare: "Varför finns det fortfarande ingen <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Oavsett om det är ett kraftfullt svep less eller bekanta verktyg grep eller sed, Windows-utvecklare vill ha enkel åtkomst till dessa kommandon i sitt dagliga arbete.

Windows Subsystem för Linux (WSL) har tagit ett stort steg framåt i detta avseende. Det låter dig anropa Linux-kommandon från Windows genom att proxysända dem wsl.exe (Till exempel, wsl ls). Även om detta är en betydande förbättring lider detta alternativ av ett antal nackdelar.

  • Allestädes närvarande tillägg wsl tråkigt och onaturligt.
  • Windows-sökvägar i argument fungerar inte alltid eftersom omvända snedstreck tolkas som escape-tecken snarare än katalogavgränsare.
  • Windows-sökvägar i argument översätts inte till motsvarande monteringspunkt i WSL.
  • Standardinställningarna respekteras inte i WSL-profiler med alias och miljövariabler.
  • Slutförande av Linux-sökväg stöds inte.
  • Kommandoslutförande stöds inte.
  • Argumentkomplettering stöds inte.

Som ett resultat av detta behandlas Linux-kommandon som andra klassens medborgare under Windows – och är svårare att använda än inbyggda kommandon. För att utjämna sina rättigheter är det nödvändigt att lösa de listade problemen.

PowerShell-funktionsomslag

Med PowerShell-funktionsomslag kan vi lägga till kommandokomplettering och eliminera behovet av prefix wsl, översätta Windows-sökvägar till WSL-sökvägar. Grundkrav för skal:

  • För varje Linux-kommando måste det finnas ett funktionsomslag med samma namn.
  • Skalet måste känna igen Windows-sökvägarna som skickas som argument och konvertera dem till WSL-sökvägar.
  • Skalet borde ringa wsl med lämpligt Linux-kommando till valfri pipeline-ingång och skicka eventuella kommandoradsargument som skickas till funktionen.

Eftersom det här mönstret kan tillämpas på vilket kommando som helst, kan vi abstrahera definitionen av dessa omslag och dynamiskt generera dem från en lista med kommandon att importera.

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

Lista $command definierar importkommandon. Vi genererar sedan dynamiskt ett funktionsomslag för var och en av dem med hjälp av kommandot Invoke-Expression (genom att först ta bort alla alias som skulle komma i konflikt med funktionen).

Funktionen itererar över kommandoradsargument, bestämmer Windows-sökvägar med hjälp av kommandon Split-Path и Test-Pathoch konverterar sedan dessa vägar till WSL-vägar. Vi kör stigarna genom en hjälparfunktion Format-WslArgument, som vi kommer att definiera senare. Det undkommer specialtecken som mellanslag och parenteser som annars skulle misstolkas.

Till sist förmedlar vi wsl pipelineinmatning och eventuella kommandoradsargument.

Med dessa omslag kan du anropa dina favorit Linux-kommandon på ett mer naturligt sätt utan att lägga till ett prefix wsl och utan att oroa dig för hur vägarna konverteras:

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

Den grundläggande uppsättningen kommandon visas här, men du kan skapa ett skal för alla Linux-kommandon genom att helt enkelt lägga till det i listan. Om du lägger till den här koden till din profil PowerShell, dessa kommandon kommer att vara tillgängliga för dig i varje PowerShell-session, precis som inbyggda kommandon!

Standardinställningar

I Linux är det vanligt att definiera alias och/eller miljövariabler i inloggningsprofiler och ställa in standardparametrar för ofta använda kommandon (till exempel, alias ls=ls -AFh eller export LESS=-i). En av nackdelarna med proxy via ett icke-interaktivt skal wsl.exe - att profilerna inte laddas, så dessa alternativ är inte tillgängliga som standard (dvs. ls i WSL och wsl ls kommer att bete sig annorlunda med aliaset som definierats ovan).

PowerShell tillhandahåller $PSDefaultParameterValues, en standardmekanism för att definiera standardparametrar, men endast för cmdlets och avancerade funktioner. Naturligtvis kan vi göra avancerade funktioner av våra skal, men detta introducerar onödiga komplikationer (till exempel, PowerShell korrelerar partiella parameternamn (till exempel, -a korrelerar med -ArgumentList), som kommer i konflikt med Linux-kommandon som tar delnamn som argument), och syntaxen för att definiera standardvärden kommer inte att vara den mest lämpliga (standardargument kräver namnet på parametern i nyckeln, inte bara kommandonamnet) .

Men med en liten modifiering av våra skal kan vi implementera en modell som liknar $PSDefaultParameterValues, och aktivera standardalternativ för Linux-kommandon!

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

Godkänd $WslDefaultParameterValues till kommandoraden skickar vi parametrar via wsl.exe. Följande visar hur du lägger till instruktioner till din PowerShell-profil för att konfigurera standardinställningar. Nu kan vi göra det!

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

Eftersom parametrarna är modellerade efter $PSDefaultParameterValuesdu kan det är lätt att inaktivera dem tillfälligt genom att installera nyckeln "Disabled" till mening $true. En ytterligare fördel med en separat hashtabell är möjligheten att inaktivera $WslDefaultParameterValues separat från $PSDefaultParameterValues.

Argumentavslut

PowerShell låter dig registrera argumenttrailers med kommandot Register-ArgumentCompleter. Bash har kraftfull programmerbara verktyg för automatisk komplettering. WSL låter dig ringa bash från PowerShell. Om vi ​​kan registrera argumentkompletteringar för våra PowerShell-funktionsomslag och anropa bash för att generera kompletteringarna, får vi full argumentkomplettering med samma precision som bash själv!

# 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 är lite tät utan att förstå några av bashs interna funktioner, men i grund och botten vad vi gör är detta:

  • Registrera en argumentkompletterare för alla våra funktionsomslag genom att skicka en lista $commands i parameter -CommandName för Register-ArgumentCompleter.
  • Vi mappar varje kommando till skalfunktionen som bash använder för autokomplettering (för att definiera autokompletteringsspecifikationer använder bash $F, förkortning för complete -F <FUNCTION>).
  • Konvertera PowerShell-argument $wordToComplete, $commandAst и $cursorPosition till det format som förväntas av bashs autokompletterande funktioner enligt specifikationerna programmerbar automatisk komplettering våldsamt slag.
  • Vi komponerar en kommandorad att överföra till wsl.exe, som säkerställer att miljön är korrekt inställd, anropar lämplig automatisk kompletteringsfunktion och matar ut resultaten rad för rad.
  • Sen ringer vi wsl med kommandoraden separerar vi utdata med radavgränsare och genererar för var och en CompletionResults, sortera dem och undvika tecken som mellanslag och parenteser som annars skulle misstolkas.

Som ett resultat kommer våra Linux-kommandoskal att använda exakt samma autokomplettering som bash! Till exempel:

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

Varje autokomplettering tillhandahåller värden som är specifika för det föregående argumentet, läser konfigurationsdata som kända värdar från WSL!

<TAB> kommer att gå igenom parametrarna. <Ctrl + пробел> kommer att visa alla tillgängliga alternativ.

Plus, eftersom vi nu har bash-autokomplettering, kan du autokomplettera Linux-vägar direkt i PowerShell!

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

I fall där bash-autokomplettering inte ger några resultat, återgår PowerShell till systemets standardsökvägar för Windows. Således kan du i praktiken använda båda vägarna samtidigt efter eget gottfinnande.

Slutsats

Med PowerShell och WSL kan vi integrera Linux-kommandon i Windows som inbyggda applikationer. Det finns ingen anledning att söka efter Win32-versioner eller Linux-verktyg eller avbryta ditt arbetsflöde genom att gå till ett Linux-skal. Bara installera WSL, konfigurera PowerShell-profil и lista de kommandon du vill importera! Rich autocompletion för Linux- och Windows-kommandoparametrar och filsökvägar är funktionalitet som inte ens är tillgänglig i inbyggda Windows-kommandon idag.

Den fullständiga källkoden som beskrivs ovan, samt ytterligare riktlinjer för att integrera den i ditt arbetsflöde, är tillgänglig här.

Vilka Linux-kommandon tycker du är mest användbara? Vilka andra vanliga saker saknas när du arbetar i Windows? Skriv i kommentarerna eller på GitHub!

Källa: will.com

Lägg en kommentar