Integración de equipos Linux в Windows utilizando PowerShell y WSL

Una pregunta típica de los desarrolladores Windows¿Por qué no hay nada aquí todavía? <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Ya sea un golpe poderoso less o herramientas familiares grep o sed, desarrolladores bajo Windows Quieren tener fácil acceso a estos comandos en su trabajo diario.

Subsistema Windows para Linux (WSL) ha dado un gran paso adelante en este sentido. Permite llamar comandos Linux de Windows, haciéndolos pasar a través de wsl.exe (p. wsl ls). Aunque se trata de una mejora significativa, esta opción adolece de una serie de desventajas.

  • Adición ubicua wsl tedioso y antinatural.
  • Caminos Windows Los argumentos no siempre funcionan porque las barras invertidas se interpretan como caracteres de escape, no como separadores de directorios.
  • Caminos Windows Los argumentos no se traducen al punto de montaje correspondiente en WSL.
  • La configuración predeterminada no se respeta en los perfiles WSL con alias y variables de entorno.
  • No se admite la finalización de rutas. Linux.
  • No se admite la finalización de comandos.
  • No se admite la finalización de argumentos.

Como resultado del equipo Linux son percibidos bajo Windows Son como ciudadanos de segunda clase, y más difíciles de utilizar que sus equipos locales. Para igualar sus derechos, es necesario abordar estos problemas.

Envoltorios de funciones de PowerShell

Con los contenedores de funciones de PowerShell, podemos agregar la finalización de comandos y eliminar la necesidad de prefijos. wslrutas de transmisión Windows En la ruta de WSL. Requisitos básicos para shells:

  • Para cada equipo Linux Debe existir una función contenedora con el mismo nombre.
  • El shell debe reconocer rutas Windows, pasados ​​como argumentos, y convertirlos en rutas WSL.
  • El shell debería llamar wsl con el comando apropiado Linux a cualquier entrada de canalización y pasando cualquier argumento de línea de comandos pasado a la función.

Dado que este patrón se puede aplicar a cualquier comando, podemos abstraer la definición de estos contenedores y generarlos dinámicamente a partir de una lista de comandos para importar.

# 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 ' ')
    }
}
"@
}

Lista $command define comandos de importación. Luego generamos dinámicamente un contenedor de funciones para cada uno de ellos usando el comando Invoke-Expression (eliminando primero cualquier alias que pueda entrar en conflicto con la función).

La función itera sobre los argumentos de la línea de comandos y determina las rutas. Windows usando comandos Split-Path и Test-Pathy luego convierte estas rutas en rutas WSL. Ejecutamos los caminos a través de una función auxiliar. Format-WslArgument, que definiremos más adelante. Evita caracteres especiales como espacios y paréntesis que de otro modo se malinterpretarían.

Finalmente, transmitimos wsl entrada de canalización y cualquier argumento de línea de comando.

Con la ayuda de estos envoltorios puedes llamar a tus comandos favoritos. Linux de una manera más natural, sin añadir un prefijo wsl y sin preocuparnos de cómo se convierten los caminos:

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

Aquí se muestra un conjunto básico de comandos, pero puedes crear una función auxiliar para cualquier comando. Linux, simplemente agregándolo a la lista. Si agrega este código a su perfil PowerShell, estos comandos estarán disponibles en cada sesión de PowerShell, ¡al igual que los comandos nativos!

Configuración por defecto

В Linux Es común definir alias y/o variables de entorno en los perfiles (perfil de inicio de sesión), estableciendo parámetros predeterminados para los comandos de uso frecuente (por ejemplo, alias ls=ls -AFh o export LESS=-i). Una de las desventajas de realizar proxy a través de un shell no interactivo wsl.exe - que los perfiles no están cargados, por lo que estas opciones no están disponibles por defecto (es decir ls en WSL y wsl ls se comportará de manera diferente con el alias definido anteriormente).

PowerShell proporciona $PSDefaultParameterValues, un mecanismo estándar para definir parámetros predeterminados, pero solo para cmdlets y funciones avanzadas. Por supuesto, podemos crear funciones avanzadas a partir de nuestros shells, pero esto introduce complicaciones innecesarias (por ejemplo, PowerShell correlaciona nombres de parámetros parciales (por ejemplo, -a se correlaciona con -ArgumentList), lo que entrará en conflicto con los comandos Linux, tomando nombres parciales como argumentos), y la sintaxis para definir valores predeterminados no será la más apropiada (la definición de argumentos predeterminados requiere el nombre del parámetro en el interruptor, no solo el nombre del comando).

Sin embargo, con una ligera modificación en nuestros shells, podemos implementar un modelo similar a $PSDefaultParameterValuesy habilitar las opciones predeterminadas para los comandos Linux!

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

Transmitiendo $WslDefaultParameterValues a la línea de comando, enviamos parámetros a través de wsl.exe. A continuación se muestra cómo agregar instrucciones a su perfil de PowerShell para configurar los ajustes predeterminados. ¡Ahora podemos hacerlo!

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

Dado que los parámetros están modelados a partir de $PSDefaultParameterValuespuedes es fácil desactivarlos temporalmente instalando la llave "Disabled" en significado $true. Un beneficio adicional de una tabla hash separada es la capacidad de deshabilitar $WslDefaultParameterValues por separado de $PSDefaultParameterValues.

Finalización del argumento

PowerShell le permite registrar avances de argumentos usando el comando Register-ArgumentCompleter. Bash tiene poderoso herramientas programables de autocompletado. WSL le permite llamar a bash desde PowerShell. Si podemos registrar la finalización de argumentos para nuestros contenedores de funciones de PowerShell y llamar a bash para generar las completaciones, obtenemos la finalización completa de los argumentos con la misma precisión que el propio bash.

# 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]
    }
}

El código es un poco denso sin entender algunas de las funciones internas de bash, pero básicamente lo que hacemos es esto:

  • Registrar un completador de argumentos para todos nuestros contenedores de funciones pasando una lista $commands en parámetro -CommandName para Register-ArgumentCompleter.
  • Asignamos cada comando a la función de shell que bash usa para el autocompletado (para definir las especificaciones de autocompletado, bash usa $F, abreviatura de complete -F <FUNCTION>).
  • Conversión de argumentos de PowerShell $wordToComplete, $commandAst и $cursorPosition en el formato esperado por las funciones de autocompletado de bash de acuerdo con las especificaciones autocompletado programable golpetazo.
  • Redactamos una línea de comando para transferir a wsl.exe, que garantiza que el entorno esté configurado correctamente, llama a la función de autocompletar adecuada y genera los resultados línea por línea.
  • Entonces llamamos wsl con la línea de comando, separamos la salida por separadores de línea y generamos para cada CompletionResults, clasificándolos y eliminando caracteres como espacios y paréntesis que de otro modo se malinterpretarían.

Como resultado, nuestros shells de comandos Linux ¡Utilizará exactamente el mismo autocompletado que en bash! Por ejemplo:

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

Cada autocompletado proporciona valores específicos del argumento anterior, leyendo datos de configuración, como hosts conocidos de WSL.

<TAB> recorrerá los parámetros. <Ctrl + пробел> mostrará todas las opciones disponibles.

Además, dado que ahora tenemos la función de autocompletado de bash funcionando, puedes autocompletar rutas. Linux ¡Directamente en PowerShell!

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

En los casos en que la función de autocompletado de bash no produzca ningún resultado, PowerShell recurre a las rutas predeterminadas del sistema. WindowsPor lo tanto, en la práctica, puede utilizar ambos caminos simultáneamente a su discreción.

Conclusión

Usando PowerShell y WSL podemos integrar comandos Linux в Windows como aplicaciones nativas. No es necesario buscar compilaciones o utilidades de Win32. Linux o interrumpir el flujo de trabajo yendo a Linux-concha. Simplemente instalar WSL, configurar Perfil de PowerShell и enumera los comandos que deseas importar¡Autocompletado avanzado para parámetros de comandos y rutas de archivo! Linux и Windows — Esta es una funcionalidad que no está disponible ni siquiera en los comandos nativos actuales. Windows.

El código fuente completo descrito anteriormente, así como pautas adicionales para incorporarlo a su flujo de trabajo, están disponibles aquí.

¿Qué equipos? Linux ¿Qué es lo que te resulta más útil? ¿Qué otras cosas familiares faltan cuando trabajas en Windows¿Escribe en los comentarios o en GitHub!

Fuente: habr.com

Compre alojamiento confiable para sitios con protección DDoS, servidores VPS VDS 🔥 Compra alojamiento web fiable con protección DDoS, servidores VPS VDS | ProHoster