Інтэгруемся каманды 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

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 у адпаведнасці са спецыфікацыямі праграмуемага аўтазавяршэння баш.
  • Складаем камандны радок для перадачы ў 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

Дадаць каментар