Інтегруємо команди Linux у Windows за допомогою PowerShell та WSL

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

Підсистема Windows для Linux (WSL) зробила величезний крок уперед у цьому плані. Вона дозволяє викликати команди 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 и 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 відповідно до специфікацій програмованого автозавершення удар.
  • Складаємо командний рядок для передачі в 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!

Джерело: habr.com

Додати коментар або відгук