Integracija Linux komandi u Windows koristeći PowerShell i WSL

Tipično pitanje Windows programera: „Zašto još nema <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Bilo da se radi o snažnom prevlačenju less ili poznatim alatima grep ili sed, Windows programeri žele lak pristup ovim komandama u svom svakodnevnom radu.

Windows podsistem za Linux (WSL) je napravio veliki iskorak u tom pogledu. Omogućava vam da pozivate Linux komande iz Windowsa tako što ćete ih proxy koristiti wsl.exe (npr. wsl ls). Iako je ovo značajno poboljšanje, ova opcija pati od niza nedostataka.

  • Sveprisutni dodatak wsl zamorno i neprirodno.
  • Windows putanje u argumentima ne funkcionišu uvijek jer se obrnute kose crte tumače kao izlazni znakovi, a ne kao separatori direktorija.
  • Windows putanje u argumentima se ne prevode na odgovarajuću tačku montiranja u WSL-u.
  • Zadane postavke se ne poštuju u WSL profilima sa pseudonima i varijablama okruženja.
  • Dovršavanje Linux putanje nije podržano.
  • Dovršavanje naredbe nije podržano.
  • Dovršavanje argumenta nije podržano.

Kao rezultat toga, Linux komande se tretiraju kao građani drugog reda pod Windowsom—i teže su za korištenje od izvornih komandi. Za izjednačavanje njihovih prava potrebno je riješiti navedene probleme.

Omotači funkcija PowerShell

Uz PowerShell funkcije omotača, možemo dodati dovršavanje naredbi i eliminirati potrebu za prefiksima wsl, prevodeći Windows putanje u WSL putanje. Osnovni zahtjevi za školjke:

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

Pošto se ovaj obrazac može primijeniti na bilo koju naredbu, možemo apstrahovati definiciju ovih omotača i dinamički ih generirati iz liste 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 ' ')
    }
}
"@
}

lista $command definira komande za uvoz. Zatim dinamički generišemo omotač funkcije za svaku od njih koristeći naredbu Invoke-Expression (prvo uklanjanjem svih alijasa koji bi bili u sukobu s funkcijom).

Funkcija ponavlja argumente komandne linije, određuje Windows putanje pomoću naredbi Split-Path и Test-Patha zatim pretvara ove staze u WSL staze. Puteve pokrećemo kroz pomoćnu funkciju Format-WslArgument, koje ćemo kasnije definisati. Izbjegava posebne znakove kao što su razmaci i zagrade koji bi inače bili pogrešno protumačeni.

Konačno, prenosimo wsl unos cjevovoda i bilo koji argument komandne linije.

Sa ovim omotima možete pozivati ​​svoje omiljene Linux komande na prirodniji način bez dodavanja prefiksa wsl i bez brige o tome kako se putanje 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 kreirati ljusku za bilo koju Linux naredbu jednostavnim dodavanjem na listu. Ako dodate ovaj kod u svoj profil PowerShell, ove komande će vam biti dostupne u svakoj PowerShell sesiji, baš kao i izvorne komande!

Zadani parametri

U Linuxu je uobičajeno definirati pseudonime i/ili varijable okruženja u profilima za prijavu, postavljajući zadane parametre za često korištene komande (na primjer, alias ls=ls -AFh ili export LESS=-i). Jedan od nedostataka proxyja kroz neinteraktivnu ljusku wsl.exe - da profili nisu učitani, tako da ove opcije nisu standardno dostupne (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 dovodi do nepotrebnih komplikacija (na primjer, PowerShell korelira djelomične nazive parametara (na primjer, -a korelira sa -ArgumentList), što će biti u sukobu s Linux naredbama koje uzimaju djelomična imena kao argumente), a sintaksa za definiranje zadanih vrijednosti neće biti najprikladnija (definiranje default argumenata zahtijeva ime parametra u ključu, a ne samo ime komande).

Međutim, uz malu modifikaciju naših školjki, možemo implementirati sličan model $PSDefaultParameterValuesi omogući podrazumevane opcije za Linux komande!

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

Prenosom $WslDefaultParameterValues u komandnu liniju, šaljemo parametre preko wsl.exe. U nastavku je prikazano kako da dodate uputstva svom PowerShell profilu da konfigurišete podrazumevane postavke. Sada to možemo!

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

Budući da su parametri modelirani po $PSDefaultParameterValues, Možeš lako ih je onemogućiti privremeno instaliranjem ključa "Disabled" u značenje $true. Dodatna prednost zasebne hash tablice je mogućnost onemogućavanja $WslDefaultParameterValues odvojeno od $PSDefaultParameterValues.

Završetak argumenta

PowerShell vam omogućava da registrujete najavu argumenta pomoću naredbe Register-ArgumentCompleter. Bash ima moćan programabilni alati za automatsko dovršavanje. WSL vam omogućava da pozovete bash iz PowerShell-a. Ako možemo registrovati dovršavanje argumenata za naše omote PowerShell funkcije i pozvati bash za generiranje dovršetka, dobićemo potpuni završetak argumenta sa 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 malo gust bez razumijevanja nekih od bash internih funkcija, ali u osnovi ono što radimo je ovo:

  • Registriranje dovršivača argumenata za sve naše omote funkcija prosljeđivanjem liste $commands u parametru -CommandName do Register-ArgumentCompleter.
  • Svaku naredbu mapiramo u funkciju ljuske koju bash koristi za automatsko dovršavanje (da bi definirao specifikacije 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 autodovršavanja prema specifikacijama programabilno automatsko dovršavanje bash.
  • Sastavljamo komandnu liniju u koju ćemo prebaciti wsl.exe, koji osigurava da je okruženje ispravno postavljeno, poziva odgovarajuću funkciju automatskog dovršavanja i daje rezultate red po red.
  • Onda zovemo wsl sa komandnom linijom, odvajamo izlaz po separatorima redova i generišemo za svaki CompletionResults, sortirajući ih i izbjegavajući znakove kao što su razmaci i zagrade koji bi inače bili pogrešno protumačeni.

Kao rezultat toga, naše Linux komandne ljuske će koristiti potpuno isto autodovrš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 daje vrijednosti specifične za prethodni argument, čitajući konfiguracijske podatke kao što su poznati hostovi iz WSL-a!

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

Osim toga, pošto sada imamo bash automatsko dovršavanje, možete automatski dovršavati Linux staze direktno u PowerShell-u!

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

U slučajevima kada bash automatsko dovršavanje ne daje nikakve rezultate, PowerShell se vraća na podrazumevane Windows putanje. Dakle, u praksi možete istovremeno koristiti oba puta po svom nahođenju.

zaključak

Koristeći PowerShell i WSL, možemo integrirati Linux komande u Windows kao izvorne aplikacije. Nema potrebe da tražite Win32 verzije ili Linux uslužne programe ili da prekidate tok posla odlaskom na Linux ljusku. Samo instalirajte WSL, konfiguriraj PowerShell profil и navedite komande koje želite da uvezete! Bogato automatsko dovršavanje za Linux i Windows parametre naredbi i putanje datoteka je funkcionalnost koja danas nije dostupna ni u izvornim Windows komandama.

Cijeli izvorni kod opisan gore, kao i dodatne smjernice za njegovo uključivanje u vaš radni tok, je dostupan ovdje.

Koje Linux komande smatrate najkorisnijim? Koje druge uobičajene stvari nedostaju pri radu u Windows-u? Pišite u komentarima ili na GitHubu!

izvor: www.habr.com

Dodajte komentar