Integrujeme príkazy Linuxu v systéme Windows pomocou PowerShell a WSL

Typická otázka vývojárov na Windows: „Prečo tu stále nie je <VLOŽTE SEM SVOJ OBLÚBENÝ PRÍKAZ LINUX>?». Будь то мощное пролистывание menej alebo zvyčajné nástroje grep alebo sed, vývojári na Windows chcú mať jednoduchý prístup k týmto príkazom vo svojej každodennej práci.

Windows Subsystem for Linux (WSL) urobila obrovský pokrok v tomto smere. Umožňuje spúšťať príkazy Linuxu z Windows tým, že ich proxuje cez wsl.exe (napríklad, wsl ls). Aj keď je to významné zlepšenie, tento spôsob trpí množstvom nedostatkov.

  • Zavedenie wsl je únavné a neprirodzené.
  • Cesty Windows v argumentoch sa ne vždy spracovávajú správne, pretože spätné lomítka sú interpretované ako escape znaky, a nie ako oddeľovače adresárov.
  • Cesty Windows v argumentoch sa neprekladajú na zodpovedajúci bod pripojenia v WSL.
  • Predvolené parametre v profiloch WSL s aliasmi a premennými prostredia sa nezohľadňujú.
  • Nepodporuje sa autokompletácia ciest Linux.
  • Nepodporuje sa autokompletácia príkazov.
  • Nepodporuje sa autokompletácia argumentov.

V dôsledku toho sa príkazy Linuxu považujú na Windows za druhotriedne — a ich používanie je zložitejšie ako u natívnych príkazov. Aby sme im poskytli rovnaké práva, musíme riešiť uvedené problémy.

Funkčné obaly PowerShell

Pomocou funkčných obalov PowerShell môžeme pridať autokompletáciu príkazov a odstrániť potrebu prefixov wsl, prevodom ciest Windows na cesty WSL. Hlavné požiadavky na obaly sú:

  • Pre každý príkaz Linux by mala existovať jedna funkcia s rovnakým názvom.
  • Funkcia by mala rozpoznať cesty Windows predané ako argumenty a previesť ich na cesty WSL.
  • Funkcia by mala volať wsl s príslušným príkazom Linux na akýkoľvek vstup potrubia a posielať akékoľvek argumenty príkazového riadku dodané funkcii.

Pretože tento vzor môže byť použitý pre akýkoľvek príkaz, môžeme abstrahovať definíciu týchto obalov a dynamicky ich generovať zo zoznamu príkazov na 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 ' ')
    }
}
"@
}

Zoznam $command určuje príkazy na import. Potom dynamicky generujeme obálku funkcie pre každý z nich pomocou príkazu Invoke-Expression (najprv odstránime akékoľvek aliasy, ktoré by mohli konfliktovať s funkciou).

Funkcia prechádza argumentmi príkazového riadku, rozpoznáva cesty Windows pomocou príkazov Split-Path a Test-Path, a potom tieto cesty prevedie na WSL cesty. Spúšťame cesty cez pomocnú funkciu Format-WslArgument, ktorú definujeme neskôr. Zabezpečuje únik špeciálnych znakov, ako sú medzery a zátvorky, ktoré by inak boli nesprávne interpretované.

Nakoniec odovzdávame wsl vstupy potrubia a akékoľvek argumenty príkazového riadku.

Pomocou takýchto obalov môžete volat svoje obľúbené príkazy Linuxu prirodzenejším spôsobom, bez pridávania prefixu wsl a bez obáv o to, ako sa cesty prevádzajú:

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

Tu je základný súbor príkazov, ale môžete vytvoriť obal pre akýkoľvek príkaz Linuxu jednoducho tým, že ho pridáte do zoznamu. Ak tento kód pridáte do svojho profily PowerShell, tieto príkazy budú pre vás dostupné v každom sedení PowerShellu, rovnako ako natívne príkazy!

Predvolené parametre

V Linuxe je zvykom definovať aliasy a/alebo premenné prostredia v profiloch (login profile), čím sa nastavujú predvolené parametre pre často používané príkazy (napríklad alias ls=ls -AFh alebo export LESS=-i). Jednou z nevýhod proxy cez neinteraktívnu shell je wsl.exe to, že profily sa nenačítavajú, takže tieto predvolené parametre nie sú k dispozícii (t. j. ls v WSL a wsl ls budú sa správať inak s aliasom definovaným vyššie).

PowerShell poskytuje $PSDefaultParameterValues, štandardný mechanizmus na definovanie predvolených parametrov, ale iba pre cmdlet a rozšírené funkcie. Samozrejme, môžeme z našich shellov urobiť rozšírené funkcie, ale to prináša zbytočné komplikácie (tak, PowerShell spája čiastočné názvy parametrov (napríklad -a oboresť -ArgumentList), ktoré budú kolidovať s príkazmi Linuxu, ktoré prijímajú čiastočné názvy ako argumenty), a syntax na definovanie predvolených hodnôt nebude najvhodnejšia (na definovanie argumentov s predvolenými hodnotami je potrebné meno parametra v kľúči, nie len meno príkazu).

Avšak s malou zmenou našich shellov môžeme implementovať model podobný $PSDefaultParameterValues, a zahrnúť predvolené parametre pre príkazy Linuxu!

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

Odovzdávajúc $WslDefaultParameterValues do príkazového riadku posielame parametre cez wsl.exe. Nižšie je znázornené, ako pridať inštrukcie do profilu PowerShell na nastavenie predvolených parametrov. Teraz to môžeme urobiť!

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

Keďže sa parametre modelujú podľa $PSDefaultParameterValues, môžete ľahko ich vypnúť dočasne nastavením kľúča "Disabled" na hodnotu $true. Ďalšou výhodou samostatnej hash tabuľky je schopnosť vypnúť $WslDefaultParameterValues samostatne od $PSDefaultParameterValues.

Doplnenia argumentov

PowerShell umožňuje registrovať dokončovače argumentov pomocou príkazu Register-ArgumentCompleter. V Bash existujú silné programovateľné prostriedky na doplnenie. WSL umožňuje vyvolať bash z PowerShell. Ak môžeme registrovať dokončovače argumentov pre naše shells funkcie PowerShell a zavolať bash na vytvorenie doplnení, dostaneme kompletné doplnenie argumentov s rovnakou presnosťou ako v samotnom 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]
    }
}

Kód je trochu hustý bez pochopenia niektorých vnútorných funkcií bash, ale vo funkcii robíme nasledovné:

  • Registrujeme dokončovač argumentov pre všetky naše obálkové funkcie, pričom odovzdávame zoznam $commands do parametra -CommandName pre Register-ArgumentCompleter.
  • Mapujeme každý príkaz na funkciu shellu, ktorú bash používa na doplnenie (na určenie špecifikácií doplnenia v bash sa používa $F, skratka pre complete -F).
  • Transformujeme argumenty PowerShell $wordToComplete, $commandAst a $cursorPosition do formátu očakávaného funkciami doplnenia bash v súlade so špecifikáciami programovateľného doplnenia bash.
  • Skladáme príkazový riadok na odovzdanie do wsl.exe, ktorý zabezpečuje správne nastavenie prostredia, vyvoláva príslušnú funkciu doplnenia a výstupy výsledkov s rozklady na riadky.
  • Potom voláme wsl s príkazovým riadkom, delíme výstup pomocou oddelovačov riadkov a generujeme pre každé CompletionResults, zoradením ich a escapovaním znakov, ako sú medzery a zátvorky, ktoré by inak boli nesprávne interpretované.

Na konci naše linuxové príkazové shells použijú presne to isté doplnenie, ako v bash! Napríklad:

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

Každé automatické doplnenie poskytuje hodnoty špecifické pre predchádzajúci argument, čítaním konfiguračných údajov, ako sú známe hostiteľské mená, z WSL!

<TAB> bude cyklicky prechádzať parametre. <Ctrl + пробел> zobrazí všetky dostupné možnosti.

Okrem toho, keďže teraz máme funkčné doplnenie bash, môžete autocompletovať cesty Linuxu priamo v PowerShell!

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

V prípadoch, keď doplnenie bash neposkytuje žiadne výsledky, PowerShell sa vracia k predvolenej systémovej ceste Windows. Takže môžete prakticky súčasne používať obidve cesty podľa vlastného uváženia.

Záver

S PowerShell a WSL môžeme integrovať príkazy Linuxu do Windows ako natívne aplikácie. Nie je potrebné hľadať Win32 zostavy alebo nástroje Linuxu alebo prerušovať pracovný tok prepínaním do linuxovej shell. nainštalujte WSL, nastavte profil PowerShell a vymenujte príkazy, ktoré chcete importovať! Bohaté automatické doplnenie pre príkazové parametre a cesty k súborom Linux a Windows - je to funkčnosť, ktorú dnes nemajú dokonca ani natívne príkazy Windows.

Celý zdrojový kód, ktorý bol popísaný vyššie, ako aj dodatočné odporúčania na jeho zahrnutie do pracovného toku sú dostupné tu.

Ktoré príkazy Linuxu považujete za najužitočnejšie? Čo vám ešte chýba pri práci vo Windowse? Napíšte v komentároch alebo na GitHub!

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster