СофтХабрахабр

[Перевод] Интегрируем команды Linux в Windows с помощью PowerShell и WSL

Типичный вопрос разработчиков под Windows: «Почему здесь до сих пор нет <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?». Будь то мощное пролистывание less или привычные инструменты grep или sed, разработчики под Windows хотят получить лёгкий доступ к этим командам в повседневной работе.

Она позволяет вызывать команды Linux из Windows, проксируя их через wsl.exe (например, wsl ls). Подсистема Windows для Linux (WSL) сделала огромный шаг вперёд в этом отношении. Хотя это значительное улучшение, но такой вариант страдает от ряда недостатков.

  • Повсеместное добавление wsl утомительно и неестественно.
  • Пути Windows в аргументах не всегда срабатывают, потому что обратные слэши интерпретируются как escape-символы, а не разделители каталогов.
  • Пути Windows в аргументах не переводятся в соответствующую точку монтирования в WSL.
  • Не учитываются параметры по умолчанию в профилях WSL с алиасами и переменными окружения.
  • Не поддерживается завершение путей Linux.
  • Не поддерживается завершение команд.
  • Не поддерживается завершение аргументов.

В результате команды Linux воспринимаются под Windows как граждане второго сорта — и их сложнее использовать, чем родные команды. Чтобы уравнять их в правах, нужно решить перечисленные проблемы.
C помощью оболочек функций PowerShell vы можем добавить автозавершение команд и устранить необходимость в префиксах 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:$_() 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 (сначала удалив любые алиасы, которые будут конфликтовать с функцией).

Мы запускаем пути через вспомогательную функцию Format-WslArgument, которую определим позже. Функция перебирает аргументы командной строки, определяет пути Windows с помощью команд Split-Path и Test-Path, а затем преобразует эти пути в пути WSL. Она экранирует специальные символы, такие как пробелы и скобки, которые в противном случае были бы неверно истолкованы.

Наконец, передаём 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 соотносит частичные имена параметров (например, -a соотносится с -ArgumentList), которые будут конфликтовать с командами Linux, принимающими частичные имена в качестве аргументов), а синтаксис для определения значений по умолчанию будет не самым подходящим (для определения аргументов по умолчанию требуется имя параметра в ключе, а не только имя команды). PowerShell предоставляет $PSDefaultParameterValues, стандартный механизм для определения параметров по умолчанию, но только для командлетов и расширенных функций.

Однако с небольшим изменением наших оболочек мы можем внедрить модель, аналогичную $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!

<Ctrl + пробел> покажет все доступные опции. <TAB> будет циклически перебирать параметры.

Кроме того, поскольку теперь у нас работает автозавершение 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.

Полный исходный код, описанный выше, а также дополнительные рекомендации по его включению в рабочий процесс доступны здесь.

Каких ещё привычных вещей не хватает при работе в Windows? Какие команды Linux вы считаете наиболее полезными? Пишите в комментариях или на GitHub!

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Проверьте также

Закрыть
Кнопка «Наверх»
Закрыть