Integrante Linuksajn komandojn en Vindozon uzante PowerShell kaj WSL

Tipa demando de Vindozaj programistoj: "Kial ankoraŭ ne ekzistas <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Ĉu ĝi estas potenca svingo less aŭ konataj iloj grepsed, Vindozaj programistoj volas facilan aliron al ĉi tiuj komandoj en sia ĉiutaga laboro.

Vindoza Subsistemo por Linukso (WSL) faris grandegan paŝon antaŭen tiurilate. Ĝi permesas vin voki Linuksajn komandojn de Vindozo per prokurado de ili wsl.exe (t.e. wsl ls). Kvankam ĉi tio estas grava plibonigo, ĉi tiu opcio suferas kelkajn malavantaĝojn.

  • Ĉiea aldono wsl teda kaj nenatura.
  • Vindozaj vojoj en argumentoj ne ĉiam funkcias ĉar malantaŭa oblikvo estas interpretitaj kiel ellasaj signoj prefere ol dosierujoj.
  • Vindozaj vojoj en argumentoj ne estas tradukitaj al la ekvivalenta munta punkto en WSL.
  • Defaŭltaj agordoj ne estas respektataj en WSL-profiloj kun kaŝnomoj kaj mediovariabloj.
  • Linukso-vojkompletigo ne estas subtenata.
  • Komandkompletigo ne estas subtenata.
  • Argumentkompletigo ne estas subtenata.

Kiel rezulto, Linuksaj komandoj estas traktataj kiel duaklasaj civitanoj sub Vindozo—kaj estas pli malfacile uzeblaj ol denaskaj komandoj. Por egaligi iliajn rajtojn, necesas solvi la listigitajn problemojn.

PowerShell-funkciaj envolvaĵoj

Kun PowerShell-funkciaj envolvaĵoj, ni povas aldoni komandkompletigon kaj forigi la bezonon de prefiksoj wsl, tradukante Vindozajn padojn en WSL-padojn. Bazaj postuloj por konkoj:

  • Por ĉiu Linuksa komando devas esti unu funkcio-envolvilo kun la sama nomo.
  • La ŝelo devas rekoni la Vindozajn vojojn pasigitajn kiel argumentojn kaj konverti ilin al WSL-vojoj.
  • La ŝelo devus voki wsl kun la taŭga Linuksa komando al iu ajn dukto-enigo kaj pasante iujn ajn komandliniajn argumentojn pasigitajn al la funkcio.

Ĉar ĉi tiu ŝablono povas esti aplikita al iu ajn komando, ni povas abstrakti la difinon de ĉi tiuj envolvaĵoj kaj dinamike generi ilin el listo de komandoj por importi.

# 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 ' ')
    }
}
"@
}

Listo de $command difinas importkomandojn. Ni tiam dinamike generas funkcion por ĉiu el ili uzante la komandon Invoke-Expression (unue forigante ajnajn kaŝnomojn kiuj konfliktus kun la funkcio).

La funkcio ripetas super komandliniaj argumentoj, determinas Vindozajn vojojn uzante komandojn Split-Path и Test-Pathkaj tiam konvertas ĉi tiujn vojojn al WSL-vojoj. Ni kuras la vojojn per helpa funkcio Format-WslArgument, kiun ni difinos poste. Ĝi eskapas specialajn signojn kiel spacojn kaj krampojn kiuj alie estus misinterpretitaj.

Fine, ni transdonas wsl dukto-enigo kaj ajnaj komandliniaj argumentoj.

Per ĉi tiuj envolvaĵoj vi povas voki viajn plej ŝatatajn Linuksajn komandojn en pli natura maniero sen aldoni prefikson wsl kaj sen zorgi pri kiel la vojoj estas konvertitaj:

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

La baza aro de komandoj estas montrita ĉi tie, sed vi povas krei ŝelon por iu Linuksa komando simple aldonante ĝin al la listo. Se vi aldonas ĉi tiun kodon al via profilon PowerShell, ĉi tiuj komandoj estos disponeblaj por vi en ĉiu PowerShell-sesio, same kiel denaskaj komandoj!

Defaŭltaj Agordoj

En Linukso, estas ofte difini kaŝnomojn kaj/aŭ mediovariablojn en ensalutprofiloj, metante defaŭltajn parametrojn por ofte uzitaj komandoj (ekzemple, alias ls=ls -AFhexport LESS=-i). Unu el la malavantaĝoj de prokurado per ne-interaga ŝelo wsl.exe - ke la profiloj ne estas ŝarĝitaj, do ĉi tiuj opcioj ne estas disponeblaj defaŭlte (t.e. ls en WSL kaj wsl ls kondutos alimaniere kun la kaŝnomo difinita supre).

PowerShell provizas $PSDefaultParameterValues, norma mekanismo por difini defaŭltajn parametrojn, sed nur por cmdletoj kaj altnivelaj funkcioj. Kompreneble, ni povas fari altnivelajn funkciojn el niaj ŝeloj, sed ĉi tio enkondukas nenecesajn komplikaĵojn (ekzemple, PowerShell korelacias partajn parametrajn nomojn (ekzemple, -a korelacias kun -ArgumentList), kiu konfliktos kun Linukso-komandoj, kiuj prenas partajn nomojn kiel argumentojn), kaj la sintakso por difini defaŭltajn valorojn ne estos la plej taŭga (defaŭltaj argumentoj postulas la nomon de la parametro en la ŝlosilo, ne nur la komandnomo) .

Tamen, kun eta modifo al niaj ŝeloj, ni povas efektivigi modelon similan al $PSDefaultParameterValues, kaj ebligu defaŭltajn opciojn por Linuksaj komandoj!

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

Pasante $WslDefaultParameterValues al la komandlinio, ni sendas parametrojn per wsl.exe. La sekvanta montras kiel aldoni instrukciojn al via PowerShell-profilo por agordi defaŭltajn agordojn. Nun ni povas fari ĝin!

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

Ĉar la parametroj estas modeligitaj post $PSDefaultParameterValues, Vi povas estas facile malebligi ilin provizore instalante la ŝlosilon "Disabled" en signifon $true. Plia avantaĝo de aparta hashtabelo estas la kapablo malŝalti $WslDefaultParameterValues aparte de $PSDefaultParameterValues.

Argumenta kompletigo

PowerShell permesas vin registri argumentajn antaŭfilmojn uzante la komandon Register-ArgumentCompleter. Bash havas potencan programeblaj aŭtokompletaj iloj. WSL permesas al vi voki bash de PowerShell. Se ni povas registri argumentkompletigojn por niaj PowerShell-funkciaj envolvaĵoj kaj voki bash por generi la kompletojn, ni ricevas plenan argumentkompletigon kun la sama precizeco kiel bash mem!

# 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]
    }
}

La kodo estas iom densa sen kompreni iujn el la internaj funkcioj de bash, sed esence kion ni faras estas ĉi tio:

  • Registri argument-kompletigon por ĉiuj niaj funkcioj envolvante per listo $commands en parametro -CommandName por Register-ArgumentCompleter.
  • Ni mapas ĉiun komandon al la ŝelfunkcio, kiun bash uzas por aŭtomata kompletigo (por difini aŭtomatajn specifojn, bash uzas $F, mallongigo por complete -F <FUNCTION>).
  • Konvertado de PowerShell-Argumentoj $wordToComplete, $commandAst и $cursorPosition en la formaton atenditan de la aŭtomataj funkcioj de bash laŭ la specifoj programebla aŭtomata kompletigo bato.
  • Ni komponas komandlinion por translokiĝi al wsl.exe, kiu certigas ke la medio estas agordita ĝuste, vokas la taŭgan aŭtomatan funkcion, kaj eligas la rezultojn laŭ linio-post-linia modo.
  • Tiam ni vokas wsl kun la komandlinio, ni apartigas la eliron per liniaj apartigiloj kaj generas por ĉiu CompletionResults, ordigante ilin kaj eskapanta signojn kiel spacoj kaj krampoj kiuj alie estus misinterpretitaj.

Kiel rezulto, niaj Linuksaj komandŝeloj uzos ĝuste la saman aŭtokompleton kiel bash! Ekzemple:

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

Ĉiu aŭtomata kompletigo liveras valorojn specifajn por la antaŭa argumento, legante agordajn datumojn kiel konatajn gastigantojn de WSL!

<TAB> biciklos tra la parametroj. <Ctrl + пробел> montros ĉiujn disponeblajn eblojn.

Krome, ĉar ni nun havas bash-aŭtomatkompleton, vi povas aŭtomate kompletigi Linuksajn vojojn rekte en PowerShell!

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

En kazoj kie bash-aŭtomatkompleto ne produktas iujn ajn rezultojn, PowerShell revenas al la sistemaj defaŭltaj Vindozaj vojoj. Tiel, praktike, vi povas samtempe uzi ambaŭ vojojn laŭ via bontrovo.

konkludo

Uzante PowerShell kaj WSL, ni povas integri Linuksajn komandojn en Vindozon kiel denaskajn aplikojn. Ne necesas serĉi Win32-konstruaĵojn aŭ Linuksajn utilecojn aŭ interrompi vian laborfluon irante al Linuksa ŝelo. Nur instalu WSL, agordi PowerShell-profilo и listigu la komandojn, kiujn vi volas importi! Riĉa aŭtokompleto por Linukso kaj Vindozaj komandparametroj kaj dosiervojoj estas funkcieco, kiu eĉ ne haveblas en denaskaj Vindozaj komandoj hodiaŭ.

La plena fontkodo priskribita supre, same kiel pliaj gvidlinioj por korpigi ĝin en vian laborfluon, estas haveblaj tie.

Kiujn Linuksajn komandojn vi trovas plej utilaj? Kiuj aliaj komunaj aferoj mankas kiam vi laboras en Vindozo? Skribu en la komentoj aŭ sur GitHub!

fonto: www.habr.com

Aldoni komenton