?". Seja a rolagem less eficiente ou as ferramentas grep ou...">

Integramos comandos do Linux no Windows usando PowerShell e WSL

Uma pergunta típica para desenvolvedores do Windows: "Por que ainda não está aqui < LINUX> ?". Seja a rolagem less eficiente ou as ferramentas grep ou sed usuais, os desenvolvedores do Windows desejam acesso fácil a esses comandos no trabalho diário.

O Windows Subsystem for Linux (WSL) deu um grande passo adiante nesse sentido. Ele permite que você chame comandos do Linux a partir do Windows, executando o proxy deles através do wsl.exe (por exemplo, wsl ls ). Embora essa seja uma melhoria significativa, essa opção sofre várias desvantagens.

  • A adição onipresente de wsl cansativa e antinatural.
  • Os caminhos do Windows nos argumentos nem sempre funcionam, porque as barras invertidas são interpretadas como caracteres de escape, não como separadores de diretório.
  • Os caminhos do Windows nos argumentos não são convertidos no ponto de montagem correspondente na WSL.
  • As configurações padrão nos perfis WSL com aliases e variáveis ​​de ambiente não são levadas em consideração.
  • A conclusão do caminho do 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 percebidos no Windows como cidadãos de segunda classe - e são mais difíceis de usar do que as equipes nativas. Para equalizar seus direitos, você precisa resolver esses problemas.

Conchas do PowerShell


Usando os wrappers de função do PowerShell, podemos adicionar a conclusão de comandos e eliminar a necessidade de prefixos wsl convertendo os caminhos do Windows em caminhos da WSL. Requisitos básicos para os reservatórios:

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

Como esse modelo pode ser aplicado a qualquer comando, podemos abstrair a definição desses shells e gerá-los dinamicamente a partir da lista de comandos a serem importados.

 # 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 (eg 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 ' ') } } "@ } 

A lista de $command define os comandos a serem importados. Em seguida, geramos dinamicamente um wrapper de função para cada um deles usando o comando Invoke-Expression (primeiro removendo quaisquer aliases que entrarão em conflito com a função).

A função itera sobre os argumentos da linha de comando, determina os caminhos do Windows usando os comandos Split-Path e Test-Path e, em seguida, converte esses caminhos em caminhos WSL. Format-WslArgument os caminhos através da função auxiliar Format-WslArgument , que definiremos mais adiante. Ele escapa caracteres especiais, como espaços e colchetes, que de outra forma seriam mal interpretados.

Por fim, passamos a entrada do wsl e quaisquer argumentos da linha de comando para o wsl .

Usando esses wrappers, você pode chamar seus comandos favoritos do Linux de uma maneira mais natural, sem adicionar o prefixo wsl e sem se preocupar com a conversão dos caminhos:

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

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

Opções padrão


No Linux, é habitual definir aliases e / ou variáveis ​​de ambiente em perfis (perfil de login), configurando 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 por meio do shell wsl.exe não interativo é que os perfis não são carregados; portanto, essas opções não estão disponíveis por padrão (ou seja, ls na WSL e wsl ls se comportam de maneira diferente com o alias definido acima).

O PowerShell fornece $ PSDefaultParameterValues , um mecanismo padrão para definir parâmetros padrão, mas apenas para cmdlets e funções avançadas. Obviamente, você pode criar funções avançadas de nossos shells, mas isso introduz complicações desnecessárias (por exemplo, o PowerShell corresponde a nomes de parâmetros parciais (por exemplo, -a corresponde a -ArgumentList ), que entrará em conflito com os comandos do Linux que usam nomes parciais como argumentos) e a sintaxe para definir valores padrão não será a mais adequada (para definir argumentos padrão, o nome do parâmetro na chave é necessário e não apenas o nome do comando).

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

Ao $WslDefaultParameterValues para a linha de comando, enviamos os parâmetros por meio de wsl.exe . A seguir, mostra como adicionar instruções a um perfil do PowerShell para definir as configurações padrão. Agora nós podemos fazer isso!

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

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

Conclusão de Argumento


O PowerShell permite registrar terminadores de argumento usando o comando Register-ArgumentCompleter . O Bash possui poderosas ferramentas de conclusão programáveis . O WSL permite que você chame o bash do PowerShell. Se pudermos registrar os terminadores de argumento para nossos wrappers de função do PowerShell e chamar o bash para criar as terminações, obteremos a conclusão completa dos argumentos com a mesma precisão do 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 rígido sem entender alguns dos elementos internos do bash, mas basicamente fazemos o seguinte:

  • Registramos o finalizador de argumentos para todos os nossos wrappers de funções passando a lista de $commands para o parâmetro -CommandName do Register-ArgumentCompleter .
  • Mapeamos cada comando para a função shell que o bash usa para o preenchimento automático (o bash usa $F para definir especificações do preenchimento automático, abreviação de complete -F <FUNCTION> ).
  • Converta argumentos do PowerShell $wordToComplete , $commandAst e $cursorPosition no formato esperado pelas funções de conclusão do bash, de acordo com as especificações de conclusão programáveis ​​do bash.
  • wsl.exe a linha de comando para transferir para o wsl.exe , que garante a configuração correta do ambiente, chama a função de preenchimento automático apropriada e exibe os resultados com quebras de linha.
  • Em seguida, chamamos wsl com a linha de comando, separamos a saída com separadores de linha e geramos CompletionResults para cada um, classificando-os e escapando caracteres como espaços e colchetes 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 ao argumento anterior, lendo dados de configuração, como hosts conhecidos, da WSL!

<TAB> alternará entre os parâmetros. <Ctrl + > mostrará todas as opções disponíveis.

Além disso, como o preenchimento automático do bash agora funciona conosco, 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 falha, o PowerShell reverte para o sistema padrão com caminhos do Windows. Assim, na prática, você pode usar simultaneamente essas e outras maneiras a seu critério.

Conclusão


Com o PowerShell e o WSL, podemos integrar comandos do Linux no Windows como aplicativos nativos. Não há necessidade de procurar compilações do Win32 ou utilitários Linux ou interromper o fluxo de trabalho alternando para o shell do Linux. Basta instalar o WSL , configurar seu perfil do PowerShell e listar os comandos que você deseja importar ! O rico preenchimento automático de parâmetros de comando e caminho para arquivos Linux e Windows é uma funcionalidade que nem hoje é encontrada nos comandos nativos do Windows atualmente.

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

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

Source: https://habr.com/ru/post/pt469257/


All Articles