Eine typische Windows-Entwicklerfrage: „Warum ist es immer noch nicht hier
< LINUX>
?“. Unabhängig davon, ob es sich um leistungsstarkes Scrollen oder um die üblichen
grep
oder
sed
Tools handelt, möchten Windows-Entwickler bei der täglichen Arbeit einen einfachen Zugriff auf diese Befehle.
Das Windows-Subsystem für Linux (WSL) hat in dieser Hinsicht einen großen Schritt nach vorne gemacht. Sie können Linux-Befehle von Windows aus aufrufen und über
wsl.exe
(z. B.
wsl ls
)
wsl ls
. Obwohl dies eine signifikante Verbesserung darstellt, weist diese Option eine Reihe von Nachteilen auf.
- Die allgegenwärtige Zugabe von
wsl
anstrengend und unnatürlich.
- Windows-Pfade in den Argumenten funktionieren nicht immer, da Backslashes als Escape-Zeichen und nicht als Verzeichnis-Trennzeichen interpretiert werden.
- Windows-Pfade in Argumenten werden nicht in den entsprechenden Einhängepunkt in der WSL übersetzt.
- Die Standardeinstellungen in WSL-Profilen mit Aliasnamen und Umgebungsvariablen werden nicht berücksichtigt.
- Die Vervollständigung des Linux-Pfads wird nicht unterstützt.
- Die Befehlsvervollständigung wird nicht unterstützt.
- Die Vervollständigung von Argumenten wird nicht unterstützt.
Infolgedessen werden Linux-Befehle unter Windows als Bürger zweiter Klasse wahrgenommen - und sie sind schwieriger zu verwenden als native Teams. Um ihre Rechte auszugleichen, müssen Sie diese Probleme lösen.
PowerShell-Shells
Mithilfe von PowerShell-Funktions-Wrappern können wir die Befehlsvervollständigung hinzufügen und die Notwendigkeit von
wsl
Präfixen beseitigen, indem wir Windows-Pfade in WSL-Pfade übersetzen. Grundvoraussetzungen für Muscheln:
- Jeder Linux-Befehl muss eine Shell der Funktion mit demselben Namen haben.
- Die Shell muss die als Argumente übergebenen Windows-Pfade erkennen und in WSL-Pfade konvertieren.
- Die Shell sollte
wsl
mit dem entsprechenden Linux-Befehl für jede Pipeline-Eingabe aufrufen und alle an die Funktion übergebenen Befehlszeilenargumente übergeben.
Da diese Vorlage auf jeden Befehl angewendet werden kann, können wir die Definition dieser Shells abstrahieren und sie dynamisch aus der Liste der zu importierenden Befehle generieren.
# 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 (eg 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 ' ') } } "@ }
Die
$command
definiert die zu importierenden Befehle. Anschließend generieren wir mit dem Befehl
Invoke-Expression
dynamisch einen Funktions-Wrapper für jeden von ihnen (entfernen Sie zuerst alle Aliase, die mit der Funktion in Konflikt stehen).
Die Funktion durchläuft die Befehlszeilenargumente, ermittelt die Windows-Pfade mithilfe der Befehle
Split-Path
und
Test-Path
und konvertiert diese Pfade dann in WSL-Pfade. Wir führen die Pfade durch die
Format-WslArgument
, die wir später definieren. Es entgeht Sonderzeichen wie Leerzeichen und Klammern, die sonst falsch interpretiert würden.
Schließlich übergeben wir die
wsl
Eingabe und alle Befehlszeilenargumente an
wsl
.
Mit diesen Wrappern können Sie Ihre bevorzugten Linux-Befehle auf natürlichere Weise
wsl
, ohne das Präfix
wsl
hinzuzufügen und ohne sich Gedanken darüber zu machen, wie die Pfade konvertiert werden:
man bash
less -i $profile.CurrentUserAllHosts
ls -Al C:\Windows\ | less
grep -Ein error *.log
tail -f *.log
Der grundlegende Befehlssatz wird hier angezeigt. Sie können jedoch eine Shell für jeden Linux-Befehl erstellen, indem Sie ihn einfach zur Liste hinzufügen. Wenn Sie diesen Code zu Ihrem PowerShell-
Profil hinzufügen, stehen Ihnen diese Befehle in jeder PowerShell-Sitzung zur Verfügung, ebenso wie die nativen Befehle!
Standardoptionen
Unter Linux ist es üblich, Aliase und / oder Umgebungsvariablen in Profilen (Anmeldeprofil) zu definieren und Standardparameter für häufig verwendete Befehle
alias ls=ls -AFh
(z. B.
alias ls=ls -AFh
oder
export LESS=-i
). Einer der Nachteile des Proxys über die nicht interaktive
wsl.exe
Shell besteht darin, dass keine Profile geladen werden. Daher sind diese Optionen standardmäßig nicht verfügbar (d. H.
wsl ls
in WSL und
wsl ls
verhalten sich mit dem oben definierten Alias unterschiedlich).
PowerShell bietet
$ PSDefaultParameterValues , einen Standardmechanismus zum Definieren von Standardparametern, jedoch nur für Cmdlets und erweiterte Funktionen. Natürlich können Sie erweiterte Funktionen aus unseren Shells
-ArgumentList
, dies führt jedoch zu unnötigen Komplikationen (z. B. stimmt PowerShell mit Teilparameternamen überein (z. B.
-a
entspricht
-ArgumentList
), was mit Linux-Befehlen in Konflikt steht, die
-ArgumentList
als Argumente akzeptieren) Die Syntax zum Definieren von Standardwerten ist nicht am besten geeignet (zum Definieren von Standardargumenten ist der Parametername im Schlüssel erforderlich und nicht nur der Befehlsname).
Mit einer geringfügigen Änderung unserer Shells können wir jedoch ein Modell implementieren, das
$PSDefaultParameterValues
ähnelt, und Standardoptionen für Linux-Befehle aktivieren!
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 ' ') } }
Durch
$WslDefaultParameterValues
an die Befehlszeile senden wir die Parameter über
wsl.exe
. Im Folgenden wird gezeigt, wie Sie einem PowerShell-Profil Anweisungen zum Konfigurieren der Standardeinstellungen hinzufügen. Jetzt können wir es schaffen!
$WslDefaultParameterValues["grep"] = "-E" $WslDefaultParameterValues["less"] = "-i" $WslDefaultParameterValues["ls"] = "-AFh --group-directories-first"
Da Parameter nach
$PSDefaultParameterValues
modelliert werden, können Sie sie
einfach vorübergehend
$PSDefaultParameterValues
, indem Sie den Schlüssel
"Disabled"
auf
$true
. Ein zusätzlicher Vorteil einer separaten Hash-Tabelle ist die Möglichkeit,
$WslDefaultParameterValues
getrennt von
$PSDefaultParameterValues
zu deaktivieren.
Argumentvervollständigung
PowerShell ermöglicht das Registrieren von Argumentterminatoren mit dem Befehl
Register-ArgumentCompleter
. Bash verfügt über leistungsstarke
programmierbare Vervollständigungswerkzeuge . Mit WSL können Sie Bash über PowerShell aufrufen. Wenn wir die Argumentterminatoren für unsere PowerShell-Funktions-Wrapper registrieren und bash aufrufen können, um die Abbrüche zu erstellen, erhalten wir die vollständige Vervollständigung der Argumente mit der gleichen Genauigkeit wie in bash selbst!
# 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] } }
Der Code ist ein wenig eng, ohne einige der Bash-Interna zu verstehen, aber im Grunde machen wir Folgendes:
- Wir registrieren den Argument-Finalizer für alle unsere Funktions-Wrapper, indem wir die Liste der
$commands
an den Parameter -CommandName
für Register-ArgumentCompleter
.
- Wir ordnen jeden Befehl der Shell-Funktion zu, die bash für die automatische Vervollständigung verwendet (bash verwendet
$F
, um die Spezifikationen für die automatische Vervollständigung zu definieren, kurz für complete -F <FUNCTION>
).
- Konvertieren Sie die PowerShell-Argumente
$wordToComplete
, $commandAst
und $cursorPosition
in das Format, das von den Bash-Abschlussfunktionen gemäß den programmierbaren Bash-Abschlussspezifikationen erwartet wird.
- Wir
wsl.exe
die Befehlszeile für die Übertragung in wsl.exe
, die die korrekte Umgebungseinstellung sicherstellt, die entsprechende Autovervollständigungsfunktion aufruft und die Ergebnisse mit Zeilenumbrüchen anzeigt.
- Dann rufen wir
wsl
mit der Befehlszeile auf, trennen die Ausgabe mit Zeilentrennzeichen und generieren CompletionResults
für jedes, sortieren sie und maskieren Zeichen wie Leerzeichen und Klammern, die sonst falsch interpretiert würden.
Infolgedessen verwenden unsere Linux-Befehlsshells genau die gleiche automatische Vervollständigung wie in bash! Zum Beispiel:
ssh -c <TAB> -J <TAB> -m <TAB> -O <TAB> -o <TAB> -Q <TAB> -w <TAB> -b <TAB>
Jede automatische Vervollständigung liefert Werte, die für das vorherige Argument spezifisch sind, und liest Konfigurationsdaten wie bekannte Hosts aus der WSL!
<TAB>
durchläuft die Parameter.
<Ctrl + >
zeigt alle verfügbaren Optionen an.
Da bash autocomplete jetzt bei uns funktioniert, können Sie Linux-Pfade auch direkt in PowerShell automatisch vervollständigen!
less /etc/<TAB>
ls /usr/share/<TAB>
vim ~/.bash<TAB>
In Fällen, in denen die automatische Vervollständigung von Bash fehlschlägt, kehrt PowerShell zum Standardsystem mit Windows-Pfaden zurück. In der Praxis können Sie diese und andere Methoden also nach eigenem Ermessen gleichzeitig verwenden.
Fazit
Mit PowerShell und WSL können wir Linux-Befehle als native Anwendungen in Windows integrieren. Sie müssen nicht nach Win32-Builds oder Linux-Dienstprogrammen suchen oder den Workflow durch Umschalten auf die Linux-Shell unterbrechen.
Installieren Sie einfach
WSL , konfigurieren Sie
Ihr PowerShell-Profil und
listen Sie die Befehle auf, die Sie importieren möchten ! Die umfangreiche automatische Vervollständigung von Befehls- und Pfadparametern für Linux- und Windows-Dateien ist eine Funktionalität, die heutzutage in nativen Windows-Befehlen nicht mehr zu finden ist.
Der oben beschriebene vollständige Quellcode sowie zusätzliche Empfehlungen für die Aufnahme in den Workflow finden Sie
hier .
Welche Linux-Befehle finden Sie am nützlichsten? Welche anderen vertrauten Dinge fehlen bei der Arbeit unter Windows? Schreiben Sie in den Kommentaren oder
auf GitHub !