Avatar

Mike

26 września, 2019

Częstym pytaniem programistów Windows jest „dlaczego Windows nie ma jeszcze <INSERT FAVORITE LINUX COMMAND HERE>?”. Niezależnie od tego, czy tęsknisz za potężnym pagerem, takim jak less, czy chcesz używać znanych poleceń, takich jak grep lub sed, programiści Windows pragną łatwego dostępu do tych poleceń jako części ich podstawowego przepływu pracy.

System Windows dla Linuksa (WSL) był ogromnym krokiem naprzód w tej dziedzinie, umożliwiając programistom wywoływanie poleceń Linuksa z systemu Windows przez pośredniczenie w nich za pomocą wsl.exe (np. wsl ls). Chociaż jest to znacząca poprawa, doświadczeniu brakuje kilku sposobów:

  • Prefiksowanie poleceń za pomocą wsl jest uciążliwe i nienaturalne
  • Ścieżki Windows przekazywane jako argumenty nie są często rozwiązywane z powodu backslashes interpretowanych jako znaki ucieczki, a nie separatory katalogów
  • Ścieżki Windows przekazywane jako argumenty nie są często rozwiązywane z powodu braku tłumaczenia na odpowiedni punktu montowania w WSL
  • Domyślne parametry zdefiniowane w profilach logowania WSL z aliasami i zmiennymi środowiskowymi nie są honorowane
  • Uzupełnianie ścieżek w systemie Linux nie jest obsługiwane
  • Uzupełnianie poleceń nie jest obsługiwane
  • Uzupełnianie argumentów nie jest obsługiwane

Wynikiem tych braków jest to, że polecenia linuksowe czują się jak obywatele drugiej klasy w stosunku do Windows.obywateli drugiej kategorii w stosunku do Windows i są trudniejsze w użyciu niż powinny być. Aby komenda czuła się jak natywna komenda Windows, musimy zająć się tymi problemami.

PowerShell Function Wrappers

Możemy usunąć potrzebę prefiksowania komend za pomocą wsl, obsługiwać tłumaczenie ścieżek Windows na ścieżki WSL i obsługiwać uzupełnianie komend za pomocą PowerShell function wrapperów. Podstawowe wymagania dotyczące wrapperów to:

  • Powinien istnieć jeden wrapper funkcji na każde polecenie linuksowe o tej samej nazwie co polecenie
  • Wrapper powinien rozpoznawać ścieżki Windows przekazywane jako argumenty i tłumaczyć je na ścieżki WSL
  • Wrapper powinien wywoływać wsl z odpowiadającym mu poleceniem linuksowym, przekazując wszelkie dane wejściowe do potoku i przekazując wszelkie argumenty wiersza poleceń przekazane do funkcji

Ponieważ ten szablon może być zastosowany do dowolnej komendy, możemy abstrahować od definicji tych wrapperów i generować je dynamicznie z listy komend 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 Ignorefunction 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 -IsAbsolute -ErrorAction Ignore) { `$args = Format-WslArgument (wsl.exe wslpath (`$args -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 -ErrorAction Ignore) { `$args = Format-WslArgument (`$args -replace "\\", "/") } } if (`$input.MoveNext()) { `$input.Reset() `$input | wsl.exe $_ (`$args -split ' ') } else { wsl.exe $_ (`$args -split ' ') }}"@}

Lista $command definiuje komendy do zaimportowania. Następnie dynamicznie generujemy wrapper funkcji dla każdego z nich używając polecenia Invoke-Expression (najpierw usuwając wszelkie aliasy, które mogłyby kolidować z funkcją).

Funkcja zapętla się przez argumenty wiersza poleceń, identyfikuje ścieżki Windows używając poleceń Split-Path i Test-Path, następnie konwertuje te ścieżki na ścieżki WSL. Przepuszczamy ścieżki przez funkcję pomocniczą, którą zdefiniujemy później, zwaną Format-WslArgument, która ucieka od znaków specjalnych, takich jak spacje i nawiasy, które w przeciwnym razie zostałyby źle zinterpretowane.

Na koniec przekazujemy dane wejściowe rurociągu i wszelkie argumenty wiersza poleceń do wsl.

Po wprowadzeniu tych funkcji możemy teraz wywoływać nasze ulubione polecenia linuksowe w bardziej naturalny sposób, bez konieczności poprzedzania ich wsl lub martwienia się o to, jak ścieżki Windows są tłumaczone na ścieżki WSL:

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

Zestaw startowy komend jest pokazany tutaj, ale możesz wygenerować wrapper dla dowolnej komendy Linuksa po prostu dodając ją do listy. Jeśli dodasz ten kod do swojego profilu PowerShell, polecenia te będą dostępne w każdej sesji PowerShell tak jak rodzime polecenia!

Parametry domyślne

W Linuksie często definiuje się aliasy i/lub zmienne środowiskowe w profilach logowania, aby ustawić domyślne parametry dla poleceń, których często używamy (np. alias ls=ls -AFh lub export LESS=-i). Jedną z wad prokserowania przez nieinteraktywną powłokę za pośrednictwem wsl.exe jest to, że profile logowania nie są ładowane, więc te domyślne parametry nie są dostępne (tj. ls w ramach WSL i wsl ls zachowywałyby się inaczej ze zdefiniowanym powyżej aliasem).

PowerShell udostępnia $PSDefaultParameterValues, standardowy mechanizm definiowania domyślnych wartości parametrów, ale tylko dla cmdletów i funkcji zaawansowanych. Przekształcenie naszych wrapperów funkcji w funkcje zaawansowane jest możliwe, ale wprowadza komplikacje (np. PowerShell dopasowuje częściowe nazwy parametrów (jak np. dopasowanie -a do -ArgumentList), co będzie kolidowało z poleceniami Linuksa, które akceptują częściowe nazwy jako argumenty), a składnia do definiowania wartości domyślnych byłaby mniej niż idealna dla tego scenariusza (wymagając nazwy parametru w kluczu do definiowania domyślnych argumentów, w przeciwieństwie do samej nazwy polecenia).

Dzięki małej zmianie w naszych wrapperach funkcji, możemy wprowadzić model podobny do $PSDefaultParameterValues i włączyć domyślne parametry dla komend Linuksa!

function global:$_() { … `$defaultArgs = ((`$WslDefaultParameterValues.$_ -split ' '), "") if (`$input.MoveNext()) { `$input.Reset() `$input | wsl.exe $_ `$defaultArgs (`$args -split ' ') } else { wsl.exe $_ `$defaultArgs (`$args -split ' ') }}

Przekazując $WslDefaultParameterValues w dół do wiersza poleceń, który wysyłamy przez wsl.exe, możesz teraz dodać oświadczenia takie jak poniżej do swojego profilu PowerShell, aby skonfigurować domyślne parametry!

$WslDefaultParameterValues = "-E"$WslDefaultParameterValues = "-i"$WslDefaultParameterValues = "-AFh --group-directories-first" 

Ponieważ jest to wzorowane na $PSDefaultParameterValues, możesz je tymczasowo wyłączyć w prosty sposób, ustawiając klucz "Disabled" na $true. Oddzielna tablica hash ma dodatkową zaletę, że można wyłączyć $WslDefaultParameterValues oddzielnie od $PSDefaultParameterValues.

Uzupełnianie argumentów

PowerShell pozwala rejestrować dopełniacze argumentów za pomocą polecenia Register-ArgumentCompleter. Bash ma potężne programowalne obiekty uzupełniania. WSL pozwala na wywołanie basha z PowerShella. Jeśli możemy zarejestrować dopełniacze argumentów dla naszych wrapperów funkcji PowerShell i wywołać bash do wygenerowania dopełnień, możemy uzyskać bogate uzupełnianie argumentów z taką samą wiernością 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.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.Extent if ($cursorPosition -lt $extent.EndColumnNumber) { # The cursor is in the middle of a word to complete. $previousWord = $commandAst.CommandElements.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.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.Extent $previousExtent = $commandAst.CommandElements.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.Extent.Text $COMP_CWORD -= 1 } # Build the command to pass to WSL. $command = $commandAst.CommandElements.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 + $_) $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 ::new($completionText, $listItemText, 'ParameterName', $completionText) }}# Helper function to escape characters in arguments passed to WSL that would otherwise be misinterpreted.function global:Format-WslArgument($arg, $interactive) { if ($interactive -and $arg.Contains(" ")) { return "'$arg'" } else { return ($arg -replace " ", "\ ") -replace "()", ('\$1', '`$1') }}

Kod jest trochę gęsty bez zrozumienia niektórych elementów wewnętrznych basha, ale w zasadzie:

  • Rejestrujemy dopełniacz argumentów dla wszystkich naszych wrapperów funkcji, przekazując listę $commands do -CommandName parametru Register-ArgumentCompleter
  • Mapujemy każde polecenie do funkcji powłoki, której bash używa do wykonania dla niego ($F, która jest nazwana po complete -F <FUNCTION> używanej do definiowania specyfikacji uzupełniania w bash)
  • Przekształcamy PowerShell’owskie $wordToComplete$commandAst, i $cursorPosition na format oczekiwany przez funkcje uzupełniania basha zgodnie ze specyfikacją programowalnego uzupełniania basha
  • Budujemy wiersz poleceń, który możemy przekazać do wsl.exe, który zapewnia, że środowisko uzupełniania jest ustawione poprawnie, wywołuje odpowiednią funkcję uzupełniania, a następnie wypisuje łańcuch zawierający wyniki uzupełniania oddzielone nowymi liniami
  • Wywołujemy następnie wsl z wiersza poleceń, dzieląc łańcuch wyjściowy na separator nowej linii, następnie generujemy CompletionResults dla każdego z nich, sortując je i uciekając od znaków takich jak spacje i nawiasy, które w przeciwnym razie zostałyby źle zinterpretowane

Efektem końcowym tego jest to, że teraz nasze linuksowe wrappery poleceń będą używać dokładnie tego samego uzupełnienia, którego używa bash! Na przykład:

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

Każde uzupełnienie dostarczy wartości specyficzne dla argumentu przed nim, wczytując dane konfiguracyjne takie jak znane hosty z WSL!

<TAB> będzie cyklicznie przechodzić przez opcje. <Ctrl + Space> pokaże wszystkie dostępne opcje.

Dodatkowo, ponieważ uzupełnianie basha jest teraz odpowiedzialne, możesz rozwiązywać ścieżki do Linuksa bezpośrednio w PowerShell!

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

W przypadkach, gdy bash completion nie zwraca żadnych wyników, PowerShell powraca do domyślnego uzupełniania, które rozwiązuje ścieżki Windows, efektywnie umożliwiając rozwiązywanie ścieżek zarówno dla Linuksa jak i Windows.

Podsumowanie

Dzięki PowerShell i WSL możemy zintegrować komendy linuksowe z Windows tak, jakby były natywnymi aplikacjami. Nie ma potrzeby polować na kompilacje Win32 narzędzi linuksowych lub być zmuszonym do przerwania pracy, aby przejść do powłoki Linuksa. Wystarczy zainstalować WSL, skonfigurować profil PowerShell i wypisać komendy, które chcemy zaimportować! Pokazane tutaj uzupełnianie argumentów zarówno opcji poleceń, jak i ścieżek plików Linuksa i Windows jest doświadczeniem, którego nie zapewniają nawet natywne polecenia Windows.

Pełny kod źródłowy opisany powyżej, jak również dodatkowe wskazówki dotyczące włączenia go do Twojego przepływu pracy są dostępne na stronie https://github.com/mikebattista/PowerShell-WSL-Interop.

Które polecenia Linuksa uważasz za najbardziej przydatne? Jakich innych części przepływu pracy dewelopera brakuje Ci na Windowsie?

Daj nam znać w komentarzach poniżej lub na GitHubie!

Avatar
Mike Battista

Starszy kierownik programu, Windows Developer Platform

Śledź

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *