Integracija ukazov Linux v Windows z uporabo PowerShell in WSL

Tipično vprašanje razvijalcev sistema Windows: »Zakaj še vedno ni <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Ne glede na to, ali gre za močan zamah less ali poznana orodja grep ali sed, razvijalci sistema Windows želijo enostaven dostop do teh ukazov pri vsakodnevnem delu.

Podsistem Windows za Linux (WSL) je v tem pogledu naredil velik korak naprej. Omogoča vam klicanje ukazov Linuxa iz sistema Windows tako, da jih prek posrednika wsl.exe (npr. wsl ls). Čeprav je to precejšnja izboljšava, ima ta možnost številne pomanjkljivosti.

  • Vseprisoten dodatek wsl dolgočasno in nenaravno.
  • Poti sistema Windows v argumentih ne delujejo vedno, ker se poševnice nazaj razlagajo kot ubežni znaki in ne kot ločila imenikov.
  • Poti Windows v argumentih niso prevedene v ustrezno točko namestitve v WSL.
  • Privzete nastavitve se ne upoštevajo v profilih WSL z vzdevki in spremenljivkami okolja.
  • Dokončanje poti v Linuxu ni podprto.
  • Dokončanje ukaza ni podprto.
  • Dokončanje argumentov ni podprto.

Posledično se ukazi Linuxa v sistemu Windows obravnavajo kot drugorazredni državljani – in jih je težje uporabljati kot izvorne ukaze. Za izenačitev njihovih pravic je potrebno rešiti naštete probleme.

Ovoji funkcij PowerShell

Z ovoji funkcij PowerShell lahko dodamo dokončanje ukazov in odpravimo potrebo po predponah wsl, prevajanje poti Windows v poti WSL. Osnovne zahteve za lupine:

  • Za vsak ukaz Linux mora obstajati ena ovojnica funkcije z istim imenom.
  • Lupina mora prepoznati poti sistema Windows, posredovane kot argumente, in jih pretvoriti v poti WSL.
  • Lupina bi morala poklicati wsl z ustreznim ukazom Linux v kateri koli vnos cevovoda in posredovanje vseh argumentov ukazne vrstice, posredovanih funkciji.

Ker je ta vzorec mogoče uporabiti za kateri koli ukaz, lahko abstrahiramo definicijo teh ovojev in jih dinamično ustvarimo iz seznama ukazov 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 ' ')
    }
}
"@
}

Seznam $command definira ukaze za uvoz. Nato z ukazom dinamično ustvarimo ovoj funkcije za vsakega od njih Invoke-Expression (tako da najprej odstranite vse vzdevke, ki bi bili v nasprotju s funkcijo).

Funkcija ponavlja argumente ukazne vrstice, določa poti sistema Windows z uporabo ukazov Split-Path и Test-Pathin nato te poti pretvori v poti WSL. Poti vodimo prek pomožne funkcije Format-WslArgument, ki jih bomo definirali kasneje. Izogne ​​se posebnim znakom, kot so presledki in oklepaji, ki bi jih sicer napačno razlagali.

Končno sporočamo wsl vnos cevovoda in vse argumente ukazne vrstice.

S temi ovoji lahko pokličete svoje najljubše ukaze Linuxa na bolj naraven način brez dodajanja predpone wsl in brez skrbi, kako se poti pretvorijo:

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

Tukaj je prikazan osnovni nabor ukazov, vendar lahko ustvarite lupino za kateri koli ukaz Linux tako, da ga preprosto dodate na seznam. Če to kodo dodate svoji profil PowerShell, ti ukazi vam bodo na voljo v vsaki seji PowerShell, tako kot izvorni ukazi!

Privzete nastavitve

V Linuxu je običajno definiranje vzdevkov in/ali okoljskih spremenljivk v prijavnih profilih, nastavitev privzetih parametrov za pogosto uporabljene ukaze (npr. alias ls=ls -AFh ali export LESS=-i). Ena od pomanjkljivosti proxyja prek neinteraktivne lupine wsl.exe - da profili niso naloženi, zato te možnosti privzeto niso na voljo (tj. ls v WSL in wsl ls se bo z zgoraj definiranim vzdevkom obnašal drugače).

PowerShell zagotavlja $PSDefaultParameterValues, standardni mehanizem za definiranje privzetih parametrov, vendar samo za cmdlete in napredne funkcije. Seveda lahko naredimo napredne funkcije iz naših lupin, vendar to povzroči nepotrebne zaplete (na primer, PowerShell povezuje imena delnih parametrov (npr. -a korelira z -ArgumentList), kar bo v nasprotju z ukazi Linuxa, ki kot argumente sprejemajo delna imena), sintaksa za definiranje privzetih vrednosti pa ne bo najprimernejša (definiranje privzetih argumentov zahteva ime parametra v ključu, ne le imena ukaza).

Vendar pa lahko z rahlo spremembo naših lupin implementiramo podoben model $PSDefaultParameterValuesin omogočite privzete možnosti za ukaze 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 ' ')
    }
}

Prehajanje $WslDefaultParameterValues v ukazno vrstico, pošljemo parametre preko wsl.exe. V nadaljevanju je prikazano, kako dodate navodila svojemu profilu PowerShell za konfiguracijo privzetih nastavitev. Zdaj zmoremo!

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

Ker so parametri modelirani po $PSDefaultParameterValueslahko preprosto jih je onemogočiti začasno z namestitvijo ključa "Disabled" v pomen $true. Dodatna prednost ločene zgoščene tabele je možnost onemogočanja $WslDefaultParameterValues ločeno od $PSDefaultParameterValues.

Dokončanje argumenta

PowerShell vam omogoča, da z ukazom registrirate napovednike argumentov Register-ArgumentCompleter. Bash ima močan programabilna orodja za samodejno dokončanje. WSL vam omogoča klic bash iz lupine PowerShell. Če lahko registriramo zaključke argumentov za naše ovijalke funkcij PowerShell in pokličemo bash za generiranje zaključkov, dobimo popoln zaključek argumentov z enako natančnostjo kot 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]
    }
}

Koda je nekoliko gosta, ne da bi razumeli nekatere bashove notranje funkcije, a v bistvu počnemo naslednje:

  • Registracija dopolnjevalnika argumentov za vse naše ovoje funkcij s posredovanjem seznama $commands v parametru -CommandName za Register-ArgumentCompleter.
  • Vsak ukaz preslikamo v lupinsko funkcijo, ki jo bash uporablja za samodokončanje (za definiranje specifikacij samodokončanja bash uporablja $F, okrajšava za complete -F <FUNCTION>).
  • Pretvarjanje argumentov PowerShell $wordToComplete, $commandAst и $cursorPosition v obliko, ki jo pričakujejo bashove funkcije samodokončanja v skladu s specifikacijami programabilno samodejno dokončanje bash.
  • Sestavimo ukazno vrstico za prenos wsl.exe, ki zagotavlja, da je okolje pravilno nastavljeno, pokliče ustrezno funkcijo samodokončanja in izpiše rezultate v vrsticah.
  • Potem pokličemo wsl z ukazno vrstico ločimo izpis z ločili vrstic in generiramo za vsako CompletionResults, jih razvršča in uhaja znakom, kot so presledki in oklepaji, ki bi jih sicer napačno razlagali.

Posledično bodo naše ukazne lupine Linux uporabljale popolnoma enako samodokončanje kot bash! Na primer:

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

Vsako samodokončanje dobavi vrednosti, specifične za prejšnji argument, pri čemer bere konfiguracijske podatke, kot so znani gostitelji iz WSL!

<TAB> bo krožil skozi parametre. <Ctrl + пробел> prikaže vse razpoložljive možnosti.

Poleg tega, ker imamo zdaj samodejno dokončanje bash, lahko samodejno dokončate poti Linuxa neposredno v lupini PowerShell!

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

V primerih, ko samodokončanje bash ne prinese nobenih rezultatov, se PowerShell vrne na sistemske privzete poti Windows. Tako lahko v praksi po lastni presoji uporabljate obe poti hkrati.

Zaključek

Z uporabo PowerShell in WSL lahko ukaze Linuxa integriramo v Windows kot izvorne aplikacije. Ni vam treba iskati gradenj Win32 ali pripomočkov za Linux ali prekiniti delovnega toka z uporabo lupine Linux. Samo namestite WSL, konfiguriraj Profil PowerShell и seznam ukazov, ki jih želite uvoziti! Bogato samodokončanje za ukazne parametre in poti datotek v sistemu Linux in Windows je funkcionalnost, ki danes ni na voljo niti v izvornih ukazih sistema Windows.

Na voljo je celotna izvorna koda, opisana zgoraj, in dodatne smernice za njeno vključitev v potek dela tukaj.

Kateri ukazi Linuxa se vam zdijo najbolj uporabni? Katere druge običajne stvari manjkajo pri delu v sistemu Windows? Zapišite v komentar oz na GitHubu!

Vir: www.habr.com

Dodaj komentar