Integriramo Linux ukaze u Windows uz pomoć PowerShell-a i WSL-a

Tipično pitanje programera na Windows-u: "Zašto ovde još uvek nema <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?». Будь то мощное пролистывание less ili poznati alati grep ili sed, programeri na Windows-u žele lak pristup ovim komandama u svakodnevnom radu.

Windows Subsystem for Linux (WSL) napravio je veliki korak napred u tom smislu. Omogućava pozivanje Linux komandi iz Windows-a, proksirajući ih kroz wsl.exe (na primer, wsl ls). Iako je ovo značajno poboljšanje, ovaj pristup pati od niza nedostataka.

  • Sveprisutna dodavanja wsl umara i deluje neprirodno.
  • Windows putanje u argumentima ne rade uvek, jer se nazadni kosa crta tumači kao escape simbol, a ne kao separator direktorijuma.
  • Windows putanje u argumentima se ne prevode u odgovarajuću tačku montiranja u WSL-u.
  • Podrazumevani parametri u WSL profilima sa aliasima i varijablama okruženja se ne uzimaju u obzir.
  • Završavanje Linux putanja nije podržano.
  • Završavanje komandi nije podržano.
  • Završavanje argumenata nije podržano.

Kao rezultat, Linux komande se pod Windows-om smatraju građanima drugog reda — i teže ih je koristiti nego domaće komande. Da bismo ih izjednačili, potrebno je rešiti nabrojane probleme.

PowerShell funkcijske omotnice

Pomoću PowerShell funkcijskih omotnica možemo dodati automatsko završavanje komandi i eliminisati potrebu za prefiksima wsl, prevodeći Windows putanje u WSL putanje. Glavni zahtevi za omotnice su:

  • Za svaku Linux komandu treba da postoji jedna funkcijska omotnica sa istim imenom.
  • Omotnica mora prepoznati Windows putanje prosleđene kao argumente i pretvoriti ih u WSL putanje.
  • Omotnica mora pozvati wsl odgovarajuću Linux komandu na bilo koji ulaz u cevovod i preneti sve argumente komandne linije prosleđene funkciji.

Budući da se ovaj obrazac može primeniti na svaku komandu, možemo apstrahovati definiciju ovih omotnica i dinamički ih generisati iz liste komandi 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 definiše komande za uvoz. Zatim dinamički generišemo omotnicu funkcije za svaku od njih, koristeći komandu Invoke-Expression (prvo uklanjajući sve alias-e koji bi mogli doći u sukob sa funkcijom).

Funkcija prolazi kroz argumente komandne linije, identifikuje Windows putanje pomoću komandi Split-Path in Test-Path, in nato pretvori te poti v poti WSL. Poti predelamo skozi pomožno funkcijo Format-WslArgument, ki jo bomo definirali pozneje. Ubeži posebne znake, kot so presledki in oklepaji, ki bi bili sicer napačno interpretirani.

Na koncu predamo wsl vhod kontejnerju in vse argumente ukazne vrstice.

S takšnimi ovoji lahko kličemo svoje najljubše ukaze Linux na bolj naraven način, brez predpona wsl in brez skrbi, kako se poti pretvarjajo:

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

Tukaj je osnovni niz ukazov, vendar lahko ustvarite ovojnico za kateri koli ukaz Linux, tako da ga preprosto dodate na seznam. Če dodate to kodo v svoj profil PowerShell, bodo ti ukazi na voljo v vsakem seji PowerShell, tako kot nativni ukazi!

Privzete nastavitve

V Linuxu je običajno določiti alias in/ali spremenljivke okolja v profilih (login profile), da se nastavijo privzete nastavitve za pogosto uporabljene ukaze (na primer, alias ls=ls -AFh ili export LESS=-i). Ena od pomanjkljivosti posredovanja preko neinteraktivne lupine wsl.exe je, da se profili ne naložijo, zato ti privzeti parametri niso na voljo (tj. ls v WSL in wsl ls se bodo obnašali različno z zgoraj določenim aliasom.

PowerShell ponuja $PSDefaultParameterValues, standardni mehanizem za določanje privzetih parametrov, vendar le za cmdlete in razširjene funkcije. Seveda lahko naše lupine pretvorimo v razširjene funkcije, vendar to prinaša dodatne zaplete (torej PowerShell obravnava delna imena parametrov (na primer, -a se povezuje z -ArgumentList), ki bodo v konflikti z ukazi Linux, ki sprejemajo delna imena kot argumente), in sintaksa za določitev privzetih vrednosti ne bo najbolj primerna (za določitev argumentov po privzetku je potrebno ime parametra v ključu in ne le ime ukaza).

Vendar pa z majhno spremembo naših lupin lahko uvedemo model podoben $PSDefaultParameterValues, in vključimo privzete parametre 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 ' ')
    }
}

S predajo $WslDefaultParameterValues v ukazatelja, pošiljamo parametre preko wsl.exe. Ispod je prikazano kako dodati uputstva u PowerShell profil za podešavanje podrazumevanih parametara. Sada to možemo da uradimo!

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

Budući da su parametri modelirani prema $PSDefaultParameterValues, možete lako ih isključiti privremeno, postavljanjem ključa "Disabled" na vrednost $true. Dodatna prednost odvojene hesh tabele je mogućnost da isključite $WslDefaultParameterValues odvojeno od $PSDefaultParameterValues.

Automatsko završavanje argumenata

PowerShell omogućava registrovanje završioca argumenata koristeći komandu Register-ArgumentCompleter. U Bash-u postoje moćni programabilni alati za automatsko završavanje. WSL omogućava pozivanje bash-a iz PowerShell-a. Ako možemo da registrujemo završioca argumenata za naše funkcije u PowerShell-u i pozovemo bash za generisanje završetaka, dobićemo potpuno automatsko završavanje argumenata sa istom preciznošću kao u samom bash-u!

# 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 razumevanja nekih internih funkcija bash-a, ali uglavnom radimo sledeće:

  • Registrovamo završioca argumenata za sve naše omotane funkcije, prosleđujući spisak $commands v parameter -CommandName za Register-ArgumentCompleter.
  • Mapiramo svaku komandu na funkciju omotanja koju bash koristi za automatsko završavanje (za određivanje specifikacija automatskog završavanja u bash-u koristi se $F, skraćenica od complete -F).
  • Transformišemo argumente PowerShell-a $wordToComplete, $commandAst in $cursorPosition u format koji očekuju funkcije automatskog završavanja bash-a prema specifikacijama programabilnog automatskog završavanja bash-a.
  • Sastavljamo komandnu liniju za prosledjivanje u wsl.exe, koja obezbeđuje pravilnu konfiguraciju okruženja, poziva odgovarajuću funkciju automatskog završavanja i vraća rezultate razdvojene po redovima.
  • Zatim pozivamo wsl sa komandnom linijom, razdvajamo izlaz separatorima linija i generišemo za svaku CompletionResults, sortirajući ih i escapirajući karaktere kao što su razmaci i zagrade, koji bi inače bili pogrešno interpretirani.

Na kraju, naše Linux komandne linije će koristiti potpuno isto automatsko završavanje kao u bash-u! Na primer:

  • ssh -c -J -m -O -o -Q -w -b

Vsaka avtoizpolnitev ponuja vrednosti, specifične za prejšnji argument, tako da prebere nastavitve, kot so znani gostitelji, iz WSL!

<TAB> bo ciklično prebiral parametre. <Ctrl + пробел> bo prikazal vse razpoložljive možnosti.

Poleg tega, ker zdaj deluje avtoizpolnitev bash, lahko avtoizpolnjujete poti Linux neposredno v PowerShell!

  • less /etc/
  • ls /usr/share/
  • vim ~/bash

V primerih, ko avtoizpolnitev bash ne vrne rezultatov, se PowerShell vrne na privzeti sistem s potmi Windows. Tako lahko v praksi hkrati uporabljate tako ene kot druge poti po svoji izbiri.

Zaključek

S pomočjo PowerShell in WSL lahko integriramo ukaze Linux v Windows kot domače aplikacije. Ni treba iskati različnih različic Win32 ali Linux orodij ali prekinjati delovnega procesa, ko preklapljate na Linux shell. Preprosto namestite WSL, nastavite profil PowerShell in naštejte ukaze, ki jih želite uvoziti! Bogato avtoizpolnjevanje za parametre ukazov in poti datotek Linux in Windows je funkcionalnost, ki je danes celo manjkajoča v domačih ukazih Windows.

Celotna izvirna koda, opisana zgoraj, ter dodatni nasveti za vključitev v delovni proces so na voljo ovde.

Katere ukaze Linux menite, da so najbolj uporabni? Kaj še pogrešate pri delu v Windows? Pišite v komentarje ali na GitHubu!

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster