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.
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
wslcom 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 bashless -i $profile.CurrentUserAllHostsls -Al C:Windows | lessgrep -Ein error *.logtail -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 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 , 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 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 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
-CommandNameno parâmetroCorrespondemos 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, usamosparaRegister-ArgumentCompleter. - $F
, uma abreviação decomplete -FConvertendo os argumentos do PowerShell). - $wordToComplete
$commandAst,$cursorPositioneno formato esperado pelas funções de preenchimento automático do bash de acordo com as especificações depreenchimento automático programável 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
wslCompletionResults, 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 , configurar e ! 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 .
Quais comandos do Linux você considera mais úteis? Que outras coisas familiares estão faltando ao trabalhar no Windows? Deixe seu comentário ou !
Fonte: habr.com
