Integrace příkazů Linuxu do Windows pomocí PowerShell a WSL

Typická otázka od vývojářů Windows: „Proč stále neexistuje <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>? Ať už jde o mocné švihnutí less nebo známé nástroje grep nebo sed, vývojáři Windows chtějí snadný přístup k těmto příkazům při své každodenní práci.

Windows Subsystém pro Linux (WSL) udělala v tomto ohledu obrovský krok vpřed. Umožňuje vám volat příkazy Linuxu z Windows jejich prostřednictvím proxy wsl.exe (Např wsl ls). Přestože se jedná o výrazné zlepšení, tato možnost trpí řadou nevýhod.

  • Všudypřítomný přídavek wsl zdlouhavé a nepřirozené.
  • Cesty Windows v argumentech nefungují vždy, protože zpětná lomítka jsou interpretována jako znaky escape, nikoli jako oddělovače adresářů.
  • Cesty Windows v argumentech se nepřekládají do odpovídajícího bodu připojení ve WSL.
  • V profilech WSL s aliasy a proměnnými prostředí nejsou respektována výchozí nastavení.
  • Dokončení cesty systému Linux není podporováno.
  • Dokončení příkazu není podporováno.
  • Dokončení argumentu není podporováno.

V důsledku toho se s příkazy Linuxu zachází pod Windows jako s občany druhé třídy – a jejich použití je obtížnější než s nativními příkazy. K vyrovnání jejich práv je nutné řešit uvedené problémy.

Obálky funkcí PowerShellu

Pomocí obalů funkcí PowerShell můžeme přidat dokončování příkazů a eliminovat potřebu prefixů wsl, překládající cesty Windows na cesty WSL. Základní požadavky na mušle:

  • Pro každý příkaz Linuxu musí existovat jeden obal funkcí se stejným názvem.
  • Shell musí rozpoznat cesty Windows předané jako argumenty a převést je na cesty WSL.
  • Shell by měl zavolat wsl s příslušným linuxovým příkazem na jakýkoli vstup z kanálu a předáním jakýchkoli argumentů příkazového řádku předávaných funkci.

Protože tento vzor lze použít na jakýkoli příkaz, můžeme abstrahovat definici těchto obalů a dynamicky je generovat ze seznamu příkazů k importu.

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

seznam $command definuje importní příkazy. Pomocí příkazu pak dynamicky vygenerujeme obal funkcí pro každý z nich Invoke-Expression (nejprve odstraněním všech aliasů, které by byly v konfliktu s funkcí).

Funkce iteruje přes argumenty příkazového řádku, určuje cesty Windows pomocí příkazů Split-Path и Test-Patha poté tyto cesty převede na cesty WSL. Cesty vedeme přes pomocnou funkci Format-WslArgument, kterou definujeme později. Vynechá speciální znaky, jako jsou mezery a závorky, které by jinak byly špatně interpretovány.

Nakonec předáváme wsl vstup z potrubí a jakékoli argumenty příkazového řádku.

S těmito obaly můžete volat své oblíbené příkazy Linuxu přirozenějším způsobem bez přidání předpony wsl a bez starostí o to, jak se cesty převádějí:

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

Zde je zobrazena základní sada příkazů, ale můžete vytvořit shell pro jakýkoli příkaz Linuxu pouhým přidáním do seznamu. Pokud tento kód přidáte do svého profil PowerShell, tyto příkazy budete mít k dispozici v každé relaci PowerShellu, stejně jako nativní příkazy!

Výchozí nastavení

V Linuxu je běžné definovat aliasy a/nebo proměnné prostředí v přihlašovacích profilech a nastavit výchozí parametry pro často používané příkazy (např. alias ls=ls -AFh nebo export LESS=-i). Jedna z nevýhod proxyování přes neinteraktivní shell wsl.exe - že profily nejsou načteny, takže tyto možnosti nejsou standardně dostupné (tj. ls ve WSL a wsl ls se bude chovat jinak s aliasem definovaným výše).

PowerShell poskytuje $PSDefaultParameterValues, standardní mechanismus pro definování výchozích parametrů, ale pouze pro rutiny a pokročilé funkce. Samozřejmě můžeme z našich shellů vytvořit pokročilé funkce, ale to přináší zbytečné komplikace (například PowerShell koreluje názvy dílčích parametrů (např. -a koreluje s -ArgumentList), což bude v konfliktu s příkazy Linuxu, které berou jako argumenty částečná jména) a syntaxe pro definování výchozích hodnot nebude nejvhodnější (výchozí argumenty vyžadují název parametru v klíči, nikoli pouze název příkazu) .

S mírnou úpravou našich skořápek však můžeme implementovat model podobný $PSDefaultParameterValuesa povolte výchozí možnosti pro příkazy Linuxu!

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

Vysíláním $WslDefaultParameterValues do příkazového řádku posíláme parametry přes wsl.exe. Následující text ukazuje, jak přidat pokyny do profilu PowerShell pro konfiguraci výchozího nastavení. Teď to dokážeme!

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

Protože parametry jsou modelovány po $PSDefaultParameterValues, Můžeš je snadné je zakázat dočasně instalací klíče "Disabled" do smyslu $true. Další výhodou samostatné hashovací tabulky je možnost deaktivace $WslDefaultParameterValues odděleně od $PSDefaultParameterValues.

Dokončení argumentace

PowerShell umožňuje zaregistrovat upoutávky argumentů pomocí příkazu Register-ArgumentCompleter. Bash má moc programovatelné nástroje automatického dokončování. WSL umožňuje volat bash z PowerShellu. Pokud můžeme zaregistrovat dokončení argumentů pro naše obaly funkcí PowerShell a zavolat bash pro vygenerování dokončení, získáme úplné dokončení argumentů se stejnou přesností jako bash samotný!

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

Kód je trochu hustý bez pochopení některých interních funkcí bash, ale v podstatě děláme toto:

  • Registrace doplňku argumentů pro všechny naše obálky funkcí předáním seznamu $commands v parametru -CommandName pro Register-ArgumentCompleter.
  • Každý příkaz mapujeme na funkci shellu, kterou bash používá pro automatické dokončování (k definování specifikací automatického dokončování používá bash $F, zkratka pro complete -F <FUNCTION>).
  • Převod argumentů PowerShellu $wordToComplete, $commandAst и $cursorPosition do formátu očekávaného funkcemi automatického dokončování bash podle specifikací programovatelné automatické dokončování bash.
  • Vytvoříme příkazový řádek pro přenos wsl.exe, který zajistí správné nastavení prostředí, zavolá příslušnou funkci automatického dokončování a výsledky zobrazí po řádcích.
  • Pak zavoláme wsl pomocí příkazového řádku oddělujeme výstup oddělovači řádků a generujeme pro každý CompletionResults, jejich řazení a escapování znaků, jako jsou mezery a závorky, které by jinak byly nesprávně interpretovány.

Výsledkem je, že naše příkazové shelly pro Linux budou používat přesně stejné automatické dokončování jako bash! Například:

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

Každé automatické dokončování dodává hodnoty specifické pro předchozí argument, čte konfigurační data, jako jsou známí hostitelé z WSL!

<TAB> bude procházet parametry. <Ctrl + пробел> zobrazí všechny dostupné možnosti.

Navíc, protože nyní máme automatické dokončování bash, můžete cesty Linuxu automaticky doplňovat přímo v PowerShellu!

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

V případech, kdy automatické dokončování bash nepřinese žádné výsledky, PowerShell se vrátí k výchozím cestám systému Windows. V praxi tak můžete podle svého uvážení používat obě cesty současně.

Závěr

Pomocí PowerShell a WSL můžeme integrovat příkazy Linuxu do Windows jako nativní aplikace. Není třeba hledat sestavení Win32 nebo nástroje Linuxu nebo přerušovat pracovní postup přechodem do prostředí Linux. Prostě nainstalovat WSL, konfigurovat Profil PowerShellu и vypište příkazy, které chcete importovat! Bohaté automatické doplňování pro parametry příkazů a cesty k souborům pro Linux a Windows je funkce, která dnes není dostupná ani v nativních příkazech Windows.

K dispozici je úplný zdrojový kód popsaný výše, stejně jako další pokyny pro jeho začlenění do vašeho pracovního postupu zde.

Které příkazy Linuxu považujete za nejužitečnější? Jaké další běžné věci při práci ve Windows chybí? Napište do komentářů popř na GitHub!

Zdroj: www.habr.com

Přidat komentář