Интегриране на Linux команди в Windows с помощта на PowerShell и WSL
Типичен въпрос от разработчиците на Windows: „Защо все още няма <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Независимо дали е мощен удар less или познати инструменти grep или sed, разработчиците на Windows искат лесен достъп до тези команди в ежедневната си работа.
Подсистема Windows за Linux (WSL) направи огромна крачка напред в това отношение. Позволява ви да извиквате Linux команди от Windows, като ги проксиирате wsl.exe (Например, wsl ls). Въпреки че това е значително подобрение, тази опция страда от редица недостатъци.
Повсеместно допълнение wsl досадно и неестествено.
Пътищата на Windows в аргументите не винаги работят, защото обратните наклонени черти се интерпретират като екраниращи знаци, а не като разделители на директории.
Пътищата на Windows в аргументите не се превеждат към съответната точка на монтиране в WSL.
Настройките по подразбиране не се зачитат в WSL профили с псевдоними и променливи на средата.
Завършването на пътя на Linux не се поддържа.
Завършването на команда не се поддържа.
Довършването на аргументи не се поддържа.
В резултат на това командите на Linux се третират като граждани от втора класа под Windows и са по-трудни за използване от родните команди. За изравняването на правата им е необходимо решаването на изброените проблеми.
Обвивки на функции PowerShell
С обвивките на функции на PowerShell можем да добавим завършване на команда и да елиминираме необходимостта от префикси wsl, превеждане на пътища на Windows в пътища на WSL. Основни изисквания за черупките:
За всяка Linux команда трябва да има една обвивка на функция със същото име.
Обвивката трябва да разпознае предадените като аргументи пътища на Windows и да ги преобразува в WSL пътища.
Черупката трябва да се обади wsl със съответната Linux команда към всеки вход на тръбопровода и предаване на всички аргументи на командния ред, предадени на функцията.
Тъй като този модел може да се приложи към всяка команда, можем да абстрахираме дефиницията на тези обвивки и динамично да ги генерираме от списък с команди за импортиране.
# 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 ' ')
}
}
"@
}
Списък $command дефинира команди за импортиране. След това динамично генерираме обвивка на функция за всеки от тях с помощта на командата Invoke-Expression (като първо премахнете всички псевдоними, които биха били в конфликт с функцията).
Функцията итерира аргументите на командния ред, определя пътищата на Windows с помощта на команди Split-Path и Test-Pathи след това преобразува тези пътища в WSL пътища. Пускаме пътеките чрез помощна функция Format-WslArgument, което ще дефинираме по-късно. Той избягва специални знаци като интервали и скоби, които иначе биха били изтълкувани погрешно.
Накрая предаваме wsl въвеждане на конвейер и всички аргументи на командния ред.
С тези обвивки можете да извиквате любимите си Linux команди по по-естествен начин, без да добавяте префикс wsl и без да се притеснявате как се преобразуват пътищата:
man bash
less -i $profile.CurrentUserAllHosts
ls -Al C:Windows | less
grep -Ein error *.log
tail -f *.log
Основният набор от команди е показан тук, но можете да създадете обвивка за всяка команда на Linux, като просто я добавите към списъка. Ако добавите този код към вашия профил PowerShell, тези команди ще бъдат достъпни за вас във всяка сесия на PowerShell, точно като собствените команди!
Настройките по подразбиране
В Linux е обичайно да се дефинират псевдоними и/или променливи на средата в профилите за влизане, задавайки параметри по подразбиране за често използвани команди (например, alias ls=ls -AFh или export LESS=-i). Един от недостатъците на прокси чрез неинтерактивна обвивка wsl.exe - че профилите не са заредени, така че тези опции не са налични по подразбиране (т.е. ls в WSL и wsl ls ще се държи различно с псевдонима, дефиниран по-горе).
PowerShell предоставя $PSDefaultParameterValues, стандартен механизъм за дефиниране на параметри по подразбиране, но само за кратки команди и разширени функции. Разбира се, можем да създадем разширени функции от нашите обвивки, но това въвежда ненужни усложнения (например PowerShell корелира частични имена на параметри (например, -a корелира с -ArgumentList), което ще бъде в конфликт с команди на Linux, които приемат частични имена като аргументи), а синтаксисът за дефиниране на стойности по подразбиране няма да е най-подходящият (аргументите по подразбиране изискват името на параметъра в ключа, а не само името на командата) .
Въпреки това, с лека модификация на нашите черупки, можем да приложим модел, подобен на $PSDefaultParameterValuesи активирайте опциите по подразбиране за Linux команди!
Преминаване $WslDefaultParameterValues към командния ред, ние изпращаме параметри чрез wsl.exe. Следното показва как да добавите инструкции към вашия PowerShell профил, за да конфигурирате настройките по подразбиране. Сега можем да го направим!
Тъй като параметрите са моделирани след $PSDefaultParameterValues, Можеш лесно е да ги деактивирате временно чрез инсталиране на ключа "Disabled" в смисъл $true. Допълнително предимство на отделната хеш таблица е възможността за деактивиране $WslDefaultParameterValues отделно от $PSDefaultParameterValues.
Завършване на аргумента
PowerShell ви позволява да регистрирате трейлъри на аргументи с помощта на командата Register-ArgumentCompleter. Bash има мощен програмируеми инструменти за автоматично довършване. WSL ви позволява да извиквате bash от PowerShell. Ако можем да регистрираме довършвания на аргументи за нашите обвивки на функции PowerShell и извикаме bash, за да генерираме довършванията, получаваме пълно завършване на аргументи със същата прецизност като самия 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]
}
}
Кодът е малко плътен, без да разбира някои от вътрешните функции на bash, но основно това, което правим, е следното:
Регистриране на инструмент за завършване на аргументи за всички наши обвивки на функции чрез предаване на списък $commands в параметър -CommandName за Register-ArgumentCompleter.
Ние свързваме всяка команда с функцията на обвивката, която bash използва за автоматично довършване (за да дефинира спецификациите за автоматично довършване, bash използва $F, съкращение от complete -F <FUNCTION>).
Преобразуване на аргументи на PowerShell $wordToComplete, $commandAst и $cursorPosition във формата, очакван от функциите за автоматично довършване на bash според спецификациите програмируемо автоматично довършване баш
Съставяме команден ред, към който да прехвърлим wsl.exe, който гарантира, че средата е настроена правилно, извиква подходящата функция за автоматично довършване и извежда резултатите ред по ред.
След това се обаждаме wsl с командния ред разделяме изхода чрез разделители на редове и генерираме за всеки CompletionResults, като ги сортирате и избягвате знаци като интервали и скоби, които иначе биха били изтълкувани погрешно.
В резултат на това нашите командни обвивки на Linux ще използват точно същото автоматично довършване като bash! Например:
Всяко автоматично довършване предоставя стойности, специфични за предишния аргумент, като чете конфигурационни данни като известни хостове от WSL!
<TAB> ще премине през параметрите. <Ctrl + пробел> ще покаже всички налични опции.
Плюс това, тъй като вече имаме bash автодовършване, можете да довършвате автоматично Linux пътища директно в PowerShell!
less /etc/<TAB>
ls /usr/share/<TAB>
vim ~/.bash<TAB>
В случаите, когато автоматичното довършване на bash не дава никакви резултати, PowerShell се връща към системните пътища по подразбиране на Windows. Така на практика можете да използвате едновременно и двата пътя по свое усмотрение.
Заключение
Използвайки PowerShell и WSL, можем да интегрираме Linux команди в Windows като родни приложения. Няма нужда да търсите компилации на Win32 или помощни програми за Linux или да прекъсвате работния си процес, като отидете в обвивка на Linux. Просто инсталирайте WSL, конфигурирайте PowerShell профил и избройте командите, които искате да импортирате! Богатото автоматично довършване за командни параметри на Linux и Windows и пътеки към файлове е функционалност, която днес дори не е налична в собствените команди на Windows.
Наличен е пълният изходен код, описан по-горе, както и допълнителни указания за включването му във вашия работен процес тук.
Кои команди на Linux намирате за най-полезни? Какви други често срещани неща липсват при работа в Windows? Напишете в коментарите или на GitHub!