<ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>
?». Будь то мощное пролистывание less
или привычные инструменты grep
или sed
, разработчики под Windows хотят получить лёгкий доступ к этим командам в повседневной работе.Подсистема Windows для Linux (WSL) сделала огромный шаг вперёд в этом отношении. Она позволяет вызывать команды Linux из Windows, проксируя их через
wsl.exe
(например, wsl ls
). Хотя это значительное улучшение, но такой вариант страдает от ряда недостатков.- Повсеместное добавление
wsl
утомительно и неестественно.
- Пути Windows в аргументах не всегда срабатывают, потому что обратные слэши интерпретируются как escape-символы, а не разделители каталогов.
- Пути Windows в аргументах не переводятся в соответствующую точку монтирования в WSL.
- Не учитываются параметры по умолчанию в профилях WSL с алиасами и переменными окружения.
- Не поддерживается завершение путей Linux.
- Не поддерживается завершение команд.
- Не поддерживается завершение аргументов.
В результате команды Linux воспринимаются под Windows как граждане второго сорта — и их сложнее использовать, чем родные команды. Чтобы уравнять их в правах, нужно решить перечисленные проблемы.
Оболочки функций PowerShell
C помощью оболочек функций 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 принято определять алиасы и/или переменные окружения в профилях (login profile), задавая параметры по умолчанию для часто используемых команд (например,
alias ls=ls -AFh
или export LESS=-i
). Один из недостатков проксирования через неинтерактивную оболочку wsl.exe
— то, что профили не загружаются, поэтому эти параметры по умолчанию недоступны (т. е. ls
в WSL и wsl ls
будут вести себя по-разному с алиасом, определённым выше).PowerShell предоставляет $PSDefaultParameterValues, стандартный механизм для определения параметров по умолчанию, но только для командлетов и расширенных функций. Конечно, можно из наших оболочек сделать расширенные функции, но это вносит лишние осложнения (так, 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 <FUNCTION>
).
- Преобразуем аргументы PowerShell
$wordToComplete
,$commandAst
и$cursorPosition
в формат, ожидаемый функциями автозавершения bash в соответствии со спецификациями программируемого автозавершения bash.
- Составляем командную строку для передачи в
wsl.exe
, который обеспечивает правильную настройку среды, вызывает соответствующую функцию автозавершения и выводит результаты с разбиением по строкам.
- Затем вызываем
wsl
с командной строкой, разделяем выдачу разделителями строк и генерируем для каждойCompletionResults
, сортируя их и экранируя символы, такие как пробелы и скобки, которые в противном случае были бы неверно истолкованы.
В итоге наши оболочки команд Linux будут использовать точно такое же автозавершение, как в bash! Например:
ssh -c <TAB> -J <TAB> -m <TAB> -O <TAB> -o <TAB> -Q <TAB> -w <TAB> -b <TAB>
Каждое автозавершение подоставляет значения, специфичные для предыдущего аргумента, считывая данные конфигурации, такие как известные хосты, из 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!
Комментарии (25)
Fox_exe
28.09.2019 16:25Собственно, давно использую «Bash for windows», который идет в составе Git'а.
Ставится в пару кликов без каких-либо костылей.
Для Сисадминов, что привыкли иметь дело с Linux — вещь незаменимая и куда более удобная, чем PowerShell (К которому ещё и привыкать надо).EvgeniyNuAfanasievich
29.09.2019 12:16+1У bash есть взаимодействие с AD, преобразование объектов в Sid и обратно и вот это всё?
Fox_exe
29.09.2019 13:27Ну совсем без костылей не обойтись, да.
Вот только Bash вполне может воспринимать команды тогоже PowerShell'а. Только с башем работать както легче…
Alkop
28.09.2019 17:01В Powershell очень не хватает нормальной работы с History и было бы удобно иметь что-то наподобие Ctrl+R под которой запускается «hh» от ultradvorka.
ForNeVeR
29.09.2019 18:49+1Начиная с какой-то версии они начали встраивать PSReadline, в котором Ctrl-R работает из коробки. Попробуйте :)
Alkop
29.09.2019 20:32в 5.1 работает точно — спасибо :)
Но работает не так как hh от ultradvorka — выводит только одну строку в которой искомый паттерн попадается в последней команде в хистори — не то :(
При этом Get-History всё ещё выводит только историю текущей сессии… Как так-то? :(
411
28.09.2019 22:20+1Перешёл на WSL 2, иногда использую как раз команды powershell(например для получения информации по занятой памяти) и файлы .exe (например для копирования из vim в буфер винды) в нём, но не наоборот. У каждого конечно свои задачи, но добавить ещё сюда новый виндовый терминал и необходимость в таком решении полностью отпадёт.
llia6an
29.09.2019 03:20Если абстрагироваться от вопроса, какая ОС лучше, то есть несколько моментов, которые у винды неудобнее в консоли. Первое — обратные слэши. В других операционках используется обычный слэш для пути, а обратный для экранирования. Обычный легче набирать, так как он на двух кливишах и без шифтов, обратный же на одной и с шифтом. Второе — верхний регистр. Имена программ редко содержат верхний регистр, а вот ключи довольно часто, особенно powershell. В терминале сильно работаешь клавой, и эти мелочи уменьшают продуктивность. Не зря например в питоне отказались от скобок и точек с запятой, так тупо меньше и легче набирать. Все эти штуки с wsl прикольно, но это больше похоже на крик бессилия, типа возьмите нашу консоль, мы прикрутим к ней что угодно, лишь бы вы её полюбили. Я годами ждал, терпел и ждал, когда всё пойдёт хорошо, но в итоге наигрался в игры, со спокойной душой поставил минт и начал получать удовольствие от того, что нет теперь 'C:\Program Files', а есть '/usr/bin', нет 'C:\Documents and Settings\Application Data\Roaming', а есть '/home//.config'. Может я привёл не полные аналоги папок, ну я думаю вы поняли.
mistergrim
29.09.2019 05:58+1обратный же на одной и с шифтом
С каких это пор он с шифтом? Более того, он набирается в любой раскладке одной и той же клавишей, в отличие от.Имена программ редко содержат верхний регистр, а вот ключи довольно часто
Это вы точно про Windows? А то я тут ключи ls посмотрел, например.llia6an
29.09.2019 13:13Вы правы, без шифта. А что с ключами ls? Да есть в верхнем регистре, но они однобуквенные, в некоторых программах много ключей, и только нижнего регистра не хватает. Открыл википедию про powershell. Таблица сравнения командлетов с аналогичными командами. Зачем писать Get-Location, если можно написать get-location. Два раза нужно нажимать шифт. Это избыточно. Нам же не любоваться потом тем, что мы там написали.
mistergrim
30.09.2019 08:09С ключами ls то, что -a и -A, -b и -B (и далее по списку) — это разные ключи. И такое сплошь и рядом.
Зачем писать Get-Location, если можно написать get-location.
Так а кто мешает? В powershell команды нечувствительны к регистру.
Goodkat
29.09.2019 11:50+1А что, в Windows 10 отменили обычные слэши в путях?
"C:/Program Files" раньше тоже работало.
Да и буква диска обычно не нужна, так что путь /Users/Goodkat/Documents в Windows мало отличается от /home/Goodkat/Documents в GNU/Linux.llia6an
29.09.2019 13:15А что, у всех windows 10?
Goodkat
29.09.2019 13:25+1Вы о чём, вообще?
WSL появилось в Windows 10. В статье и вашем комментарии речь о WSL.
До Windows 10 (скорее всего и в Windows 10 тоже — было бы странным ломать обратную совместимость, но я не проверял) в путях можно использовать прямые слэши наравне с обратными.
А теперь поясните, пожалуйста, что вы имели в виду в своём последнем комментарии.
ForNeVeR
29.09.2019 18:52Все известные мне системные API давно умели в прямые слэши в путях (кроме разных интересных случаев с UNC и т.п.). Ну то есть как минимум с XP, а то и с NT4 это работает нормально.
Единственная существенная проблема, если вы пытаетесь перейти на использование прямых слэшей везде — это
cmd.exe
, он не любит такие слэши в некоторых случаях (в большинстве?). А всё остальное обычно работает.
ForNeVeR
29.09.2019 18:56Буква диска обычно не нужна, только если у вас всё на одном диске.
Само по себе утверждение, на мой взгляд, опасное — вон, ребята в Meteor в течение очень долгого времени не могли починить старт приложения с любого другого диска, кроме C — из-за того, что думали сходным образом :)
Работать с путями стоит очень осторожно. Хотя если вы не разрабатываете софт, а пользуетесь уже готовым, в рамках повседневной деятельности можно спокойно передавать пути без указания диска. Вы верно заметили, что это должно работать.
Goodkat
29.09.2019 19:32+1Речь идёт о работе в консоли, когда приходится руками набирать пути.
ForNeVeR
29.09.2019 19:50Да, спасибо, я это понял и просто на всякий случай предупредил о более общих проблемах, которые возникают на практике, и поделился смешными историями.
Ну, мало ли, вдруг кто-то из участников дискуссии считает, что уже давно на всех Windows-машинах диск остался только один? :)
Это ни в коем случае не возражение, а скорее дополнение к сказанному вами.
sved
29.09.2019 03:43Странно пользователям Windows жаловаться на отсутствие утилит grep и sed.
Ведь их можно просто установить .
Ну Cygwin наше всё. Без него вообще никуда, я считаю.
Полезные графические утилиты там тоже присутствуют. Мне например нравится baobabjusthabrauser
29.09.2019 12:10+2Статье не хватает тега «Троллейбус из буханки хлеба».
Это не про удобство работы.
teakettle
30.09.2019 15:10Реквестирую вставку
apt-get update && apt-get upgrade
, чтобы оно научилось все-таки нормально обновляться…
Bedal
sved
Похоже, у вас очень узкое представление о разработчиках, либо у вас очень специфические задачи
Bedal
Наверно, Вы правы. 40 лет личного программленья и руководства командой программёров явно недостаточно для формирования сколько-нибудь значимого мнения.