Integrujemy polecenia Linux w Windows za pomocą PowerShell i WSL

Typowe pytanie programistów Windows: „Dlaczego tutaj wciąż nie ma <WSTAW TUTAJ ULUBIONĄ KOMENDĘ LINUX>?». Будь то мощное пролистывание less lub znanych narzędzi grep lub sed, programiści Windows chcą uzyskać łatwy dostęp do tych komend w codziennej pracy.

Podsystem Windows dla systemu Linux (WSL) zrobił ogromny krok naprzód w tym zakresie. Umożliwia wywoływanie poleceń Linux z Windows, przesyłając je przez wsl.exe (na przykład, wsl ls). Mimo że to znaczna poprawa, to takie rozwiązanie ma szereg wad.

  • Powszechne dodawanie wsl jest męczące i nienaturalne.
  • Ścieżki Windows w argumentach nie zawsze działają, ponieważ odwrotne ukośniki są interpretowane jako znaki ucieczki, a nie jako separator katalogów.
  • Ścieżki Windows w argumentach nie są tłumaczone na odpowiedni punkt montowania w WSL.
  • Nie uwzględnia domyślnych parametrów w profilach WSL z aliasami i zmiennymi środowiskowymi.
  • Nie obsługuje uzupełniania ścieżek Linux.
  • Nie obsługuje uzupełniania komend.
  • Nie obsługuje uzupełniania argumentów.

W rezultacie polecenia Linux są postrzegane w Windows jako obywatele drugiej kategorii — trudniejsze w użyciu niż native commands. Aby wyrównać ich prawa, należy rozwiązać wymienione problemy.

Funkcje powłok PowerShell

Dzięki funkcjom powłok PowerShell możemy dodać automatyczne uzupełnianie poleceń i wyeliminować potrzebę prefiksów wsl, tłumacząc ścieżki Windows na ścieżki WSL. Podstawowe wymagania dla powłok to:

  • Dla każdego polecenia Linux powinna być jedna powłoka funkcji o tej samej nazwie.
  • Powłoka powinna rozpoznawać ścieżki Windows przekazywane jako argumenty i przekształcać je w ścieżki WSL.
  • Powłoka powinna wywoływać wsl z odpowiednim poleceniem Linux dla każdego wejścia potoku i przekazując wszelkie argumenty wiersza poleceń przekazywane do funkcji.

Ponieważ ten szablon może być zastosowany do dowolnego polecenia, możemy abstrahować definicję tych powłok i dynamicznie je generować z listy poleceń do 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 ' ')
    }
}
"@
}

Lista $command określa polecenia do importu. Następnie dynamicznie generujemy opakowanie funkcji dla każdego z nich, używając polecenia Invoke-Expression (najpierw usuwając wszelkie aliasy, które mogą kolidować z funkcją).

Funkcja przeszukuje argumenty wiersza poleceń, określa ścieżki Windows za pomocą poleceń Split-Path i Test-Path, a następnie konwertuje te ścieżki na ścieżki WSL. Przechodzimy przez ścieżki za pomocą funkcji pomocniczej Format-WslArgument, którą określimy później. Escapuje ona specjalne znaki, takie jak spacje i nawiasy, które w przeciwnym razie mogłyby być błędnie zinterpretowane.

Na koniec przekazujemy wsl dane wejściowe potoku oraz wszelkie argumenty wiersza poleceń.

Dzięki takim wrapperom można wywoływać ulubione komendy Linuxa w bardziej naturalny sposób, bez dodawania prefiksu wsl i bez martwienia się, jak są konwertowane ścieżki:

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

Tutaj przedstawiony jest podstawowy zestaw komend, ale możesz stworzyć wrapper dla dowolnej komendy Linuxa, po prostu dodając ją do listy. Jeśli dodasz ten kod do swojego profilu PowerShell, te komendy będą dostępne w każdej sesji PowerShell, tak jak komendy natywne!

Domyślne parametry

W Linuxie powszechnie określa się aliasy i/lub zmienne środowiskowe w profilach (profil logowania), ustalając domyślne parametry dla często używanych komend (na przykład, alias ls=ls -AFh lub export LESS=-i). Jedną z wad proxy przez nieinteraktywną powłokę wsl.exe jest to, że profile się nie ładują, więc te domyślne parametry są niedostępne (tzn. ls w WSL i wsl ls będą zachowywać się różnie z aliasem określonym powyżej).

PowerShell udostępnia $PSDefaultParameterValues, standardowy mechanizm definiowania domyślnych parametrów, ale tylko dla cmdletów i zaawansowanych funkcji. Oczywiście, możemy z naszych powłok zrobić zaawansowane funkcje, ale wprowadza to dodatkowe komplikacje (tak, PowerShell odnosi się do częściowych nazw parametrów (na przykład, -a odnosi się do -ArgumentList), które będą kolidować z komendami Linuxa akceptującymi częściowe nazwy jako argumenty), a składnia do definiowania domyślnych wartości nie będzie najbardziej odpowiednia (do określenia argumentów domyślnych wymagane jest podanie nazwy parametru w kluczu, a nie tylko nazwy komendy).

Jednak dzięki niewielkiej zmianie naszych powłok możemy wdrożyć model podobny do $PSDefaultParameterValues, i włączyć domyślne parametry dla komend Linuxa!

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

Przekazując $WslDefaultParameterValues w terminalu przesyłamy parametry przez wsl.exe. Poniżej pokazano, jak dodać instrukcje do profilu PowerShell w celu skonfigurowania wartości domyślnych. Teraz możemy to zrobić!

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

Ponieważ parametry są modelowane po $PSDefaultParameterValues, możesz łatwo je wyłączyć na chwilę, ustawiając klucz "Disabled" na wartość $true. Dodatkową zaletą osobnej tablicy mieszanej jest możliwość wyłączenia $WslDefaultParameterValues oddzielnie od $PSDefaultParameterValues.

Automatyczne uzupełnianie argumentów

PowerShell pozwala rejestrować uzupełniacze argumentów za pomocą polecenia Register-ArgumentCompleter. W Bash są potężne programowalne narzędzia do automatycznego uzupełniania. WSL pozwala wywoływać bash z PowerShell. Jeśli możemy zarejestrować uzupełniacze argumentów dla naszych funkcji w PowerShell i wywołać bash w celu utworzenia uzupełnień, uzyskamy pełne automatyczne uzupełnienie argumentów z taką samą precyzją, jak w samym bashu!

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

Kod jest nieco zbity bez zrozumienia niektórych wewnętrznych funkcji bash, ale w zasadzie robimy to:

  • Rejestrujemy uzupełniacz argumentów dla wszystkich naszych funkcji opakowujących, przekazując listę $commands do parametru -CommandName do Register-ArgumentCompleter.
  • Mapujemy każdą komendę z funkcją powłoki, którą bash używa do automatycznego uzupełnienia (do określenia specyfikacji automatycznego uzupełniania w bash używa się $F, skrót od complete -F).
  • Przekształcamy argumenty PowerShell $wordToComplete, $commandAst i $cursorPosition w format oczekiwany przez funkcje automatycznego uzupełniania bash zgodnie ze specyfikacjami programowalnego automatycznego uzupełniania bash.
  • Składamy polecenie do przekazania do wsl.exe, które zapewnia poprawną konfigurację środowiska, wywołuje odpowiednią funkcję automatycznego uzupełnienia i zwraca wyniki z podziałem na linie.
  • Następnie wywołujemy wsl z poleceniem, dzielimy wynik na podstawie końców linii i generujemy dla każdego CompletionResults, sortując je i eskapując znaki, takie jak spacje i nawiasy, które w przeciwnym razie byłyby błędnie interpretowane.

W rezultacie nasze powłoki komend Linux będą używać dokładnie takiego samego uzupełnienia automatycznego, jak w bashu! Na przykład:

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

Każde autouzpełnienie dostarcza wartości specyficzne dla wcześniejszego argumentu, odczytując dane konfiguracyjne, takie jak znane hosty, z WSL!

<TAB> będzie cyklicznie iterować przez parametry. <Ctrl + пробел> wyświetli wszystkie dostępne opcje.

Ponadto, ponieważ teraz mamy działające autouzpełnienie bash, możesz autouzpełniać ścieżki Linuxa bezpośrednio w PowerShell!

  • less /etc/
  • ls /usr/share/
  • vim ~/.ash

W przypadkach, gdy autouzpełnienie bash nie dostarcza żadnych wyników, PowerShell powraca do systemu domyślnego z ścieżkami Windows. Tak więc, w praktyce możesz używać jednocześnie zarówno tych, jak i innych ścieżek według własnego uznania.

Podsumowanie

Dzięki PowerShell i WSL możemy integrować polecenia Linuxa w Windows jako natywne aplikacje. Nie ma potrzeby szukania kompilacji Win32 lub narzędzi Linuxa ani przerywania pracy, przechodząc do powłoki Linux. Po prostu zainstaluj WSL, skonfiguruj profil PowerShell i wylistuj polecenia, które chcesz zaimportować! Bogate autouzpełnienie dla parametrów poleceń i ścieżek plików Linux i Windows to funkcjonalność, której dziś nie ma nawet w natywnych poleceniach Windows.

Pełny kod źródłowy, opisany powyżej, a także dodatkowe zalecenia dotyczące jego włączenia do procesu roboczego są dostępne tutaj.

Jakie polecenia Linuxa uważasz za najbardziej przydatne? Jakich jeszcze znanych funkcji brakuje podczas pracy w Windows? Napisz w komentarzach lub na GitHubie!

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster