Integración de comandos de Linux en Windows mediante PowerShell e WSL

Unha pregunta típica dos desenvolvedores de Windows: "Por que aínda non hai <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Tanto se se trata dun golpe poderoso less ou ferramentas coñecidas grep ou sed, Os desenvolvedores de Windows queren un acceso sinxelo a estes comandos no seu traballo diario.

Subsistema de Windows para Linux (WSL) deu un gran paso adiante neste sentido. Permítelle chamar comandos de Linux desde Windows mediante un proxy wsl.exe (por exemplo wsl ls). Aínda que esta é unha mellora significativa, esta opción presenta unha serie de desvantaxes.

  • Engadido omnipresente wsl tedioso e antinatural.
  • As rutas de Windows nos argumentos non sempre funcionan porque as barras inclinadas inversas interprétanse como caracteres de escape en lugar de como separadores de directorios.
  • As rutas de Windows nos argumentos non se traducen ao punto de montaxe correspondente en WSL.
  • Non se respectan os axustes predeterminados nos perfís WSL con alias e variables de ambiente.
  • Non se admite a finalización da ruta de Linux.
  • Non se admite a realización de comandos.
  • Non se admite a finalización do argumento.

Como resultado, os comandos de Linux son tratados como cidadáns de segunda clase baixo Windows, e son máis difíciles de usar que os comandos nativos. Para igualar os seus dereitos, é necesario resolver os problemas enumerados anteriormente.

Envoltorios de funcións de PowerShell

Cos envoltorios de funcións de PowerShell, podemos engadir a finalización de comandos e eliminar a necesidade de prefixos wsl, traducindo camiños de Windows en camiños WSL. Requisitos básicos para as cunchas:

  • Para cada comando de Linux debe haber un envoltorio de funcións co mesmo nome.
  • O shell debe recoñecer as rutas de Windows pasadas como argumentos e convertelas en rutas WSL.
  • O shell debería chamar wsl co comando Linux apropiado a calquera entrada de canalización e pasando os argumentos da liña de comandos pasados ​​á función.

Dado que este patrón pódese aplicar a calquera comando, podemos abstraer a definición destes envoltorios e xeralos de forma dinámica a partir dunha 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. Despois xeramos dinámicamente un envoltorio de funcións para cada un deles mediante o comando Invoke-Expression (eliminando primeiro os alias que entrarían en conflito coa función).

A función itera sobre os argumentos da liña de comandos, determina as rutas de Windows mediante comandos Split-Path и Test-Pathe despois converte estes camiños en camiños WSL. Percorremos os camiños a través dunha función auxiliar Format-WslArgument, que definiremos máis adiante. Escapa de caracteres especiais como espazos e parénteses que doutro xeito serían mal interpretados.

Por último, transmitimos wsl entrada de pipeline e calquera argumento da liña de comandos.

Con estes envoltorios podes chamar aos teus comandos favoritos de Linux dun xeito máis natural sen engadir un prefixo wsl e sen preocuparse de como se converten os camiños:

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

O conxunto básico de comandos móstrase aquí, pero pode crear un shell para calquera comando de Linux simplemente engadíndoo á lista. Se engades este código ao teu perfil PowerShell, estes comandos estarán dispoñibles para ti en cada sesión de PowerShell, igual que os comandos nativos.

Configuración predeterminada

En Linux, é común definir alias e/ou variables de ambiente nos perfís de inicio de sesión, establecendo parámetros predeterminados para os comandos de uso frecuente (por exemplo, alias ls=ls -AFh ou export LESS=-i). Unha das desvantaxes do proxy a través dun shell non interactivo wsl.exe - que os perfís non están cargados, polo que estas opcións non están dispoñibles de forma predeterminada (i.e. ls en WSL e wsl ls comportarase de forma diferente co alias definido anteriormente).

PowerShell proporciona $PSDefaultParameterValues, un mecanismo estándar para definir parámetros predeterminados, pero só para cmdlets e funcións avanzadas. Por suposto, podemos facer funcións avanzadas dos nosos shells, pero isto introduce complicacións innecesarias (por exemplo, PowerShell correlaciona os nomes de parámetros parciais (por exemplo, -a correlaciona con -ArgumentList), que entrará en conflito cos comandos de Linux que toman nomes parciais como argumentos), e a sintaxe para definir os valores predeterminados non será a máis axeitada (para definir os argumentos predeterminados require o nome do parámetro na clave, non só o nome do comando).

Non obstante, cunha lixeira modificación dos nosos shells, podemos implementar un modelo similar a $PSDefaultParameterValues, e activa as opcións predeterminadas para os 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 ' ')
    }
}

Ao transmitir $WslDefaultParameterValues á liña de comandos, enviamos parámetros mediante wsl.exe. O seguinte mostra como engadir instrucións ao teu perfil de PowerShell para configurar os axustes predeterminados. Agora podemos facelo!

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

Dado que os parámetros están modelados $PSDefaultParameterValues, Podes é fácil desactivalos temporalmente instalando a chave "Disabled" en significado $true. Un beneficio adicional dunha táboa hash separada é a posibilidade de desactivala $WslDefaultParameterValues separadamente de $PSDefaultParameterValues.

Finalización do argumento

PowerShell permítelle rexistrar tráilers de argumentos mediante o comando Register-ArgumentCompleter. Bash ten poder ferramentas programables de autocompletado. WSL permítelle chamar a bash desde PowerShell. Se podemos rexistrar as conclusións dos argumentos para os nosos envoltorios de funcións de PowerShell e chamar a bash para xerar as conclusións, obtemos a finalización completa dos argumentos coa mesma precisión que o 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]
    }
}

O código é un pouco denso sen entender algunhas das funcións internas de bash, pero basicamente o que facemos é isto:

  • Rexistrando un completador de argumentos para todos os nosos envoltorios de funcións pasando unha lista $commands en parámetro -CommandName para Register-ArgumentCompleter.
  • Asignamos cada comando á función de shell que usa bash para o autocompletado (para definir as especificacións de autocompletado, bash usa $F, abreviatura de complete -F <FUNCTION>).
  • Conversión de argumentos de PowerShell $wordToComplete, $commandAst и $cursorPosition no formato esperado polas funcións de autocompletado de bash segundo as especificacións autocompletado programable bash.
  • Compoñemos unha liña de comandos para transferir wsl.exe, que garante que o ambiente estea configurado correctamente, chama á función de autocompletar adecuada e emite os resultados liña por liña.
  • Despois chamamos wsl coa liña de comandos, separamos a saída por separadores de liña e xeramos para cada un CompletionResults, clasificándoos e escapando caracteres como espazos e parénteses que doutro xeito serían mal interpretados.

Como resultado, os nosos shells de comandos de Linux usarán exactamente o mesmo autocompletado que bash! Por exemplo:

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

Cada autocompletado proporciona valores específicos para o argumento anterior, lendo datos de configuración como os hosts coñecidos de WSL.

<TAB> fará un ciclo a través dos parámetros. <Ctrl + пробел> mostrará todas as opcións dispoñibles.

Ademais, xa que agora temos autocompletado bash, podes completar automaticamente as rutas de Linux directamente en PowerShell.

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

Nos casos nos que o autocompletado bash non produce ningún resultado, PowerShell volve ás rutas predeterminadas do sistema de Windows. Así, na práctica, pode usar simultáneamente ambos os camiños á súa discreción.

Conclusión

Usando PowerShell e WSL, podemos integrar comandos de Linux en Windows como aplicacións nativas. Non hai necesidade de buscar compilacións Win32 ou utilidades de Linux nin interromper o seu fluxo de traballo indo a un shell de Linux. Só instalar WSL, configurar Perfil de PowerShell и lista os comandos que queres importar! O autocompletado rico para os parámetros de comandos de Linux e Windows e as rutas de ficheiros é unha funcionalidade que nin sequera está dispoñible nos comandos nativos de Windows hoxe.

O código fonte completo descrito anteriormente, así como as directrices adicionais para incorporalo ao seu fluxo de traballo, están dispoñibles aquí.

Que comandos de Linux che parecen máis útiles? Que outras cousas comúns faltan cando se traballa en Windows? Escribe nos comentarios ou en github!

Fonte: www.habr.com

Engadir un comentario