Wir integrieren Linux-Teams in Windows mit PowerShell und WSL

Eine häufige Frage von Windows-Entwicklern: „Warum gibt es hier immer noch kein <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?». Будь то мощное пролистывание less oder gewohnte Werkzeuge grep oder sed, Windows-Entwickler möchten einfachen Zugriff auf diese Befehle im täglichen Arbeitsablauf haben.

Das Windows-Subsystem für Linux (WSL) hat in dieser Hinsicht einen großen Schritt nach vorn gemacht. Es ermöglicht das Ausführen von Linux-Befehlen aus Windows heraus, indem es diese über wsl.exe (z. B., wsl ls). Obwohl dies eine erhebliche Verbesserung darstellt, hat diese Methode einige Nachteile.

  • Die weitverbreitete Verwendung von wsl ist ermüdend und unnatürlich.
  • Windows-Pfade in Argumenten funktionieren nicht immer, da Rückwärtsschslashes als Escape-Zeichen und nicht als Verzeichnistrenner interpretiert werden.
  • Windows-Pfade in Argumenten werden nicht in den entsprechenden WSL-Mount-Punkt übersetzt.
  • Standardparameter in WSL-Profilen mit Aliases und Umgebungsvariablen werden nicht berücksichtigt.
  • Die vervollständigung von Linux-Pfaden wird nicht unterstützt.
  • Die Vervollständigung von Befehlen wird nicht unterstützt.
  • Die Vervollständigung von Argumenten wird nicht unterstützt.

In der Folge werden Linux-Befehle unter Windows als Bürger zweiter Klasse betrachtet – und ihre Nutzung ist komplizierter als bei nativen Befehlen. Um ihre Rechte zu fördern, müssen die aufgeführten Probleme gelöst werden.

PowerShell-Funktionshüllen

Mit Hilfe von PowerShell-Funktionshüllen können wir die Autovervollständigung von Befehlen hinzufügen und die Notwendigkeit von Präfixen beseitigen wsl, indem wir Windows-Pfade in WSL-Pfade übersetzen. Die grundlegenden Anforderungen an die Schalen sind:

  • Für jeden Linux-Befehl muss es eine Funktionshülle mit demselben Namen geben.
  • Die Hülle muss Windows-Pfade erkennen, die als Argumente übergeben werden, und diese in WSL-Pfade umwandeln.
  • Die Hülle muss wsl den entsprechenden Linux-Befehl für jede Eingabe aus der Pipeline aufrufen und alle übergebenen Befehlszeilenargumente an die Funktion weitergeben.

Da dieses Muster auf jeden Befehl angewendet werden kann, können wir die Definition dieser Hüllen abstrahieren und sie dynamisch aus einer Liste von Befehlen zum Importieren 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 die zu importierenden Befehle. Dann generieren wir dynamisch eine Funktionshülle für jeden von ihnen, indem wir den Befehl Invoke-Expression verwenden (zunächst alle Aliase entfernen, die mit der Funktion Konflikte verursachen könnten).

Die Funktion durchläuft die Kommandozeilenargumente, identifiziert Windows-Pfade mit Kommandos Split-Path und Test-Path, und wandelt dann diese Pfade in WSL-Pfade um. Wir leiten die Pfade durch eine Hilfsfunktion Format-WslArgument, die wir später definieren werden. Diese Funktion maskiert spezielle Zeichen wie Leerzeichen und Klammern, die andernfalls falsch interpretiert werden könnten.

Schließlich übergeben wir wsl die Eingaben des Pipelines und alle Kommandozeilenargumente.

Mit solchen Wrappern können Sie Ihre bevorzugten Linux-Befehle auf natürlicher Weise aufrufen, ohne ein Präfix hinzuzufügen wsl und sich keine Sorgen darüber zu machen, wie die Pfade umgewandelt werden:

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

Hier ist eine grundlegende Sammlung von Befehlen, aber Sie können eine Hülle für jeden Linux-Befehl erstellen, indem Sie ihn einfach zur Liste hinzufügen. Wenn Sie diesen Code in Ihr ein Profil PowerShell einfügen, sind diese Befehle in jeder PowerShell-Sitzung für Sie verfügbar, wie native Befehle!

Standardoptionen

In Linux ist es üblich, Aliase und/oder Umgebungsvariablen in Profilen (Anmeldeprofilen) zu definieren, um 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 besteht darin, dass die Profile nicht geladen werden, weshalb diese Parameter standardmäßig nicht verfügbar sind (d. h. ls in WSL und wsl ls sich unterschiedlich verhalten werden, je nachdem, welchen Alias wir oben definiert haben).

PowerShell bietet $PSDefaultParameterValues, einen Standardmechanismus zur Festlegung von Standardparametern, jedoch nur für Cmdlets und erweiterte Funktionen. Natürlich können wir aus unseren Shells erweiterte Funktionen erstellen, aber das bringt unnötige Komplikationen mit sich (so vergleicht PowerShell partielle Parameternamen (zum Beispiel, -a entspricht -ArgumentList), die mit Linux-Befehlen in Konflikt stehen, die partielle Namen als Argumente akzeptieren), und die Syntax zur Festlegung von Standardwerten wird nicht optimal sein (um Standardargumente festzulegen, ist der Parametername im Schlüssel erforderlich, nicht nur der Name des Befehls).

Mit einer kleinen Anpassung unserer Shells können wir jedoch ein Modell implementieren, das ähnlich ist, $PSDefaultParameterValuesund Standardparameter 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 die Übergabe $WslDefaultParameterValues an die Eingabeaufforderung senden wir Parameter über wsl.exe. Im Folgenden sehen Sie, wie Sie Anweisungen im PowerShell-Profil hinzufügen, um die Standardparameter zu konfigurieren. Jetzt können wir dies tun!

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

Da die Parameter modelliert werden nach $PSDefaultParameterValues, können Sie sie leicht deaktivieren vorübergehend, indem Sie den Schlüssel "Disabled" auf $truesetzen. Ein zusätzlicher Vorteil einer separaten Hashtabelle ist die Möglichkeit, $WslDefaultParameterValues unabhängig von $PSDefaultParameterValues.

Die Autovervollständigung von Argumenten

PowerShell ermöglicht es, Argumentvollender mit dem Befehl Register-ArgumentCompleter. In Bash gibt es leistungsstarke programmierbare Mittel zur Autovervollständigung. WSL ermöglicht das Aufrufen von Bash aus PowerShell. Wenn wir Abschlussermittler für unsere PowerShell-Funktionen registrieren und Bash für die Erstellung von Abschlussmeldungen aufrufen können, erhalten wir eine vollständige Autovervollständigung der Argumente mit derselben 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 etwas dicht, ohne einige interne Funktionen von Bash zu verstehen, aber im Wesentlichen machen wir Folgendes:

  • Wir registrieren den Abschlussermittler für alle unsere Funktionswraps und übergeben eine Liste $commands zum Parameter -CommandName für Register-ArgumentCompleter.
  • Wir verknüpfen jeden Befehl mit der Shellfunktion, die Bash für die Autovervollständigung verwendet (zur Bestimmung der Spezifikationen für die Autovervollständigung in Bash wird verwendet $F, eine Abkürzung für complete -F).
  • Wir transformieren die PowerShell-Argumente $wordToComplete, $commandAst und $cursorPosition in ein Format, das von den Autovervollständigungsfunktionen von Bash erwartet wird, gemäß den Spezifikationen des programmierbaren Autocomplete bash.
  • Wir erstellen die Befehlszeile, die an wsl.exe, übergeben wird, das die richtige Umgebungsanpassung gewährleistet, die entsprechende Autovervollständigungsfunktion aufruft und die Ergebnisse zeilenweise ausgibt.
  • Dann rufen wir auf wsl mit der Befehlszeile, trennen wir die Ausgabe durch Zeilenumbrüche und generieren für jede CompletionResults, indem wir sie sortieren und Zeichen wie Leerzeichen und Klammern escapen, die sonst falsch interpretiert würden.

Am Ende werden unsere Linux-Befehlszeilen genau das gleiche Autocomplete verwenden wie bash! Zum Beispiel:

  • ssh -c -J -m -O -o -Q -w -b

Jedes Autocomplete liefert Werte, die für das vorherige Argument spezifisch sind, indem es Konfigurationsdaten wie bekannte Hosts aus WSL ausliest!

<TAB> wird die Parameter zyklisch durchlaufen. <Ctrl + пробел> zeigt alle verfügbaren Optionen an.

Außerdem, da jetzt das bash-Autocomplete funktioniert, können Sie Linux-Pfade direkt in PowerShell autocompleten!

  • less /etc/
  • ls /usr/share/
  • vim ~/bash

In Fällen, in denen das bash-Autocomplete keine Ergebnisse liefert, kehrt PowerShell zum Standard-System mit Windows-Pfaden zurück. So können Sie in der Praxis beide Pfade nach Belieben verwenden.

Fazit

Mit PowerShell und WSL können wir Linux-Befehle nahtlos in Windows integrieren, als wären sie native Anwendungen. Es ist nicht nötig, nach Win32-Builds oder Linux-Utilities zu suchen oder den Workflow zu unterbrechen, um zur Linux-Shell zu wechseln. Einfach WSL installieren, konfigurieren Sie das PowerShell-Profil und listen Sie die Befehle auf, die Sie importieren möchten! Die umfassende Auto-Vervollständigung für Befehlsoptionen und Dateipfade in Linux und Windows ist eine Funktion, die derzeit sogar bei den nativen Windows-Befehlen nicht verfügbar ist.

Der vollständige oben beschriebene Quellcode sowie zusätzliche Empfehlungen zur Integration in den Workflow sind verfügbar hier.

Welche Linux-Befehle empfinden Sie als besonders nützlich? Welche vertrauten Dinge fehlen Ihnen bei der Arbeit in Windows? Schreiben Sie es in die Kommentare oder auf GitHub!

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster