Integracja poleceń systemu Linux z systemem Windows przy użyciu PowerShell i WSL

Typowe pytanie programistów Windows: „Dlaczego nadal nie ma <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Niezależnie od tego, czy jest to potężne uderzenie less lub znane narzędzia grep lub sed, programiści systemu Windows chcą mieć łatwy dostęp do tych poleceń w swojej codziennej pracy.

Podsystem Windows dla systemu Linux (WSL) zrobił w tym zakresie ogromny krok naprzód. Umożliwia wywoływanie poleceń systemu Linux z systemu Windows poprzez przesyłanie ich przez serwer proxy wsl.exe (Np wsl ls). Chociaż jest to znacząca poprawa, opcja ta ma wiele wad.

  • Wszechobecny dodatek wsl nudne i nienaturalne.
  • Ścieżki systemu Windows w argumentach nie zawsze działają, ponieważ ukośniki odwrotne są interpretowane jako znaki ucieczki, a nie jako separatory katalogów.
  • Ścieżki systemu Windows w argumentach nie są tłumaczone na odpowiedni punkt podłączenia w WSL.
  • Ustawienia domyślne nie są przestrzegane w profilach WSL z aliasami i zmiennymi środowiskowymi.
  • Uzupełnianie ścieżki w systemie Linux nie jest obsługiwane.
  • Kończenie poleceń nie jest obsługiwane.
  • Uzupełnianie argumentów nie jest obsługiwane.

W rezultacie polecenia systemu Linux są traktowane w systemie Windows jak obywatele drugiej kategorii i są trudniejsze w użyciu niż polecenia natywne. Aby zrównać ich prawa, konieczne jest rozwiązanie wymienionych problemów.

Opakowania funkcji programu PowerShell

Dzięki opakowaniu funkcji programu PowerShell możemy dodać uzupełnianie poleceń i wyeliminować potrzebę stosowania przedrostków wsl, tłumacząc ścieżki systemu Windows na ścieżki WSL. Podstawowe wymagania dotyczące muszli:

  • Dla każdego polecenia systemu Linux musi istnieć jedno opakowanie funkcji o tej samej nazwie.
  • Powłoka musi rozpoznać ścieżki systemu Windows przekazane jako argumenty i przekonwertować je na ścieżki WSL.
  • Powłoka powinna wywołać wsl za pomocą odpowiedniego polecenia systemu Linux do dowolnego wejścia potoku i przekazania argumentów wiersza poleceń przekazanych do funkcji.

Ponieważ ten wzorzec można zastosować do dowolnego polecenia, możemy wyodrębnić definicję tych opakowań i dynamicznie wygenerować je z listy poleceń do zaimportowania.

# 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 definiuje polecenia importu. Następnie dynamicznie generujemy opakowanie funkcji dla każdego z nich za pomocą polecenia Invoke-Expression (najpierw usuwając wszelkie aliasy, które mogłyby kolidować z funkcją).

Funkcja iteruje po argumentach wiersza poleceń, określa ścieżki Windows za pomocą poleceń Split-Path и Test-Patha następnie konwertuje te ścieżki na ścieżki WSL. Ścieżki uruchamiamy za pomocą funkcji pomocniczej Format-WslArgument, które zdefiniujemy później. Ucieka od znaków specjalnych, takich jak spacje i nawiasy, które w przeciwnym razie zostałyby błędnie zinterpretowane.

Na koniec przekazujemy wsl dane wejściowe potoku i dowolne argumenty wiersza poleceń.

Za pomocą tych opakowań możesz wywoływać swoje ulubione polecenia Linuksa w bardziej naturalny sposób, bez dodawania przedrostka wsl i nie martwiąc się o sposób konwersji ścieżek:

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

Tutaj pokazano podstawowy zestaw poleceń, ale możesz utworzyć powłokę dla dowolnego polecenia systemu Linux, po prostu dodając je do listy. Jeśli dodasz ten kod do swojego profil PowerShell, te polecenia będą dostępne w każdej sesji PowerShell, podobnie jak polecenia natywne!

Ustawienia domyślne

W systemie Linux powszechne jest definiowanie aliasów i/lub zmiennych środowiskowych w profilach logowania, ustawiając domyślne parametry dla często używanych poleceń (na przykład alias ls=ls -AFh lub export LESS=-i). Jedna z wad proxy przez nieinteraktywną powłokę wsl.exe - że profile nie są ładowane, więc te opcje nie są domyślnie dostępne (tzn. ls w WSL i wsl ls będzie zachowywał się inaczej z aliasem zdefiniowanym powyżej).

PowerShell zapewnia Wartości parametrów $PSDefault, standardowy mechanizm definiowania parametrów domyślnych, ale tylko dla poleceń cmdlet i funkcji zaawansowanych. Oczywiście możemy tworzyć zaawansowane funkcje z naszych powłok, ale wprowadza to niepotrzebne komplikacje (na przykład PowerShell koreluje częściowe nazwy parametrów (na przykład -a koreluje z -ArgumentList), co będzie kolidować z poleceniami Linuksa, które jako argumenty przyjmują częściowe nazwy), a składnia definiowania wartości domyślnych nie będzie najwłaściwsza (argumenty domyślne wymagają nazwy parametru w kluczu, a nie tylko nazwy polecenia) .

Jednak po niewielkiej modyfikacji naszych muszli możemy wdrożyć model podobny do $PSDefaultParameterValuesi włącz domyślne opcje poleceń systemu 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 ' ')
    }
}

Przechodzący $WslDefaultParameterValues do linii poleceń wysyłamy parametry poprzez wsl.exe. Poniżej pokazano, jak dodać instrukcje do profilu PowerShell, aby skonfigurować ustawienia domyślne. Teraz możemy to zrobić!

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

Ponieważ parametry są wzorowane $PSDefaultParameterValues, Możesz łatwo je wyłączyć tymczasowo, instalując klucz "Disabled" w znaczenie $true. Dodatkową zaletą oddzielnej tabeli mieszającej jest możliwość wyłączenia $WslDefaultParameterValues oddzielnie od $PSDefaultParameterValues.

Uzupełnianie argumentu

PowerShell umożliwia rejestrację zwiastunów argumentów za pomocą polecenia Register-ArgumentCompleter. Bash ma moc programowalne narzędzia do automatycznego uzupełniania. WSL umożliwia wywoływanie bash z PowerShell. Jeśli uda nam się zarejestrować uzupełnienia argumentów dla opakowań funkcji PowerShell i wywołać bash w celu wygenerowania uzupełnień, otrzymamy pełne uzupełnienie argumentów z taką samą precyzją jak sam 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]
    }
}

Kod jest nieco gęsty i nie rozumie niektórych wewnętrznych funkcji basha, ale zasadniczo robimy to:

  • Rejestracja modułu uzupełniającego argumenty dla wszystkich naszych opakowań funkcji poprzez przekazanie listy $commands w parametrze -CommandName dla Register-ArgumentCompleter.
  • Mapujemy każde polecenie na funkcję powłoki, której bash używa do autouzupełniania (w celu zdefiniowania specyfikacji autouzupełniania, bash używa $F, skrót od complete -F <FUNCTION>).
  • Konwersja argumentów PowerShell $wordToComplete, $commandAst и $cursorPosition do formatu oczekiwanego przez funkcje autouzupełniania basha zgodnie ze specyfikacjami programowalne automatyczne uzupełnianie grzmotnąć.
  • Tworzymy linię poleceń, do której należy przenieść wsl.exe, który zapewnia poprawną konfigurację środowiska, wywołuje odpowiednią funkcję automatycznego uzupełniania i wyświetla wyniki wiersz po wierszu.
  • Potem dzwonimy wsl za pomocą wiersza poleceń oddzielamy dane wyjściowe separatorami linii i generujemy je dla każdego CompletionResults, sortując je i usuwając znaki ucieczki, takie jak spacje i nawiasy, które w przeciwnym razie zostałyby błędnie zinterpretowane.

W rezultacie nasze powłoki poleceń Linuksa będą używać dokładnie tego samego autouzupełniania, co bash! Na przykład:

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

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

<TAB> będzie przełączać parametry. <Ctrl + пробел> pokaże wszystkie dostępne opcje.

Ponadto, ponieważ mamy teraz autouzupełnianie bash, możesz automatycznie uzupełniać ścieżki Linuksa bezpośrednio w PowerShell!

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

W przypadkach, gdy autouzupełnianie bash nie daje żadnych wyników, PowerShell przywraca domyślne ścieżki systemu Windows. W praktyce zatem można według własnego uznania korzystać z obu ścieżek jednocześnie.

wniosek

Używając PowerShell i WSL, możemy zintegrować polecenia Linuksa z Windowsem jako aplikacje natywne. Nie ma potrzeby wyszukiwania kompilacji Win32 lub narzędzi dla systemu Linux ani przerywania pracy poprzez przechodzenie do powłoki systemu Linux. Tylko zainstaluj WSL, skonfiguruj Profil PowerShella и wypisz polecenia, które chcesz zaimportować! Bogate autouzupełnianie parametrów poleceń systemu Linux i Windows oraz ścieżek plików to funkcjonalność, która nie jest obecnie dostępna nawet w natywnych poleceniach systemu Windows.

Dostępny jest pełny kod źródłowy opisany powyżej, a także dodatkowe wytyczne dotyczące włączenia go do przepływu pracy tutaj.

Które polecenia systemu Linux uważasz za najbardziej przydatne? Jakich innych typowych rzeczy brakuje podczas pracy w systemie Windows? Napisz w komentarzu lub na GitHub!

Źródło: www.habr.com

Dodaj komentarz