Интегриране на 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 команди!

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!

Източник: www.habr.com

Добавяне на нов коментар