Avatar

Mike

26 de septiembre, 2019

Una pregunta común que tienen los desarrolladores de Windows es «¿por qué Windows no tiene <INSERT FAVORITE LINUX COMMAND HERE> todavía?». Ya sea que anhelen un paginador potente como less o que quieran usar comandos familiares como grep o sed, los desarrolladores de Windows desean un acceso fácil a estos comandos como parte de su flujo de trabajo principal.

El Subsistema de Windows para Linux (WSL) fue un gran paso adelante en este sentido, permitiendo a los desarrolladores llamar a través de los comandos de Linux desde Windows por medio de wsl.exe (por ejemplo, wsl ls). Aunque es una mejora significativa, la experiencia tiene varias carencias:

  • Prefijar los comandos con wsl es tedioso y poco natural
  • Las rutas de Windows pasadas como argumentos no suelen resolverse debido a que las barras invertidas se interpretan como caracteres de escape en lugar de separadores de directorio
  • Las rutas de Windows pasadas como argumentos no suelen resolverse debido a que no se traducen al apropiado punto de montaje dentro de WSL
  • Los parámetros por defecto definidos en los perfiles de inicio de sesión de WSL con alias y variables de entorno no se respetan
  • 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
  • El resultado de estas deficiencias es que los comandos de Linux se sienten como ciudadanos de segunda clase con respecto a Windows y son más difíciles de usar.ciudadanos de segunda clase respecto a Windows y son más difíciles de usar de lo que deberían. Para que un comando se sienta como un comando nativo de Windows, tendremos que resolver estos problemas.

    Envolturas de funciones de PowerShell

    Podemos eliminar la necesidad de prefijar los comandos con wsl, manejar la traducción de las rutas de Windows a las rutas de WSL, y soportar la finalización de comandos con envolturas de funciones de PowerShell. Los requisitos básicos de los wrappers son:

    • Debe haber una envoltura de función por comando Linux con el mismo nombre que el comando
    • La envoltura debe reconocer las rutas de Windows pasadas como argumentos y traducirlas a rutas WSL
    • La envoltura debe invocar wsl con el comando Linux correspondiente, canalizando cualquier entrada de la tubería y pasando los argumentos de la línea de comandos pasados a la función
    • Dado que esta plantilla se puede aplicar a cualquier comando, podemos abstraer la definición de estos wrappers y generarlos dinámicamente a partir de una lista de comandos a 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 Ignorefunction 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 -IsAbsolute -ErrorAction Ignore) { `$args = Format-WslArgument (wsl.exe wslpath (`$args -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 -ErrorAction Ignore) { `$args = Format-WslArgument (`$args -replace "\\", "/") } } if (`$input.MoveNext()) { `$input.Reset() `$input | wsl.exe $_ (`$args -split ' ') } else { wsl.exe $_ (`$args -split ' ') }}"@}

La lista $command define los comandos a importar. A continuación, generamos dinámicamente la envoltura de la función para cada uno de ellos utilizando el comando Invoke-Expression (eliminando primero cualquier alias que pudiera entrar en conflicto con la función).

La función recorre los argumentos de la línea de comandos, identifica las rutas de Windows utilizando los comandos Split-Path y Test-Path, y luego convierte esas rutas en rutas WSL. Pasamos las rutas a través de una función de ayuda que definiremos más adelante llamada Format-WslArgument que escapa caracteres especiales como espacios y paréntesis que de otra manera serían mal interpretados.

Por último, pasamos la entrada de la tubería y cualquier argumento de la línea de comandos a través de wsl.

Con estas envolturas de funciones en su lugar, ahora podemos llamar a nuestros comandos favoritos de Linux de una manera más natural sin tener que prefijarlos con wsl o preocuparse de cómo se traducen las rutas de Windows a las rutas de WSL:

  • man bash
  • less -i $profile.CurrentUserAllHosts
  • ls -Al C:\Windows\ | less
  • .

  • grep -Ein error *.log
  • tail -f *.log
  • Aquí se muestra un conjunto inicial de comandos, pero puede generar una envoltura para cualquier comando de Linux simplemente añadiéndolo a la lista. Si añades este código a tu perfil de PowerShell, estos comandos estarán disponibles en cada sesión de PowerShell al igual que los comandos nativos!

    Parámetros por defecto

    Es común en Linux definir alias y/o variables de entorno dentro de los perfiles de inicio de sesión para establecer parámetros por defecto para los comandos que utilizas frecuentemente (por ejemplo, alias ls=ls -AFh o export LESS=-i). Uno de los inconvenientes de proxiar a través de un shell no interactivo mediante wsl.exe es que no se cargan los perfiles de acceso, por lo que estos parámetros por defecto no están disponibles (por ejemplo ls dentro de WSL y wsl ls se comportarían de forma diferente con el alias definido anteriormente).

    PowerShell proporciona $PSDefaultParameterValues, un mecanismo estándar para definir los valores de los parámetros por defecto, pero sólo para cmdlets y funciones avanzadas. Convertir nuestras envolturas de funciones en funciones avanzadas es posible, pero introduce complicaciones (por ejemplo, PowerShell coincide con los nombres parciales de los parámetros (como la coincidencia de -a para -ArgumentList) que entrará en conflicto con los comandos de Linux que aceptan los nombres parciales como argumentos), y la sintaxis para definir los valores por defecto sería menos que ideal para este escenario (requiriendo el nombre de un parámetro en la clave para definir los argumentos por defecto en lugar de sólo el nombre del comando).¡

    Con un pequeño cambio en nuestras envolturas de funciones, podemos introducir un modelo similar a $PSDefaultParameterValues y habilitar los parámetros por defecto para los comandos de Linux!

function global:$_() { … `$defaultArgs = ((`$WslDefaultParameterValues.$_ -split ' '), "") if (`$input.MoveNext()) { `$input.Reset() `$input | wsl.exe $_ `$defaultArgs (`$args -split ' ') } else { wsl.exe $_ `$defaultArgs (`$args -split ' ') }}

Pasando $WslDefaultParameterValues a la línea de comandos que enviamos a través de wsl.exe, ¡ahora puedes añadir sentencias como las siguientes a tu perfil de PowerShell para configurar parámetros por defecto!

$WslDefaultParameterValues = "-E"$WslDefaultParameterValues = "-i"$WslDefaultParameterValues = "-AFh --group-directories-first" 

Dado que esto está modelado después de $PSDefaultParameterValues, puedes deshabilitarlos temporalmente de forma sencilla estableciendo la clave "Disabled" a $true. Una tabla hash separada tiene la ventaja adicional de poder desactivar $WslDefaultParameterValues por separado de $PSDefaultParameterValues.

Completar argumentos

PowerShell permite registrar completadores de argumentos con el comando Register-ArgumentCompleter. Bash tiene potentes facilidades de completado programable. WSL te permite llamar a bash desde PowerShell. Si podemos registrar completadores de argumentos para nuestros envoltorios de funciones de PowerShell y llamar a través de bash para generar los completadores, ¡podemos obtener un completado de argumentos rico con la misma fidelidad que dentro del 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.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.Extent if ($cursorPosition -lt $extent.EndColumnNumber) { # The cursor is in the middle of a word to complete. $previousWord = $commandAst.CommandElements.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.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.Extent $previousExtent = $commandAst.CommandElements.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.Extent.Text $COMP_CWORD -= 1 } # Build the command to pass to WSL. $command = $commandAst.CommandElements.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 + $_) $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 ::new($completionText, $listItemText, 'ParameterName', $completionText) }}# Helper function to escape characters in arguments passed to WSL that would otherwise be misinterpreted.function global:Format-WslArgument($arg, $interactive) { if ($interactive -and $arg.Contains(" ")) { return "'$arg'" } else { return ($arg -replace " ", "\ ") -replace "()", ('\$1', '`$1') }}

El código es un poco denso sin una comprensión de algunos internos de bash, pero básicamente:

  • Registramos el completador de argumentos para todas nuestras envolturas de funciones pasando la lista $commands al parámetro -CommandName de Register-ArgumentCompleter
  • Mapeamos cada comando a la función de shell que bash utiliza para completarlo ($F que tiene el nombre de complete -F <FUNCTION> utilizado para definir las especificaciones de finalización en bash)
  • Convertimos el $wordToComplete de PowerShell, $commandAst, y $cursorPosition los argumentos en el formato esperado por las funciones de finalización de bash según la especificación de finalización programable de bash
  • Construimos una línea de comandos que podemos pasar a wsl.exe que asegura que el entorno de finalización está configurado correctamente, invoca la función de finalización apropiada, luego emite una cadena que contiene los resultados de la finalización separados por nuevas líneas
  • Luego invocamos wsl con la línea de comandos, dividimos la cadena de salida en el nuevo separador de líneas, luego generamos CompletionResults para cada uno, ordenándolos, y escapando caracteres como espacios y paréntesis que de otra manera serían malinterpretados
    • ¡El resultado final de esto es que ahora nuestras envolturas de comandos de Linux usarán exactamente la misma terminación que usa bash! Por ejemplo:

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

      Cada finalización proporcionará valores específicos para el argumento que le precede, leyendo datos de configuración como hosts conocidos desde dentro de WSL!

      <TAB> recorrerá las opciones. <Ctrl + Space> mostrará todas las opciones disponibles.

      Además, como el completamiento de bash está ahora a cargo, ¡puede resolver las rutas de Linux directamente dentro de PowerShell!

      • less /etc/<TAB>
      • ls /usr/share/<TAB>
      • vim ~/.bash<TAB>
      • En los casos en los que el completamiento bash no devuelve ningún resultado, PowerShell vuelve a su finalización por defecto que resolverá las rutas de Windows, permitiéndole efectivamente resolver tanto las rutas de Linux como las de Windows a voluntad.

        Conclusión

        Con PowerShell y WSL, podemos integrar los comandos de Linux en Windows como si fueran aplicaciones nativas. No hace falta andar buscando builds Win32 de utilidades Linux ni verse obligado a interrumpir el flujo de trabajo para entrar en un shell Linux. Simplemente instale WSL, configure su perfil de PowerShell y liste los comandos que desea importar. La rica terminación de argumentos que se muestra aquí, tanto de las opciones de comandos como de las rutas de archivos de Linux y Windows, es una experiencia que incluso los comandos nativos de Windows no proporcionan hoy en día.

        El código fuente completo descrito anteriormente, así como una guía adicional para incorporarlo a su flujo de trabajo, está disponible en https://github.com/mikebattista/PowerShell-WSL-Interop.

        ¿Qué comandos de Linux encuentra más útiles? Qué otras partes de tu flujo de trabajo de desarrollador encuentras que faltan en Windows?

        ¡Haznoslo saber en los comentarios de abajo o en GitHub!

        Avatar
        Mike Battista

        Administrador de programas senior, Plataforma de Desarrolladores de Windows

        Sigue

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *