Integrujeme týmy Linux do Windows pomocí PowerShell a WSL

Typická otázka vývojářů na Windows: „Proč zde stále není <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?». Будь то мощное пролистывание less nebo běžné nástroje grep nebo sed, vývojáři na Windows chtějí mít snadný přístup k těmto příkazům ve své každodenní práci.

Systém Windows pro Linux (WSL) udělal v tomto ohledu obrovský krok vpřed. Umožňuje vyvolávat příkazy Linuxu z Windows, přesměrovávající je přes wsl.exe (například, wsl ls). I když je to významné zlepšení, tato varianta trpí celou řadou nedostatků.

  • Hromadná přidání wsl jsou únavná a nepřirozená.
  • Cesty Windows v argumentech nefungují vždy správně, protože zpětné lomítka jsou interpretována jako únikové znaky, nikoli jako oddělovače adresářů.
  • Cesty Windows v argumentech se nepřevádějí na odpovídající bod připojení v WSL.
  • Není brán v úvahu výchozí parametry v profilech WSL s aliasy a proměnnými prostředí.
  • Není podporováno dokončení cest Linuxu.
  • Není podporováno dokončení příkazů.
  • Není podporováno dokončení argumentů.

V důsledku toho jsou příkazy Linuxu vnímány v Windows jako občané druhé kategorie — a jejich použití je složitější než u nativních příkazů. Abychom jim dali stejná práva, je třeba vyřešit uvedené problémy.

Funkční shell PowerShell

Pomocí funkčních shellů PowerShell můžeme přidat automatické dokončování příkazů a eliminovat potřebu prefixů wsl, přesměrováním cest Windows na cesty WSL. Hlavní požadavky na shelly jsou:

  • Pro každý příkaz Linux musí být jeden funkční shell se stejným názvem.
  • Shell musí rozpoznat cesty Windows předané jako argumenty a převést je na cesty WSL.
  • Shell musí volat wsl s odpovídajícím příkazem Linuxu na jakýkoli vstup potrubí a předávat jakékoli argumenty příkazového řádku, které byly předány funkci.

Jelikož se tato šablona může použít na jakýkoli příkaz, můžeme abstrahovat definici těchto shellů a dynamicky je generovat ze seznamu příkazů pro 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 ' ')
    }
}
"@
}

Seznam $command určuje příkazy k importu. Poté dynamicky generujeme obalovou funkci pro každou z nich pomocí příkazu Invoke-Expression (nejprve odstraníme jakékoli aliasy, které by mohly kolidovat s funkcí).

Funkce prochází argumenty příkazového řádku, určuje cesty Windows pomocí příkazů Split-Path a Test-Path, a poté tyto cesty převede na cesty WSL. Spouštíme cesty prostřednictvím pomocné funkce Format-WslArgument, kterou definujeme později. Ta escape-uje speciální znaky, jako jsou mezery a závorky, které by jinak byly nesprávně interpretovány.

Nakonec předáme wsl vstupy potrubí a jakékoli argumenty příkazové řádky.

Pomocí takových obalů můžete volat oblíbené příkazy Linuxu přirozenějším způsobem, aniž byste přidávali prefix wsl a nemusíte se obávat, jak se cesty převedou:

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

Zde je uvedena základní sada příkazů, ale můžete vytvořit obal pro jakýkoli příkaz Linuxu, jednoduše jej přidáním do seznamu. Pokud tento kód přidáte do svého profilu PowerShell, tyto příkazy budou dostupné v každé relaci PowerShell, stejně jako nativní příkazy!

Výchozí parametry

V Linuxu je běžné definovat aliasy a/nebo proměnné prostředí v profilech (login profile), které určují výchozí parametry pro často používané příkazy (například, alias ls=ls -AFh nebo export LESS=-i). Jednou z nevýhod proxy přes neinteraktivní shell wsl.exe je, že profily se nenakládají, a proto tyto výchozí parametry nejsou dostupné (tj. ls ve WSL a wsl ls se budou chovat jinak s aliasem definovaným výše).

PowerShell poskytuje $PSDefaultParameterValues, standardní mechanismus pro definici výchozích parametrů, ale pouze pro cmdlety a rozšířené funkce. Samozřejmě, můžeme z našich shellů udělat rozšířené funkce, ale to přináší zbytečné komplikace (například, PowerShell přiřazuje částečné názvy parametrů (například, -a přiřazuje k -ArgumentList), které budou kolidovat s příkazy Linuxu akceptujícími částečné názvy jako argumenty), a syntaxe pro definování výchozích hodnot nebude nejvhodnější (pro určení výchozích argumentů je potřeba název parametru v klíči, nikoli pouze jméno příkazu).

S drobnou úpravou našich shellů však můžeme implementovat model podobný $PSDefaultParameterValues, a zahrnout výchozí parametry pro příkazy Linux!

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

Předáváním $WslDefaultParameterValues do příkazového řádku posíláme parametry pomocí wsl.exe. Níže je uvedeno, jak přidat pokyny do profilu PowerShell pro nastavení výchozích hodnot parametrů. Teď to můžeme udělat!

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

Protože jsou parametry modelovány podle $PSDefaultParameterValues, můžete snadno je vypnout dočasně nastavením klíče "Disabled" na hodnotu $true. Další výhodou samostatné hash tabulky je možnost vypnout $WslDefaultParameterValues samostatně od $PSDefaultParameterValues.

Doplňování argumentů

PowerShell umožňuje registrovat dokončovače argumentů pomocí příkazu Register-ArgumentCompleter. V Bash existují mocné programovatelné nástroje pro doplňování. WSL umožňuje volat bash z PowerShellu. Pokud můžeme registrovat dokončovače argumentů pro naše funkce PowerShell a zavolat bash pro vytváření dokončení, získáme úplné doplňování argumentů s přesností jako v samotném bash!

# 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 zásadě děláme toto:

  • Registrujeme dokončovač argumentů pro všechny naše obálky funkcí, předáváme seznam $commands do parametru -CommandName pro Register-ArgumentCompleter.
  • Mapujeme každý příkaz na funkci shellu, kterou bash používá pro doplňování automaticky (pro určení specifikací doplňování v bash se používá $F, zkrácení pro complete -F).
  • Převádíme argumenty PowerShell $wordToComplete, $commandAst a $cursorPosition do formátu, který očekávají funkce doplňování bash podle specifikací programovatelného doplňování bash.
  • Sestavujeme příkazový řádek pro předání do wsl.exe, který zajišťuje správné nastavení prostředí, volá příslušnou funkci doplňování a zobrazuje výsledky s rozdělením po řádcích.
  • Poté voláme wsl s příkazovým řádkem, dělíme výstup podle oddělovačů řádků a generujeme pro každé CompletionResults, třídíme je a escapujeme znaky, jako jsou mezery a závorky, které by jinak byly nesprávně interpretovány.

Nakonec naše shellové příkazy Linuxu budou používat stejné doplňování jako v bash! Například:

  • ssh -c -J -m -O -o -Q -w -b

Každé automatické dokončení poskytuje hodnoty specifické pro předchozí argument, čte nastavení, jako jsou známé hostitele, z WSL!

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

Kromě toho, že nyní máme funkční automatické dokončení bash, můžete automaticky dokončovat cesty Linuxu přímo v PowerShellu!

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

V případech, kdy automatické dokončení bash nevrací žádné výsledky, se PowerShell vrací k výchozímu systému s cestami Windows. Tímto způsobem můžete prakticky současně používat obě cesty podle svého uvážení.

Závěr

S PowerShellem a WSL můžeme integrovat příkazy Linuxu do Windows jako nativní aplikace. Není třeba hledat Win32 verze nebo linuxové nástroje, ani přerušovat pracovní tok přechodem do linuxového shellu. Stačí nainstalovat WSL, nakonfigurovat profil PowerShell a vypíše příkazy, které chcete importovat! Bohaté automatické dokončení pro parametry příkazů a cesty k souborům Linuxu a Windows – to je funkce, kterou dnes nemají ani nativní příkazy Windows.

Celý zdrojový kód uvedený výše a také další doporučení k jeho zahrnutí do pracovního postupu jsou k dispozici zde.

Jaké příkazy Linuxu považujete za nejvíce užitečné? Jaké další běžné věci postrádáte při práci ve Windows? Napište komentáře nebo na GitHubu!

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster