Integrando comandos do Linux ao Windows usando PowerShell e WSL

Uma pergunta típica dos desenvolvedores do Windows: “Por que ainda não existe <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Quer seja um golpe poderoso less ou ferramentas familiares grep ou sed, os desenvolvedores do Windows desejam acesso fácil a esses comandos em seu trabalho diário.

Subsistema Windows para Linux (WSL) deu um enorme passo em frente neste sentido. Ele permite que você chame comandos do Linux a partir do Windows, fazendo proxy deles através wsl.exe (Por exemplo, wsl ls). Embora esta seja uma melhoria significativa, esta opção apresenta várias desvantagens.

  • Adição onipresente wsl tedioso e antinatural.
  • Os caminhos do Windows nos argumentos nem sempre funcionam porque as barras invertidas são interpretadas como caracteres de escape em vez de separadores de diretório.
  • Os caminhos do Windows nos argumentos não são traduzidos para o ponto de montagem correspondente no WSL.
  • As configurações padrão não são respeitadas em perfis WSL com aliases e variáveis ​​de ambiente.
  • A conclusão do caminho Linux não é suportada.
  • A conclusão do comando não é suportada.
  • A conclusão do argumento não é suportada.

Como resultado, os comandos do Linux são tratados como cidadãos de segunda classe no Windows – e são mais difíceis de usar do que os comandos nativos. Para equalizar seus direitos, é necessário resolver os problemas listados.

Wrappers de função do PowerShell

Com wrappers de função do PowerShell, podemos adicionar a conclusão de comandos e eliminar a necessidade de prefixos wsl, traduzindo caminhos do Windows em caminhos WSL. Requisitos básicos para shells:

  • Para cada comando do Linux deve haver um wrapper de função com o mesmo nome.
  • O shell deve reconhecer os caminhos do Windows passados ​​como argumentos e convertê-los em caminhos WSL.
  • O shell deve chamar wsl com o comando Linux apropriado para qualquer entrada de pipeline e passando quaisquer argumentos de linha de comando passados ​​para a função.

Como esse padrão pode ser aplicado a qualquer comando, podemos abstrair a definição desses wrappers e gerá-los dinamicamente a partir de uma 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 importação. Em seguida, geramos dinamicamente um wrapper de função para cada um deles usando o comando Invoke-Expression (primeiro removendo quaisquer aliases que possam entrar em conflito com a função).

A função itera sobre argumentos de linha de comando, determina caminhos do Windows usando comandos Split-Path и Test-Pathe então converte esses caminhos em caminhos WSL. Executamos os caminhos por meio de uma função auxiliar Format-WslArgument, que definiremos mais adiante. Ele escapa de caracteres especiais, como espaços e parênteses, que de outra forma seriam mal interpretados.

Por fim, transmitimos wsl entrada do pipeline e quaisquer argumentos de linha de comando.

Com esses wrappers você pode chamar seus comandos favoritos do Linux de uma forma mais natural, sem adicionar um prefixo wsl e sem se preocupar em como os caminhos são convertidos:

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

O conjunto básico de comandos é mostrado aqui, mas você pode criar um shell para qualquer comando do Linux simplesmente adicionando-o à lista. Se você adicionar este código ao seu perfil PowerShell, esses comandos estarão disponíveis para você em todas as sessões do PowerShell, assim como os comandos nativos!

Parâmetros padrão

No Linux, é comum definir aliases e/ou variáveis ​​de ambiente em perfis de login, definindo parâmetros padrão para comandos usados ​​com frequência (por exemplo, alias ls=ls -AFh ou export LESS=-i). Uma das desvantagens do proxy através de um shell não interativo wsl.exe - que os perfis não são carregados, portanto estas opções não estão disponíveis por padrão (ou seja, ls na WSL e wsl ls se comportará de maneira diferente com o alias definido acima).

PowerShell fornece $PSDefaultParameterValues, um mecanismo padrão para definir parâmetros padrão, mas apenas para cmdlets e funções avançadas. É claro que podemos criar funções avançadas a partir de nossos shells, mas isso introduz complicações desnecessárias (por exemplo, o PowerShell correlaciona nomes de parâmetros parciais (por exemplo, -a correlaciona com -ArgumentList), que entrará em conflito com comandos do Linux que levam nomes parciais como argumentos), e a sintaxe para definir valores padrão não será a mais apropriada (argumentos padrão requerem o nome do parâmetro na chave, não apenas o nome do comando) .

Porém, com uma ligeira modificação em nossos shells, podemos implementar um modelo semelhante ao $PSDefaultParameterValuese ative as opções padrão para comandos do 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 ' ')
    }
}

Passagem $WslDefaultParameterValues para a linha de comando, enviamos parâmetros via wsl.exe. Veja a seguir como adicionar instruções ao seu perfil do PowerShell para definir as configurações padrão. Agora podemos fazer isso!

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

Como os parâmetros são modelados a partir de $PSDefaultParameterValues, Você pode é fácil desativá-los temporariamente instalando a chave "Disabled" em significado $true. Um benefício adicional de uma tabela hash separada é a capacidade de desativar $WslDefaultParameterValues separadamente de $PSDefaultParameterValues.

Conclusão do argumento

PowerShell permite registrar trailers de argumentos usando o comando Register-ArgumentCompleter. Bash tem poderoso ferramentas de preenchimento automático programáveis. WSL permite que você chame o bash do PowerShell. Se pudermos registrar conclusões de argumentos para nossos wrappers de função do PowerShell e chamar o bash para gerar as conclusões, obteremos a conclusão completa dos argumentos com a mesma precisão que o próprio 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 é um pouco denso sem entender algumas funções internas do bash, mas basicamente o que fazemos é isto:

  • Registrando um completador de argumento para todos os nossos wrappers de função passando uma lista $commands no parâmetro -CommandName para Register-ArgumentCompleter.
  • Mapeamos cada comando para a função shell que o bash usa para preenchimento automático (para definir especificações de preenchimento automático, o bash usa $F, abreviatura de complete -F <FUNCTION>).
  • Convertendo argumentos do PowerShell $wordToComplete, $commandAst и $cursorPosition no formato esperado pelas funções de preenchimento automático do bash de acordo com as especificações preenchimento automático programável festança.
  • Compomos uma linha de comando para transferir para wsl.exe, que garante que o ambiente esteja configurado corretamente, chama a função de preenchimento automático apropriada e gera os resultados linha por linha.
  • Então ligamos wsl com a linha de comando, separamos a saída por separadores de linha e geramos para cada CompletionResults, classificando-os e escapando de caracteres como espaços e parênteses que, de outra forma, seriam mal interpretados.

Como resultado, nossos shells de comando do Linux usarão exatamente o mesmo preenchimento automático do bash! Por exemplo:

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

Cada preenchimento automático fornece valores específicos do argumento anterior, lendo dados de configuração como hosts conhecidos do WSL!

<TAB> percorrerá os parâmetros. <Ctrl + пробел> mostrará todas as opções disponíveis.

Além disso, como agora temos o preenchimento automático do bash, você pode preencher automaticamente os caminhos do Linux diretamente no PowerShell!

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

Nos casos em que o preenchimento automático do bash não produz nenhum resultado, o PowerShell reverte para os caminhos padrão do Windows do sistema. Assim, na prática, você pode usar simultaneamente os dois caminhos a seu critério.

Conclusão

Usando PowerShell e WSL, podemos integrar comandos do Linux ao Windows como aplicativos nativos. Não há necessidade de procurar compilações Win32 ou utilitários Linux ou interromper seu fluxo de trabalho acessando um shell Linux. Apenas instalar WSL, configurar Perfil do PowerShell и liste os comandos que você deseja importar! O preenchimento automático avançado para parâmetros de comando e caminhos de arquivo do Linux e Windows é uma funcionalidade que hoje nem está disponível nos comandos nativos do Windows.

O código-fonte completo descrito acima, bem como diretrizes adicionais para incorporá-lo ao seu fluxo de trabalho, estão disponíveis aqui.

Quais comandos do Linux você considera mais úteis? Que outras coisas comuns estão faltando ao trabalhar no Windows? Escreva nos comentários ou no GitHub!

Fonte: habr.com

Adicionar um comentário