Pagsasama ng mga Linux command sa Windows gamit ang PowerShell at WSL

Isang karaniwang tanong mula sa mga developer ng Windows: “Bakit wala pa rin <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Kung ito ay isang malakas na pag-swipe less o pamilyar na mga kasangkapan grep o sed, gusto ng mga developer ng Windows ng madaling pag-access sa mga command na ito sa kanilang pang-araw-araw na gawain.

Windows Subsystem para sa Linux (WSL) ay gumawa ng isang malaking hakbang pasulong sa bagay na ito. Binibigyang-daan ka nitong tumawag sa mga command ng Linux mula sa Windows sa pamamagitan ng pag-proxy sa kanila wsl.exe (hal. wsl ls). Kahit na ito ay isang makabuluhang pagpapabuti, ang pagpipiliang ito ay naghihirap mula sa isang bilang ng mga disadvantages.

  • Kalat-kalat na karagdagan wsl nakakapagod at hindi natural.
  • Ang mga Windows path sa mga argumento ay hindi palaging gumagana dahil ang mga backslashes ay binibigyang-kahulugan bilang mga escape character kaysa sa mga directory separator.
  • Ang mga Windows path sa mga argumento ay hindi isinasalin sa kaukulang mount point sa WSL.
  • Ang mga default na setting ay hindi iginagalang sa mga profile ng WSL na may mga alias at mga variable ng kapaligiran.
  • Hindi sinusuportahan ang pagkumpleto ng landas ng Linux.
  • Hindi sinusuportahan ang pagkumpleto ng command.
  • Hindi sinusuportahan ang pagkumpleto ng argumento.

Bilang resulta, ang mga utos ng Linux ay itinuturing na parang mga pangalawang klaseng mamamayan sa ilalim ng Windows—at mas mahirap gamitin kaysa sa mga katutubong utos. Upang mapantayan ang kanilang mga karapatan, kinakailangan upang malutas ang mga nakalistang problema.

PowerShell function wrapper

Sa PowerShell function wrapper, maaari tayong magdagdag ng pagkumpleto ng command at alisin ang pangangailangan para sa mga prefix wsl, nagsasalin ng mga Windows path sa mga WSL path. Mga pangunahing kinakailangan para sa mga shell:

  • Para sa bawat Linux command dapat mayroong isang function wrapper na may parehong pangalan.
  • Dapat kilalanin ng shell ang mga landas ng Windows na ipinasa bilang mga argumento at i-convert ang mga ito sa mga landas ng WSL.
  • Dapat tumawag ang shell wsl gamit ang naaangkop na utos ng Linux sa anumang input ng pipeline at pagpasa ng anumang argumento ng command line na ipinasa sa function.

Dahil ang pattern na ito ay maaaring ilapat sa anumang command, maaari nating i-abstract ang kahulugan ng mga wrapper na ito at dynamic na bumuo ng mga ito mula sa isang listahan ng mga command na ii-import.

# 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 ' ')
    }
}
"@
}

Listahan $command tumutukoy sa mga utos sa pag-import. Pagkatapos ay dynamic kaming bumuo ng isang function wrapper para sa bawat isa sa kanila gamit ang command Invoke-Expression (sa pamamagitan ng unang pag-alis ng anumang mga alias na salungat sa function).

Ang function ay umuulit sa mga argumento ng command line, tinutukoy ang mga landas ng Windows gamit ang mga command Split-Path и Test-Pathat pagkatapos ay i-convert ang mga path na ito sa mga WSL path. Pinapatakbo namin ang mga landas sa pamamagitan ng function ng helper Format-WslArgument, na tutukuyin natin mamaya. Tinatakasan nito ang mga espesyal na karakter tulad ng mga puwang at panaklong na kung hindi man ay maaring ma-misinterpret.

Sa wakas, ipinarating namin wsl input ng pipeline at anumang mga argumento ng command line.

Sa mga wrapper na ito maaari mong tawagan ang iyong mga paboritong Linux command sa mas natural na paraan nang hindi nagdaragdag ng prefix wsl at nang hindi nababahala tungkol sa kung paano binago ang mga landas:

  • man bash
  • less -i $profile.CurrentUserAllHosts
  • ls -Al C:Windows | less
  • grep -Ein error *.log
  • tail -f *.log

Ang pangunahing hanay ng mga command ay ipinapakita dito, ngunit maaari kang lumikha ng isang shell para sa anumang Linux command sa pamamagitan lamang ng pagdaragdag nito sa listahan. Kung idaragdag mo ang code na ito sa iyong profile PowerShell, magiging available sa iyo ang mga command na ito sa bawat session ng PowerShell, tulad ng mga native na command!

Mga Default na Setting

Sa Linux, karaniwan nang tumukoy ng mga alias at/o mga variable ng kapaligiran sa mga profile sa pag-login, na nagtatakda ng mga default na parameter para sa mga madalas na ginagamit na command (halimbawa, alias ls=ls -AFh o export LESS=-i). Isa sa mga disadvantage ng proxying sa pamamagitan ng isang non-interactive na shell wsl.exe - na ang mga profile ay hindi na-load, kaya ang mga opsyon na ito ay hindi magagamit bilang default (i.e. ls sa WSL at wsl ls iba ang kilos sa alias na tinukoy sa itaas).

Nagbibigay ang PowerShell $PSDefaultParameterValues, isang karaniwang mekanismo para sa pagtukoy ng mga default na parameter, ngunit para lamang sa mga cmdlet at advanced na function. Siyempre, maaari tayong gumawa ng mga advanced na function mula sa ating mga shell, ngunit nagpapakilala ito ng mga hindi kinakailangang komplikasyon (halimbawa, iniuugnay ng PowerShell ang mga partial na pangalan ng parameter (halimbawa, -a nauugnay sa -ArgumentList), na salungat sa mga utos ng Linux na kumukuha ng mga bahagyang pangalan bilang mga argumento), at ang syntax para sa pagtukoy ng mga default na halaga ay hindi magiging pinakaangkop (ang mga default na argumento ay nangangailangan ng pangalan ng parameter sa susi, hindi lamang ang pangalan ng command) .

Gayunpaman, sa isang bahagyang pagbabago sa aming mga shell, maaari kaming magpatupad ng isang modelo na katulad ng $PSDefaultParameterValues, at paganahin ang mga default na opsyon para sa mga utos ng 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 ' ')
    }
}

pagpasa $WslDefaultParameterValues sa command line, nagpapadala kami ng mga parameter sa pamamagitan ng wsl.exe. Ipinapakita ng sumusunod kung paano magdagdag ng mga tagubilin sa iyong PowerShell profile upang i-configure ang mga default na setting. Ngayon ay magagawa na natin ito!

$WslDefaultParameterValues["grep"] = "-E"
$WslDefaultParameterValues["less"] = "-i"
$WslDefaultParameterValues["ls"] = "-AFh --group-directories-first"

Dahil ang mga parameter ay na-modelo pagkatapos $PSDefaultParameterValueskaya mo madaling i-disable ang mga ito pansamantala sa pamamagitan ng pag-install ng susi "Disabled" sa kahulugan $true. Ang isang karagdagang benepisyo ng isang hiwalay na talahanayan ng hash ay ang kakayahang i-disable $WslDefaultParameterValues hiwalay sa $PSDefaultParameterValues.

Pagkumpleto ng argumento

Pinapayagan ka ng PowerShell na magrehistro ng mga trailer ng argumento gamit ang command Register-ArgumentCompleter. Malakas ang Bash programmable auto-completion tool. Pinapayagan ka ng WSL na tumawag ng bash mula sa PowerShell. Kung maaari naming irehistro ang mga pagkumpleto ng argumento para sa aming mga PowerShell function wrapper at tumawag ng bash upang mabuo ang mga pagkumpleto, makakakuha kami ng ganap na pagkumpleto ng argumento na may parehong katumpakan gaya ng bash mismo!

# 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]
    }
}

Ang code ay medyo siksik nang hindi nauunawaan ang ilan sa mga panloob na pag-andar ng bash, ngunit karaniwang kung ano ang ginagawa namin ay ito:

  • Pagrerehistro ng isang argument completer para sa lahat ng aming function wrapper sa pamamagitan ng pagpasa ng isang listahan $commands sa parameter -CommandName para sa Register-ArgumentCompleter.
  • Mapa namin ang bawat command sa shell function na ginagamit ng bash para sa autocompletion (upang tukuyin ang mga detalye ng autocompletion, ginagamit ng bash $F, pagdadaglat para sa complete -F <FUNCTION>).
  • Kino-convert ang PowerShell Argument $wordToComplete, $commandAst и $cursorPosition sa format na inaasahan ng mga function ng autocompletion ng bash ayon sa mga pagtutukoy programmable na awtomatikong pagkumpleto bash.
  • Bumubuo kami ng command line na ililipat wsl.exe, na nagsisiguro na ang kapaligiran ay naka-set up nang tama, tumatawag sa naaangkop na auto-completion function, at naglalabas ng mga resulta sa line-by-line na paraan.
  • Tapos tumawag kami wsl gamit ang command line, pinaghihiwalay namin ang output sa pamamagitan ng mga line separator at bumubuo para sa bawat isa CompletionResults, pagbubukod-bukod sa mga ito at pagtakas sa mga character tulad ng mga puwang at panaklong na kung hindi man ay mali ang kahulugan.

Bilang resulta, ang aming Linux command shell ay gagamit ng eksaktong parehong autocompletion bilang bash! Halimbawa:

  • ssh -c <TAB> -J <TAB> -m <TAB> -O <TAB> -o <TAB> -Q <TAB> -w <TAB> -b <TAB>

Ang bawat autocompletion ay nagbibigay ng mga value na partikular sa nakaraang argument, binabasa ang data ng configuration gaya ng mga kilalang host mula sa WSL!

<TAB> ay iikot sa mga parameter. <Ctrl + пробел> ipapakita ang lahat ng magagamit na opsyon.

Dagdag pa, dahil mayroon na kaming bash autocompletion, maaari mong i-autocomplete ang mga path ng Linux nang direkta sa PowerShell!

  • less /etc/<TAB>
  • ls /usr/share/<TAB>
  • vim ~/.bash<TAB>

Sa mga kaso kung saan ang bash autocompletion ay hindi nagbubunga ng anumang mga resulta, ang PowerShell ay babalik sa mga default na Windows path ng system. Kaya, sa pagsasanay, maaari mong sabay na gamitin ang parehong mga landas sa iyong paghuhusga.

Konklusyon

Gamit ang PowerShell at WSL, maaari naming isama ang mga command ng Linux sa Windows bilang mga native na application. Hindi na kailangang maghanap ng mga Win32 build o Linux utility o matakpan ang iyong daloy ng trabaho sa pamamagitan ng pagpunta sa isang Linux shell. Basta i-install ang WSL, i-configure Profile ng PowerShell и ilista ang mga utos na gusto mong i-import! Ang rich autocompletion para sa Linux at Windows command parameters at file path ay functionality na hindi pa available sa mga native na command ng Windows ngayon.

Ang buong source code na inilarawan sa itaas, pati na rin ang mga karagdagang alituntunin para sa pagsasama nito sa iyong workflow, ay available dito.

Aling mga utos ng Linux ang nakikita mong pinakakapaki-pakinabang? Ano ang iba pang mga karaniwang bagay ang nawawala kapag nagtatrabaho sa Windows? Sumulat sa mga komento o sa GitHub!

Pinagmulan: www.habr.com

Magdagdag ng komento