Integration von Linux-Befehlen in Windows mithilfe von PowerShell und WSL

Eine typische Frage von Windows-Entwicklern: „Warum gibt es immer noch keine <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Ob es ein kraftvoller Schlag ist less oder vertraute Werkzeuge grep oder sedWindows-Entwickler wünschen sich bei ihrer täglichen Arbeit einen einfachen Zugriff auf diese Befehle.

Windows-Subsystem für Linux (WSL) hat in dieser Hinsicht einen großen Schritt nach vorne gemacht. Es ermöglicht Ihnen, Linux-Befehle von Windows aus aufzurufen, indem Sie sie per Proxy weiterleiten wsl.exe (ZB wsl ls). Obwohl dies eine erhebliche Verbesserung darstellt, weist diese Option eine Reihe von Nachteilen auf.

  • Allgegenwärtige Ergänzung wsl langweilig und unnatürlich.
  • Windows-Pfade in Argumenten funktionieren nicht immer, da Backslashes als Escape-Zeichen und nicht als Verzeichnistrennzeichen interpretiert werden.
  • Windows-Pfade in Argumenten werden nicht in den entsprechenden Mountpunkt in der WSL übersetzt.
  • Standardeinstellungen werden in WSL-Profilen mit Aliasnamen und Umgebungsvariablen nicht berücksichtigt.
  • Die Linux-Pfadvervollständigung wird nicht unterstützt.
  • Die Befehlsvervollständigung wird nicht unterstützt.
  • Die Argumentvervollständigung wird nicht unterstützt.

Infolgedessen werden Linux-Befehle unter Windows wie Bürger zweiter Klasse behandelt – und sind schwieriger zu verwenden als native Befehle. Um ihre Rechte anzugleichen, ist es notwendig, die aufgeführten Probleme zu lösen.

PowerShell-Funktionswrapper

Mit PowerShell-Funktionswrappern können wir die Befehlsvervollständigung hinzufügen und die Notwendigkeit von Präfixen beseitigen wsl, übersetzt Windows-Pfade in WSL-Pfade. Grundvoraussetzungen für Muscheln:

  • Für jeden Linux-Befehl muss es einen Funktions-Wrapper mit demselben Namen geben.
  • Die Shell muss die als Argumente übergebenen Windows-Pfade erkennen und in WSL-Pfade konvertieren.
  • Die Shell sollte aufrufen wsl mit dem entsprechenden Linux-Befehl an jede Pipeline-Eingabe und Übergabe aller an die Funktion übergebenen Befehlszeilenargumente.

Da dieses Muster auf jeden Befehl angewendet werden kann, können wir die Definition dieser Wrapper abstrahieren und sie dynamisch aus einer Liste von zu importierenden Befehlen 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 (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 ' ')
    }
}
"@
}

Liste $command definiert Importbefehle. Anschließend generieren wir mithilfe des Befehls dynamisch einen Funktions-Wrapper für jeden von ihnen Invoke-Expression (Indem Sie zunächst alle Aliase entfernen, die mit der Funktion in Konflikt geraten würden.)

Die Funktion iteriert über Befehlszeilenargumente und ermittelt Windows-Pfade mithilfe von Befehlen Split-Path и Test-Pathund konvertiert diese Pfade dann in WSL-Pfade. Wir führen die Pfade durch eine Hilfsfunktion aus Format-WslArgument, die wir später definieren werden. Es maskiert Sonderzeichen wie Leerzeichen und Klammern, die andernfalls falsch interpretiert würden.

Schließlich vermitteln wir wsl Pipeline-Eingabe und alle Befehlszeilenargumente.

Mit diesen Wrappern können Sie Ihre bevorzugten Linux-Befehle auf natürlichere Weise aufrufen, ohne ein Präfix hinzuzufügen wsl 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 hinzufügen Profil PowerShell, diese Befehle stehen Ihnen in jeder PowerShell-Sitzung zur Verfügung, genau wie native Befehle!

Standardeinstellungen

Unter Linux ist es üblich, Aliase und/oder Umgebungsvariablen in Anmeldeprofilen zu definieren und Standardparameter für häufig verwendete Befehle festzulegen (z. B. alias ls=ls -AFh oder export LESS=-i). Einer der Nachteile des Proxyings über eine nicht interaktive Shell wsl.exe - dass die Profile nicht geladen werden, sodass diese Optionen standardmäßig nicht verfügbar sind (d. h. ls in WSL und wsl ls wird sich mit dem oben definierten Alias ​​anders verhalten).

PowerShell bietet $PSDefaultParameterValues, ein Standardmechanismus zum Definieren von Standardparametern, jedoch nur für Cmdlets und erweiterte Funktionen. Natürlich können wir aus unseren Shells erweiterte Funktionen machen, aber das führt zu unnötigen Komplikationen (zum Beispiel korreliert PowerShell Teilparameternamen (zum Beispiel -a korreliert mit -ArgumentList), was zu Konflikten mit Linux-Befehlen führt, die Teilnamen als Argumente verwenden), und die Syntax zum Definieren von Standardwerten ist nicht die am besten geeignete (zum Definieren von Standardargumenten ist der Parametername im Schlüssel erforderlich, nicht nur der Befehlsname).

Mit einer geringfügigen Modifikation unserer Shells können wir jedoch ein ähnliches Modell implementieren $PSDefaultParameterValues, und aktivieren Sie Standardoptionen für Linux-Befehle!

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

Vorbeigehen $WslDefaultParameterValues An die Kommandozeile senden wir Parameter per wsl.exe. Im Folgenden wird gezeigt, wie Sie Ihrem PowerShell-Profil Anweisungen hinzufügen, um Standardeinstellungen zu konfigurieren. Jetzt können wir es schaffen!

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

Da die Parameter nachgebildet sind $PSDefaultParameterValues, Sie können es ist einfach, sie zu deaktivieren vorübergehend durch Installation des Schlüssels "Disabled" in die Bedeutung $true. Ein zusätzlicher Vorteil einer separaten Hash-Tabelle ist die Möglichkeit zur Deaktivierung $WslDefaultParameterValues getrennt von $PSDefaultParameterValues.

Argumentvervollständigung

Mit PowerShell können Sie Argumenttrailer mithilfe des Befehls registrieren Register-ArgumentCompleter. Bash hat mächtig programmierbare Autovervollständigungstools. Mit WSL können Sie Bash über PowerShell aufrufen. Wenn wir Argumentvervollständigungen für unsere PowerShell-Funktionswrapper registrieren und Bash aufrufen können, um die Vervollständigungen zu generieren, erhalten wir eine vollständige Argumentvervollständigung mit der gleichen Präzision wie 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 etwas dicht, ohne einige der internen Funktionen von Bash zu verstehen, aber im Grunde machen wir Folgendes:

  • Registrieren eines Argumentvervollständigers für alle unsere Funktionswrapper durch Übergabe einer Liste $commands im Parameter -CommandName für Register-ArgumentCompleter.
  • Wir ordnen jeden Befehl der Shell-Funktion zu, die Bash für die automatische Vervollständigung verwendet (um Spezifikationen für die automatische Vervollständigung zu definieren, verwendet Bash). $F, Abkürzung für complete -F <FUNCTION>).
  • Konvertieren von PowerShell-Argumenten $wordToComplete, $commandAst и $cursorPosition in das von den Autovervollständigungsfunktionen der Bash gemäß den Spezifikationen erwartete Format programmierbare Autovervollständigung schlag.
  • Wir erstellen eine Befehlszeile zur Übertragung wsl.exe, die sicherstellt, dass die Umgebung korrekt eingerichtet ist, die entsprechende Autovervollständigungsfunktion aufruft und die Ergebnisse Zeile für Zeile ausgibt.
  • Dann rufen wir an wsl Mit der Befehlszeile trennen wir die Ausgabe durch Zeilentrennzeichen und generieren für jede CompletionResults, sie sortieren und Zeichen wie Leerzeichen und Klammern maskieren, die andernfalls falsch interpretiert würden.

Infolgedessen verwenden unsere Linux-Befehlsshells genau die gleiche Autovervollständigung wie Bash! Zum Beispiel:

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

Jede Autovervollstä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.

Und da wir jetzt über die automatische Bash-Vervollständigung verfügen, können Sie Linux-Pfade direkt in PowerShell automatisch vervollständigen!

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

In Fällen, in denen die automatische Bash-Vervollständigung keine Ergebnisse liefert, greift PowerShell auf die standardmäßigen Windows-Systempfade zurück. Somit können Sie in der Praxis nach eigenem Ermessen beide Wege gleichzeitig nutzen.

Abschluss

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 Ihren Arbeitsablauf durch den Aufruf einer Linux-Shell unterbrechen. Nur WSL installieren, konfigurieren PowerShell-Profil и Listen Sie die Befehle auf, die Sie importieren möchten! Die umfassende automatische Vervollständigung für Linux- und Windows-Befehlsparameter und Dateipfade ist eine Funktionalität, die heute nicht einmal in nativen Windows-Befehlen verfügbar ist.

Der oben beschriebene vollständige Quellcode sowie zusätzliche Richtlinien für die Integration in Ihren Workflow sind verfügbar hier.

Welche Linux-Befehle finden Sie am nützlichsten? Welche anderen häufigen Dinge fehlen beim Arbeiten unter Windows? Schreibt es in die Kommentare bzw auf GitHub!

Source: habr.com

Kommentar hinzufügen