Integrimi i komandave Linux në Windows duke përdorur PowerShell dhe WSL

Një pyetje tipike nga zhvilluesit e Windows: "Pse nuk ka ende <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Pavarësisht nëse është një rrëshqitje e fuqishme less ose mjete të njohura grep ose sed, zhvilluesit e Windows duan qasje të lehtë në këto komanda në punën e tyre të përditshme.

Nënsistemi Windows për Linux (WSL) ka bërë një hap të madh përpara në këtë drejtim. Kjo ju lejon të thërrisni komandat Linux nga Windows duke i proxy ato përmes wsl.exe (p.sh., wsl ls). Edhe pse ky është një përmirësim i rëndësishëm, ky opsion vuan nga një sërë disavantazhesh.

  • Shtim i kudondodhur wsl e lodhshme dhe e panatyrshme.
  • Shtigjet e Windows në argumente nuk funksionojnë gjithmonë sepse kthesat e prapme interpretohen si karaktere ikjeje dhe jo si ndarës të drejtorive.
  • Shtigjet e Windows në argumente nuk përkthehen në pikën përkatëse të montimit në WSL.
  • Cilësimet e parazgjedhura nuk respektohen në profilet WSL me pseudonime dhe ndryshore mjedisore.
  • Përfundimi i rrugës Linux nuk mbështetet.
  • Përfundimi i komandës nuk mbështetet.
  • Përfundimi i argumentit nuk mbështetet.

Si rezultat, komandat Linux trajtohen si qytetarë të klasit të dytë nën Windows - dhe janë më të vështira për t'u përdorur sesa komandat vendase. Për të barazuar të drejtat e tyre, është e nevojshme të zgjidhen problemet e listuara.

Mbështjellësit e funksionit PowerShell

Me mbështjellëset e funksionit PowerShell, ne mund të shtojmë kompletimin e komandës dhe të eliminojmë nevojën për parashtesa wsl, duke përkthyer shtigjet e Windows në shtigje WSL. Kërkesat themelore për predha:

  • Për çdo komandë Linux duhet të ketë një mbështjellës funksioni me të njëjtin emër.
  • Predha duhet të njohë shtigjet e Windows të kaluar si argumente dhe t'i konvertojë ato në shtigje WSL.
  • Predha duhet të thërrasë wsl me komandën e duhur Linux në çdo hyrje të tubacionit dhe duke kaluar çdo argument të linjës së komandës që i kalon funksionit.

Meqenëse ky model mund të zbatohet për çdo komandë, ne mund të abstragojmë përkufizimin e këtyre mbështjellësve dhe t'i gjenerojmë ato në mënyrë dinamike nga një listë komandash për 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 ' ')
    }
}
"@
}

Listë $command përcakton komandat e importit. Më pas ne gjenerojmë në mënyrë dinamike një mbështjellës funksioni për secilën prej tyre duke përdorur komandën Invoke-Expression (duke hequr fillimisht çdo pseudonim që do të binte ndesh me funksionin).

Funksioni përsëritet mbi argumentet e linjës së komandës, përcakton shtigjet e Windows duke përdorur komanda Split-Path и Test-Pathdhe më pas i konverton këto shtigje në shtigje WSL. Ne i drejtojmë shtigjet përmes një funksioni ndihmës Format-WslArgument, të cilin do ta përcaktojmë më vonë. I shpëton karaktereve të veçanta si hapësira dhe kllapa që përndryshe do të keqinterpretoheshin.

Së fundi, ne përcjellim wsl hyrja e tubacionit dhe çdo argument i linjës së komandës.

Me këto mbështjellës ju mund të telefononi komandat tuaja të preferuara Linux në një mënyrë më të natyrshme pa shtuar një prefiks wsl dhe pa u shqetësuar se si shndërrohen shtigjet:

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

Grupi bazë i komandave shfaqet këtu, por ju mund të krijoni një guaskë për çdo komandë Linux thjesht duke e shtuar atë në listë. Nëse e shtoni këtë kod në tuajin tuaj profil PowerShell, këto komanda do të jenë të disponueshme për ju në çdo sesion të PowerShell, ashtu si komandat amtare!

Cilësimet e parazgjedhura

Në Linux, është e zakonshme të përcaktohen pseudonimet dhe/ose variablat e mjedisit në profilet e hyrjes, duke vendosur parametrat e paracaktuar për komandat e përdorura shpesh (për shembull, alias ls=ls -AFh ose export LESS=-i). Një nga disavantazhet e proxying përmes një shell jo-interaktive wsl.exe - që profilet nuk janë të ngarkuara, kështu që këto opsione nuk janë të disponueshme si parazgjedhje (d.m.th. ls në WSL dhe wsl ls do të sillet ndryshe me pseudonimin e përcaktuar më sipër).

PowerShell ofron $PSDefaultParameterValues, një mekanizëm standard për përcaktimin e parametrave të paracaktuar, por vetëm për cmdlet dhe funksione të avancuara. Natyrisht, ne mund të bëjmë funksione të avancuara nga predha tona, por kjo sjell komplikime të panevojshme (për shembull, PowerShell lidh emrat e parametrave të pjesshëm (për shembull, -a lidhet me -ArgumentList), i cili do të jetë në konflikt me komandat Linux që marrin emra të pjesshëm si argumente), dhe sintaksa për përcaktimin e vlerave të paracaktuara nuk do të jetë më e përshtatshme (argumentet e parazgjedhura kërkojnë emrin e parametrit në çelës, jo vetëm emrin e komandës) .

Megjithatë, me një modifikim të vogël në guaskat tona, ne mund të zbatojmë një model të ngjashëm me $PSDefaultParameterValues, dhe aktivizoni opsionet e paracaktuara për komandat 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 ' ')
    }
}

Duke kaluar $WslDefaultParameterValues në vijën e komandës, ne dërgojmë parametra nëpërmjet wsl.exe. Më poshtë tregon se si të shtoni udhëzime në profilin tuaj PowerShell për të konfiguruar cilësimet e paracaktuara. Tani mund ta bëjmë!

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

Meqenëse parametrat janë modeluar pas $PSDefaultParameterValuesmundeni është e lehtë t'i çaktivizosh ato përkohësisht duke instaluar çelësin "Disabled" në kuptim $true. Një përfitim shtesë i një tabele të veçantë hash është aftësia për të çaktivizuar $WslDefaultParameterValues veçmas nga $PSDefaultParameterValues.

Plotësimi i argumentit

PowerShell ju lejon të regjistroni trailerat e argumenteve duke përdorur komandën Register-ArgumentCompleter. Bash ka fuqi mjete të programueshme të plotësimit automatik. WSL ju lejon të telefononi bash nga PowerShell. Nëse mund të regjistrojmë plotësimet e argumenteve për mbështjellësit tanë të funksionit PowerShell dhe të thërrasim bash për të gjeneruar kompletimet, marrim plotësimin e plotë të argumentit me të njëjtën saktësi si vetë 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]
    }
}

Kodi është pak i dendur pa kuptuar disa nga funksionet e brendshme të bash, por në thelb ajo që ne bëjmë është kjo:

  • Regjistrimi i një plotësuesi të argumenteve për të gjithë mbështjellësit e funksioneve tona duke kaluar një listë $commands në parametër -CommandName për Register-ArgumentCompleter.
  • Ne hartojmë secilën komandë me funksionin e guaskës që bash përdor për plotësimin automatik (për të përcaktuar specifikimet e plotësimit automatik, përdor bash $F, shkurtim për complete -F <FUNCTION>).
  • Konvertimi i argumenteve të PowerShell $wordToComplete, $commandAst и $cursorPosition në formatin e pritur nga funksionet e plotësimit automatik të bash sipas specifikimeve kompletim automatik i programueshëm bash.
  • Ne krijojmë një linjë komande për ta transferuar wsl.exe, i cili siguron që mjedisi është konfiguruar saktë, thërret funksionin e duhur të plotësimit automatik dhe nxjerr rezultatet në mënyrë rresht pas rreshti.
  • Pastaj ne thërrasim wsl me vijën e komandës, ne ndajmë daljen me ndarës të rreshtave dhe gjenerojmë për secilën CompletionResults, duke i renditur ato dhe duke i shpëtuar karaktereve si hapësira dhe kllapa që përndryshe do të keqinterpretoheshin.

Si rezultat, predhat tona të komandave Linux do të përdorin saktësisht të njëjtin plotësim automatik si bash! Për shembull:

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

Çdo plotësim automatik jep vlera specifike për argumentin e mëparshëm, duke lexuar të dhënat e konfigurimit siç janë hostet e njohur nga WSL!

<TAB> do të qarkullojë nëpër parametra. <Ctrl + пробел> do të shfaqë të gjitha opsionet e disponueshme.

Plus, meqenëse tani kemi plotësim automatik bash, ju mund të plotësoni automatikisht shtigjet e Linux direkt në PowerShell!

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

Në rastet kur plotësimi automatik i bash nuk prodhon ndonjë rezultat, PowerShell kthehet në shtigjet e parazgjedhura të sistemit të Windows. Kështu, në praktikë, ju mund të përdorni njëkohësisht të dyja shtigjet sipas gjykimit tuaj.

Përfundim

Duke përdorur PowerShell dhe WSL, ne mund të integrojmë komandat Linux në Windows si aplikacione vendase. Nuk ka nevojë të kërkoni për ndërtime Win32 ose shërbime Linux ose të ndërprisni rrjedhën e punës duke shkuar te një guaskë Linux. Vetëm instaloni WSL, konfiguroni Profili i PowerShell и listoni komandat që dëshironi të importoni! Plotësimi automatik i pasur për parametrat e komandave Linux dhe Windows dhe shtigjet e skedarëve është funksionalitet që nuk disponohet as në komandat e Windows-it sot.

Kodi i plotë burimor i përshkruar më sipër, si dhe udhëzime shtesë për përfshirjen e tij në rrjedhën tuaj të punës, është i disponueshëm këtu.

Cilat komanda Linux ju duken më të dobishme? Cilat gjëra të tjera të zakonshme mungojnë kur punoni në Windows? Shkruani në komente ose në GitHub!

Burimi: www.habr.com

Shto një koment