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
parametruRegister-ArgumentCompleter
- Mapujemy każde polecenie do funkcji powłoki, której bash używa do wykonania dla niego (
$F
, która jest nazwana pocomplete -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 generujemyCompletionResults
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!
Mike Battista
Starszy kierownik programu, Windows Developer Platform
Śledź