Integrácia príkazov systému Linux do systému Windows pomocou prostredia PowerShell a WSL

Typická otázka vývojárov systému Windows: „Prečo stále neexistuje <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Či už ide o silný ťah less alebo známe nástroje grep alebo sed, vývojári Windows chcú pri svojej každodennej práci ľahký prístup k týmto príkazom.

Windows Subsystém pre Linux (WSL) urobila v tomto smere obrovský krok vpred. Umožňuje vám volať príkazy Linuxu zo systému Windows ich prostredníctvom proxy wsl.exe (Napr wsl ls). Hoci ide o výrazné zlepšenie, táto možnosť trpí množstvom nevýhod.

  • Všadeprítomný prídavok wsl únavné a neprirodzené.
  • Cesty systému Windows v argumentoch nie vždy fungujú, pretože spätné lomky sa interpretujú ako znaky escape a nie ako oddeľovače adresárov.
  • Cesty systému Windows v argumentoch sa neprekladajú do zodpovedajúceho bodu pripojenia vo WSL.
  • V profiloch WSL s aliasmi a premennými prostredia sa nerešpektujú predvolené nastavenia.
  • Dokončenie cesty systému Linux nie je podporované.
  • Dokončenie príkazu nie je podporované.
  • Dokončenie argumentu nie je podporované.

Výsledkom je, že s príkazmi Linuxu sa v systéme Windows zaobchádza ako s občanmi druhej kategórie – a ich používanie je náročnejšie ako natívne príkazy. Na zrovnoprávnenie ich práv je potrebné riešiť uvedené problémy.

Obal funkcií PowerShell

Pomocou obalov funkcií PowerShell môžeme pridať dokončenie príkazov a eliminovať potrebu prefixov wsl, ktorý prekladá cesty systému Windows na cesty WSL. Základné požiadavky na škrupiny:

  • Pre každý príkaz Linuxu musí existovať jeden obal funkcie s rovnakým názvom.
  • Shell musí rozpoznať cesty Windows odovzdané ako argumenty a skonvertovať ich na cesty WSL.
  • Škrupina by mala zavolať wsl s príslušným linuxovým príkazom na akýkoľvek vstup potrubia a odovzdaním akýchkoľvek argumentov príkazového riadka odovzdaných funkcii.

Keďže tento vzor možno použiť na 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 definuje importné príkazy. Potom pomocou príkazu dynamicky vygenerujeme obal funkcií pre každý z nich Invoke-Expression (najprv odstránením všetkých aliasov, ktoré by boli v konflikte s funkciou).

Funkcia iteruje cez argumenty príkazového riadku, určuje cesty Windows pomocou príkazov Split-Path и Test-Patha potom tieto cesty skonvertuje na cesty WSL. Cesty vedieme cez pomocnú funkciu Format-WslArgument, ktorý si zadefinujeme neskôr. Vynecháva špeciálne znaky, ako sú medzery a zátvorky, ktoré by inak boli nesprávne interpretované.

Nakoniec prenesieme wsl potrubný vstup a všetky argumenty príkazového riadku.

S týmito obalmi môžete volať svoje obľúbené príkazy Linuxu prirodzenejším spôsobom bez pridania predpony wsl a bez obáv o to, ako sa cesty prevedú:

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

Základná sada príkazov je zobrazená tu, ale môžete vytvoriť shell pre akýkoľvek príkaz Linux jednoduchým pridaním do zoznamu. Ak pridáte tento kód do svojho profil PowerShell, tieto príkazy budete mať k dispozícii v každej relácii PowerShell, rovnako ako natívne príkazy!

Predvolené nastavenia

V Linuxe je bežné definovať aliasy a/alebo premenné prostredia v prihlasovacích profiloch, pričom sa nastavujú predvolené parametre pre často používané príkazy (napr. alias ls=ls -AFh alebo export LESS=-i). Jedna z nevýhod proxyovania cez neinteraktívny shell wsl.exe - že profily nie sú načítané, takže tieto možnosti nie sú štandardne dostupné (t.j. ls vo WSL a wsl ls sa bude správať inak s aliasom definovaným vyššie).

PowerShell poskytuje $PSDefaultParameterValues, štandardný mechanizmus na definovanie predvolených parametrov, ale len pre cmdlety a pokročilé funkcie. Samozrejme, môžeme z našich shellov urobiť pokročilé funkcie, ale to prináša zbytočné komplikácie (napríklad PowerShell koreluje názvy čiastkových parametrov (napr. -a koreluje s -ArgumentList), čo bude v konflikte s príkazmi Linuxu, ktoré berú ako argumenty čiastočné názvy) a syntax na definovanie predvolených nastavení nebude najvhodnejšia (definovanie predvolených argumentov vyžaduje názov parametra v kľúči, nielen názov príkazu).

S miernou úpravou našich mušlí však môžeme implementovať model podobný ako $PSDefaultParameterValuesa povoľte predvolené možnosti pre príkazy systému 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 ' ')
    }
}

Pasovanie $WslDefaultParameterValues do príkazového riadku posielame parametre cez wsl.exe. Nasledujúci text ukazuje, ako pridať pokyny do profilu PowerShell na konfiguráciu predvolených nastavení. Teraz to dokážeme!

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

Keďže parametre sú modelované po $PSDefaultParameterValuesmôžete je ľahké ich deaktivovať dočasne inštaláciou kľúča "Disabled" do významu $true. Ďalšou výhodou samostatnej hašovacej tabuľky je možnosť deaktivácie $WslDefaultParameterValues oddelene od $PSDefaultParameterValues.

Dokončenie argumentácie

PowerShell vám umožňuje zaregistrovať upútavky argumentov pomocou príkazu Register-ArgumentCompleter. Bash má moc programovateľné nástroje automatického dokončovania. WSL vám umožňuje volať bash z PowerShell. Ak dokážeme zaregistrovať dokončenia argumentov pre naše obaly funkcií PowerShell a zavolať bash na generovanie dokončení, získame úplné dokončenie argumentov s rovnakou presnosťou ako samotný 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í bashu, ale v podstate robíme toto:

  • Registrácia dokončovača argumentov pre všetky naše funkcie wrapper odovzdaním zoznamu $commands v parametri -CommandName pre Register-ArgumentCompleter.
  • Každý príkaz mapujeme na funkciu shell, ktorú bash používa na automatické dopĺňanie (na definovanie špecifikácií automatického dopĺňania používa bash $F, skratka pre complete -F <FUNCTION>).
  • Konverzia argumentov PowerShell $wordToComplete, $commandAst и $cursorPosition do formátu očakávaného funkciami automatického dopĺňania bash podľa špecifikácií programovateľné automatické dokončovanie bash.
  • Vytvoríme príkazový riadok na prenos wsl.exe, ktorý zaisťuje správne nastavenie prostredia, zavolá príslušnú funkciu automatického dokončovania a výsledky vypíše riadok po riadku.
  • Potom zavoláme wsl s príkazovým riadkom oddeľujeme výstup oddeľovačmi riadkov a generujeme pre každý CompletionResults, ich triedenie a escapovanie znakov, ako sú medzery a zátvorky, ktoré by inak boli nesprávne interpretované.

Výsledkom je, že naše príkazové shelly pre Linux budú používať presne rovnaké automatické dopĺňanie ako bash! Napríklad:

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

Každé automatické dopĺňanie dodáva hodnoty špecifické pre predchádzajúci argument, čítajúc konfiguračné údaje, ako sú napríklad známi hostitelia z WSL!

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

Navyše, keďže teraz máme automatické dopĺňanie bash, cesty systému Linux môžete automaticky dopĺňať priamo v prostredí PowerShell!

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

V prípadoch, keď automatické dokončovanie bash neprinesie žiadne výsledky, PowerShell sa vráti na predvolené cesty systému Windows. V praxi tak môžete súčasne využívať obe cesty podľa vlastného uváženia.

Záver

Pomocou PowerShell a WSL môžeme integrovať príkazy Linuxu do Windowsu ako natívne aplikácie. Nie je potrebné hľadať zostavy Win32 alebo linuxové pomôcky alebo prerušovať pracovný postup prechodom do prostredia Linuxu. Len nainštalovať WSL, nakonfigurovať Profil PowerShell и zoznam príkazov, ktoré chcete importovať! Bohaté automatické dopĺňanie parametrov príkazov a ciest k súborom pre systémy Linux a Windows je funkcia, ktorá dnes nie je dostupná ani v natívnych príkazoch systému Windows.

K dispozícii je úplný zdrojový kód popísaný vyššie, ako aj ďalšie pokyny na jeho začlenenie do vášho pracovného postupu tu.

Ktoré príkazy systému Linux považujete za najužitočnejšie? Aké ďalšie bežné veci chýbajú pri práci v systéme Windows? Napíšte do komentárov resp na GitHub!

Zdroj: hab.com

Pridať komentár