< 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
doRegister-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 decomplete -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 owsl.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 geramosCompletionResults
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 !