Често срещан въпрос на разработчиците под Windows: „Защо все още няма <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?». Будь то мощное пролистывание less или познатите инструменти grep или sed, разработчиците под Windows искат лесен достъп до тези команди в ежедневната работа.
направи огромна крачка напред в това отношение. Тя позволява извикването на Linux команди от Windows, проксирвайки ги през wsl.exe (например, wsl ls). Въпреки че е значително подобрение, този вариант страда от редица недостатъци.
- Разпространеното добавяне
wslе уморително и неестествено. - Windows пътищата в аргументите не винаги работят, защото обратните наклонени черти се интерпретират като escape-символи, а не като разделители на директории.
- 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 и Тестов Път, след което преобразува тези пътища в пътища на WSL. Ние преминаваме пътищата през помощна функция Format-WslArgument, която ще дефинираме по-късно. Тя ескейпира специални символи, като празни места и скоби, които иначе биха били неправилно интерпретирани.
Накрая предаваме wsl входните данни на конвейера и всичките аргументи от командния ред.
Чрез такива обвивки можете да извиквате любимите си команди на Linux по-естествен начин, без да добавяте префикс wsl и без да се притеснявате как се преобразуват пътищата:
man bashless -i $profile.CurrentUserAllHostsls -Al C:Windows | lessgrep -Ein error *.logtail -f *.log
Тук е показан основен набор от команди, но можете да създадете обвивка за всяка команда на Linux, просто като я добавите в списъка. Ако добавите този код в своя PowerShell, тези команди ще бъдат достъпни за вас във всяка сесия на PowerShell, точно като нативни команди!
Стандартни параметри
В Linux е обичайно да се определят алиаси и/или променливи на средата в профилите (login profile), задавайки стандартни параметри за често използвани команди (например, alias ls=ls -AFh или export LESS=-i). Един от недостатъците на проксито през неинтерактивна обвивка wsl.exe е, че профилите не се зареждат, така че тези стандартни параметри не са на разположение (т.е. ls в WSL и wsl ls ще се държат по различен начин с алиас, дефиниран по-горе).
PowerShell предоставя , стандартен механизъм за определяне на стандартни параметри, но само за командлети и разширени функции. Разбира се, можем да превърнем нашите обвивки в разширени функции, но това добавя излишни усложнения (така, PowerShell съпоставя частични имена на параметри (например, -a се съпоставя с -ArgumentList), които ще влязат в конфликт с команди на Linux, приемайки частични имена като аргументи), а синтаксисът за определяне на стандартни стойности не би бил най-подходящият (за да определите аргументи по подразбиране, е нужно името на параметъра в ключа, а не само името на командата).
Въпреки това, с малка промяна на нашите обвивки можем да внедрим модел, подобен на $PSDefaultParameterValues, и да включим стандартни параметри за командите на 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 ' ')
}
} Предавайки $WslDefaultParameterValues в командния ред, ние изпращаме параметри чрез wsl.exe. По-долу е показано как да добавите инструкции в профила PowerShell за настройка на параметрите по подразбиране. Сега можем да направим това!
$WslDefaultParameterValues["grep"] = "-E"
$WslDefaultParameterValues["less"] = "-i"
$WslDefaultParameterValues["ls"] = "-AFh --group-directories-first" Тъй като параметрите са моделирани след $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). - Преобразуваме аргументите на PowerShell
$wordToComplete,$commandAstи$cursorPositionв формат, очакван от функциите за автоматично завършване на bash в съответствие с спецификациите на bash. - Ситуацията на командния ред за предаване в
wsl.exe, която осигурява правилно настройване на средата, извиква съответната функция за автоматично завършване и извежда резултатите с разделяне по редове. - След това извикваме
wslс командния ред, разделяме изхода с разделители за редове и генерираме за всякаCompletionResults, като ги сортираме и екранираме символите, като интервали и скоби, които иначе биха били неправилно интерпретирани.
В крайна сметка нашите команди в Linux ще използват точно същото автоматично завършване, както в bash! Например:
ssh -c -J -m -O -o -Q -w -b
Автозавършването предоставя стойности, специфични за предишния аргумент, извличайки конфигурационни данни като познати хостове от WSL!
<TAB> ще преминава през параметрите циклично. <Ctrl + пробел> ще покаже всички налични опции.
Освен това, сега когато имаме работа с автозавършване на bash, можете да завършвате пътища на Linux директно в PowerShell!
less /etc/ls /usr/share/vim ~/ .bash
В случаите, когато автозавършването на bash не дава резултати, PowerShell се връща към системата по подразбиране с Windows пътища. Така на практика можете да използвате и двата типа пътища едновременно по ваша преценка.
Заключение
С PowerShell и WSL можем да интегрираме Linux команди в Windows като нативни приложения. Няма нужда да търсите Win32 версии или Linux инструменти, нито да прекъсвате работния процес, преминавайки в Linux команден ред. Просто , конфигурирайте и ! Богатото автозавършване за командни параметри и пътища на файлове Linux и Windows е функционалност, която дори не съществува в нативните команди на Windows.
Целият изходен код, описан по-горе, както и допълнителни насоки за неговото интегриране в работния процес са налични .
Кои Linux команди смятате за най-полезни? Какви още познати неща липсват при работа с Windows? Напишете в коментарите или !
Източник: habr.com
