Integramos comandos Linux no Windows usando PowerShell e WSL

Uma pergunta típica dos desenvolvedores Windows: "Por que ainda não há <INSIRA AQUI O SEU COMANDO LINUX FAVORITO>?». Будь то мощное пролистывание less ou ferramentas familiares grep ou sed, os desenvolvedores Windows querem ter acesso fácil a esses comandos no dia a dia.

O Subsistema Windows para Linux (WSL) deu um grande passo à frente nesse aspecto. Ele permite chamar comandos Linux a partir do Windows, fazendo a intermediação através de wsl.exe (por exemplo, wsl ls). Embora essa seja uma melhoria significativa, essa abordagem sofre de vários inconvenientes.

  • A inclusão generalizada wsl é cansativa e antinatural.
  • Caminhos do Windows em argumentos nem sempre funcionam, pois as barras invertidas são interpretadas como caracteres de escape, e não como separadores de diretórios.
  • Caminhos do Windows em argumentos não são traduzidos para o ponto de montagem correspondente no WSL.
  • As opções padrão em perfis WSL com aliases e variáveis de ambiente não são consideradas.
  • A conclusão de caminhos Linux não é suportada.
  • A conclusão de comandos não é suportada.
  • A conclusão de argumentos não é suportada.

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

Funções do PowerShell

Com as funções do PowerShell, podemos adicionar a conclusão automática de comandos e eliminar a necessidade de prefixos wsl, traduzindo caminhos do Windows para caminhos WSL. Os requisitos principais para as funções são:

  • Para cada comando Linux, deve haver uma função com o mesmo nome.
  • A função deve reconhecer os caminhos do Windows passados como argumentos e convertê-los em caminhos WSL.
  • A função deve chamar wsl com o comando Linux correspondente para qualquer entrada no pipeline e passar quaisquer argumentos da linha de comando fornecidos à função.

Como esse padrão pode ser aplicado a qualquer comando, podemos abstrair a definição dessas funções e gerá-las dinamicamente a partir de uma lista de comandos para importação.

# 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 os comandos para importação. Em seguida, geramos dinamicamente uma função wrapper 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 percorre os argumentos da linha de comando, identifica os caminhos do Windows usando os comandos Split-Path e Test-Path, e depois converte esses caminhos em caminhos WSL. Estamos passando caminhos por meio de uma função auxiliar Format-WslArgument, que definiremos mais tarde. Ela escapa caracteres especiais, como espaços e parênteses, que de outra forma seriam interpretados incorretamente.

Finalmente, passamos wsl a entrada do pipeline e quaisquer argumentos de linha de comando.

Com esses wrappers, é possível chamar seus comandos Linux favoritos de maneira mais natural, sem adicionar um prefixo wsl e sem se preocupar com como os caminhos são convertidos:

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

Aqui está um conjunto básico de comandos, mas você pode criar um wrapper para qualquer comando Linux, basta adicioná-lo à lista. Se você adicionar este código ao seu perfil PowerShell, esses comandos estarão disponíveis em cada sessão do PowerShell, assim como os comandos nativos!

Parâmetros padrão

No Linux, é comum definir aliases e/ou variáveis de ambiente em perfis (login profile), definindo parâmetros padrão para comandos frequentemente usados (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, esses parâmetros padrão não estão disponíveis (ou seja, ls no WSL e wsl ls se comportarão 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, é possível transformar nossos shells em funções avançadas, mas isso traz complicações desnecessárias (por exemplo, PowerShell relaciona nomes parciais de parâmetros (por exemplo, -a relaciona-se com -ArgumentList), que entrarão em conflito com comandos Linux que aceitam nomes parciais como argumentos), e a sintaxe para definir valores padrão não será a mais adequada (para definir argumentos padrão, é necessário o nome do parâmetro na chave, e não apenas o nome do comando).

No entanto, com uma pequena modificação em nossos shells, podemos implementar um modelo semelhante $PSDefaultParameterValues, e incluir parâmetros padrão para 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 ' ')
    }
}

Passando $WslDefaultParameterValues na linha de comando, enviamos parâmetros através de wsl.exe. Abaixo está mostrado como adicionar instruções ao perfil do PowerShell para configurar os parâmetros padrão. Agora podemos fazer isso!

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

Uma vez que os parâmetros são modelados após $PSDefaultParameterValues, você pode desativá-los facilmente temporariamente, definindo a chave "Disabled" como valor $true. Uma vantagem adicional de uma tabela hash separada é a capacidade de desativar $WslDefaultParameterValues separadamente de $PSDefaultParameterValues.

O preenchimento automático de argumentos

O PowerShell permite registrar completadores de argumentos usando o comando Register-ArgumentCompleter. O Bash possui ferramentas programáveis poderosas para preenchimento automático. O WSL permite chamar o bash a partir do PowerShell. Se pudermos registrar completadores de argumentos para nossas funções de shell do PowerShell e chamar o bash para criar conclusões, obteremos preenchimento automático de argumentos com a mesma precisão que no próprio bash!O código é um pouco denso sem compreender algumas funções internas do bash, mas principalmente fazemos o seguinte:

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

Registramos o completador de argumentos para todas as nossas funções de wrapper, passando a lista

  • $commands -CommandName no parâmetro Correspondemos cada comando com a função de shell que o bash usa para preenchimento automático (para determinar as especificações de preenchimento automático no bash, usamos para Register-ArgumentCompleter.
  • $F , uma abreviação decomplete -F Convertendo os argumentos do PowerShell).
  • $wordToComplete $commandAst, $cursorPosition e no formato esperado pelas funções de preenchimento automático do bash de acordo com as especificações de preenchimento automático programável bash. Compomos a linha de comando a ser passada para
  • , que garante a configuração adequada do ambiente, chama a função de preenchimento automático correspondente e gera os resultados com quebra de linha. wsl.exeEm seguida, chamamos
  • com a linha de comando, dividindo a saída por quebras de linha e gerando para cada wsl CompletionResults , ordenando-os e escapando caracteres como espaços e parênteses, que de outra forma seriam mal interpretados.No final, nossas shells de comando Linux usarão o mesmo preenchimento automático que no bash! Por exemplo:

ssh -c -J -m -O -o -Q -w -b

  • ssh -c -J -m -O -o -Q -w -b

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

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

Além disso, agora que temos o completamento automático do bash funcionando, você pode completar caminhos do Linux diretamente no PowerShell!

  • less /etc/
  • ls /usr/share/
  • vim ~/ .bash

Nos casos em que o completamento automático do bash não retorna resultados, o PowerShell reverte ao sistema padrão com caminhos do Windows. Assim, na prática, você pode usar ambos os caminhos à sua escolha.

Conclusão

Com o PowerShell e o WSL, podemos integrar comandos do Linux no Windows como aplicativos nativos. Não há necessidade de procurar builds Win32 ou utilitários do Linux ou interromper o fluxo de trabalho mudando para o shell do Linux. Basta instalar o WSL, configurar o perfil do PowerShell e listar os comandos que deseja importar! O completamento automático rico para parâmetros de comandos e caminhos de arquivos do Linux e Windows é uma funcionalidade que não existe hoje nem mesmo nos comandos nativos do Windows.

O código-fonte completo descrito acima, assim como recomendações adicionais para incluí-lo no fluxo de trabalho, está disponível aqui.

Quais comandos do Linux você considera mais úteis? Que outras coisas familiares estão faltando ao trabalhar no Windows? Deixe seu comentário ou no GitHub!

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster