Integración de comandos de Linux en Windows usando PowerShell y WSL

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

Subsistema de Windows para Linux (WSL) ha dado un gran paso adelante en este sentido. Le permite llamar comandos de Linux desde Windows mediante proxy 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.
  • Las rutas de Windows en los argumentos no siempre funcionan porque las barras invertidas se interpretan como caracteres de escape en lugar de separadores de directorios.
  • Las rutas de Windows en 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 de Linux.
  • No se admite la finalización de comandos.
  • No se admite la finalización de argumentos.

Como resultado, los comandos de Linux son tratados como ciudadanos de segunda clase en Windows y son más difíciles de usar que los comandos nativos. Para igualar sus derechos, es necesario resolver los problemas enumerados.

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. wsl, traduciendo rutas de Windows en rutas WSL. Requisitos básicos para conchas:

  • Para cada comando de Linux debe haber un contenedor de funciones con el mismo nombre.
  • El shell debe reconocer las rutas de Windows pasadas como argumentos y convertirlas en rutas WSL.
  • El shell debería llamar wsl con el comando de Linux apropiado a cualquier entrada de canalización y pasando cualquier argumento de línea de comando 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 de Windows mediante 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 estos wrappers podrás llamar a tus comandos favoritos de Linux de una forma 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 el conjunto básico de comandos, pero puede crear un shell para cualquier comando de 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

En Linux, es común definir alias y/o variables de entorno en perfiles de inicio de sesión, estableciendo parámetros predeterminados para comandos usados ​​frecuentemente (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), que entrará en conflicto con los comandos de Linux que toman nombres parciales como argumentos), y la sintaxis para definir los valores predeterminados no será la más apropiada (los argumentos predeterminados requieren el nombre del parámetro en la clave, no solo el nombre del comando) .

Sin embargo, con una ligera modificación en nuestros shells, podemos implementar un modelo similar a $PSDefaultParameterValues¡Y habilite las opciones predeterminadas para los comandos de 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 de Linux utilizarán exactamente el mismo autocompletado que 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 el autocompletado de bash, ¡puedes autocompletar rutas de Linux directamente en PowerShell!

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

En los casos en los que el autocompletado de bash no produce ningún resultado, PowerShell vuelve a las rutas de Windows predeterminadas del sistema. Por 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 de Linux en Windows como aplicaciones nativas. No es necesario buscar compilaciones de Win32 o utilidades de Linux ni interrumpir el flujo de trabajo yendo a un shell de Linux. Justo instalar WSL, configurar Perfil de PowerShell и enumera los comandos que deseas importar! El autocompletado enriquecido para parámetros de comandos y rutas de archivos de Linux y Windows es una funcionalidad que ni siquiera está disponible en los comandos nativos de Windows en la actualidad.

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

¿Qué comandos de Linux te parecen más útiles? ¿Qué otras cosas comunes faltan al trabajar en Windows? Escribe en los comentarios o en GitHub!

Fuente: habr.com

Añadir un comentario