Integracija Linux naredbi u Windows pomoću PowerShell i WSL

Tipično pitanje Windows programera: “Zašto još uvijek nema <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Bilo da se radi o snažnom zamahu less ili poznatih alata grep ili sed, Windows programeri žele jednostavan pristup ovim naredbama u svom svakodnevnom radu.

Windows podsustav za Linux (WSL) je u tom smislu napravila veliki korak naprijed. Omogućuje vam pozivanje Linux naredbi iz Windowsa proxyjem wsl.exe (Na primjer, wsl ls). Iako je ovo značajno poboljšanje, ova opcija ima brojne nedostatke.

  • Sveprisutni dodatak wsl zamorno i neprirodno.
  • Windows staze u argumentima ne rade uvijek jer se obrnute kose crte tumače kao izlazni znakovi, a ne kao separatori direktorija.
  • Windows staze u argumentima nisu prevedene na odgovarajuću točku montiranja u WSL-u.
  • Zadane postavke ne poštuju se u WSL profilima s aliasima i varijablama okruženja.
  • Dovršavanje staze u Linuxu nije podržano.
  • Dovršavanje naredbi nije podržano.
  • Dovršavanje argumenata nije podržano.

Kao rezultat toga, Linux naredbe se pod Windowsima tretiraju kao građani drugog reda—i teže ih je koristiti od izvornih naredbi. Za izjednačavanje njihovih prava potrebno je riješiti navedene probleme.

Omotači funkcija PowerShell

S omotačima funkcija PowerShell možemo dodati dovršavanje naredbi i eliminirati potrebu za prefiksima wsl, prevođenje Windows staza u WSL staze. Osnovni zahtjevi za školjke:

  • Za svaku Linux naredbu mora postojati jedan omotač funkcije s istim imenom.
  • Ljuska mora prepoznati Windows staze proslijeđene kao argumente i pretvoriti ih u WSL staze.
  • Ljuska bi trebala nazvati wsl s odgovarajućom Linux naredbom na bilo koji ulaz cjevovoda i prosljeđivanjem svih argumenata naredbenog retka proslijeđenih funkciji.

Budući da se ovaj obrazac može primijeniti na bilo koju naredbu, možemo apstrahirati definiciju ovih omotača i dinamički ih generirati iz popisa naredbi za uvoz.

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

popis $command definira naredbe za uvoz. Zatim dinamički generiramo omotač funkcije za svaku od njih pomoću naredbe Invoke-Expression (najprije uklanjanjem svih aliasa koji bi bili u sukobu s funkcijom).

Funkcija ponavlja argumente naredbenog retka, određuje Windows staze pomoću naredbi Split-Path и Test-Patha zatim te staze pretvara u WSL staze. Provlačimo staze kroz pomoćnu funkciju Format-WslArgument, što ćemo kasnije definirati. Izbjegava posebne znakove kao što su razmaci i zagrade koji bi inače bili pogrešno protumačeni.

Na kraju, prenosimo wsl unos cjevovoda i sve argumente naredbenog retka.

Pomoću ovih omotača možete pozvati svoje omiljene Linux naredbe na prirodniji način bez dodavanja prefiksa wsl i bez brige o tome kako se staze pretvaraju:

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

Ovdje je prikazan osnovni skup naredbi, ali možete stvoriti ljusku za bilo koju Linux naredbu jednostavnim dodavanjem na popis. Ako dodate ovaj kôd svom profil PowerShell, ove naredbe bit će vam dostupne u svakoj PowerShell sesiji, baš kao i izvorne naredbe!

Zadane postavke

U Linuxu je uobičajeno definirati aliase i/ili varijable okruženja u profilima za prijavu, postavljajući zadane parametre za često korištene naredbe (na primjer, alias ls=ls -AFh ili export LESS=-i). Jedan od nedostataka proxyja kroz neinteraktivnu ljusku wsl.exe - da profili nisu učitani, pa ove opcije nisu dostupne prema zadanim postavkama (tj. ls u WSL i wsl ls ponašat će se drugačije s gore definiranim aliasom).

PowerShell pruža $PSDefaultParameterValues, standardni mehanizam za definiranje zadanih parametara, ali samo za cmdlete i napredne funkcije. Naravno, možemo napraviti napredne funkcije od naših ljuski, ali to uvodi nepotrebne komplikacije (na primjer, PowerShell povezuje djelomična imena parametara (na primjer, -a korelira s -ArgumentList), što će biti u sukobu s Linux naredbama koje uzimaju djelomične nazive kao argumente), a sintaksa za definiranje zadanih vrijednosti neće biti najprikladnija (zadani argumenti zahtijevaju naziv parametra u ključu, a ne samo naziv naredbe) .

Međutim, uz male izmjene naših ljuski, možemo implementirati model sličan $PSDefaultParameterValues, i omogućite zadane opcije za Linux naredbe!

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 ' ')
    }
}

Pretjecanje $WslDefaultParameterValues u naredbeni redak, šaljemo parametre putem wsl.exe. Sljedeće pokazuje kako dodati upute svom PowerShell profilu za konfiguriranje zadanih postavki. Sada mi to možemo!

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

Budući da su parametri modelirani prema $PSDefaultParameterValuesmožeš lako ih je onemogućiti privremeno ugradnjom ključa "Disabled" u vrijednosti $true. Dodatna prednost zasebne hash tablice je mogućnost onemogućavanja $WslDefaultParameterValues odvojeno od $PSDefaultParameterValues.

Završetak argumenta

PowerShell vam omogućuje da registrirate najave argumenata pomoću naredbe Register-ArgumentCompleter. Bash je moćan programabilni alati za automatsko dovršavanje. WSL vam omogućuje da pozovete bash iz PowerShell-a. Ako možemo registrirati dovršavanja argumenata za naše omotače funkcija PowerShell i pozvati bash za generiranje dovršavanja, dobivamo potpuno dovršavanje argumenata s istom preciznošću kao i sam 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]
    }
}

Kod je pomalo gust bez razumijevanja nekih bashovih internih funkcija, ali u osnovi ono što radimo je ovo:

  • Registriranje dovršivača argumenata za sve naše omotače funkcija prosljeđivanjem popisa $commands u parametru -CommandName za Register-ArgumentCompleter.
  • Svaku naredbu preslikavamo na funkciju ljuske koju bash koristi za automatsko dovršavanje (za definiranje specifikacija automatskog dovršavanja, bash koristi $F, skraćenica za complete -F <FUNCTION>).
  • Pretvaranje PowerShell argumenata $wordToComplete, $commandAst и $cursorPosition u format koji očekuju bashove funkcije automatskog dovršavanja prema specifikacijama programabilno automatsko dovršavanje udarac.
  • Sastavljamo naredbeni redak za prijenos wsl.exe, koji osigurava da je okruženje ispravno postavljeno, poziva odgovarajuću funkciju automatskog dovršavanja i ispisuje rezultate redak po redak.
  • Onda zovemo wsl pomoću naredbenog retka odvajamo izlaz separatorima reda i generiramo za svaki CompletionResults, njihovo sortiranje i izbjegavanje znakova poput razmaka i zagrada koji bi inače bili pogrešno protumačeni.

Kao rezultat toga, naše Linux naredbene ljuske će koristiti potpuno isto automatsko dovršavanje kao bash! Na primjer:

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

Svako automatsko dovršavanje isporučuje vrijednosti specifične za prethodni argument, čitajući konfiguracijske podatke kao što su poznati hostovi iz WSL-a!

<TAB> kružit će kroz parametre. <Ctrl + пробел> prikazat će sve dostupne opcije.

Osim toga, budući da sada imamo bash automatsko dovršavanje, možete automatski dovršavati Linux staze izravno u PowerShellu!

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

U slučajevima kada bash autocompletion ne daje nikakve rezultate, PowerShell se vraća na zadane Windows staze sustava. Dakle, u praksi možete istovremeno koristiti oba puta prema vlastitom nahođenju.

Zaključak

Koristeći PowerShell i WSL, možemo integrirati Linux naredbe u Windows kao izvorne aplikacije. Nema potrebe tražiti Win32 međugradnje ili Linux uslužne programe ili prekidati tijek rada odlaskom na Linux ljusku. Samo instalirajte WSL, konfigurirati PowerShell profil и popis naredbi koje želite uvesti! Bogato automatsko dovršavanje za Linux i Windows parametre naredbi i staze datoteka je funkcionalnost koja danas nije dostupna ni u izvornim Windows naredbama.

Cjeloviti izvorni kod opisan gore, kao i dodatne smjernice za njegovu ugradnju u vaš tijek rada, dostupni su здесь.

Koje Linux naredbe smatrate najkorisnijima? Koje druge uobičajene stvari nedostaju pri radu u sustavu Windows? Pišite u komentarima ili na GitHubu!

Izvor: www.habr.com

Dodajte komentar