Integració d'ordres de Linux a Windows mitjançant PowerShell i WSL

Una pregunta típica dels desenvolupadors de Windows: "Per què encara no hi ha <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Tant si es tracta d'un lliscament potent less o eines familiars grep o sed, els desenvolupadors de Windows volen accedir fàcilment a aquestes ordres en el seu treball diari.

Subsistema Windows per a Linux (WSL) ha fet un gran pas endavant en aquest sentit. Us permet cridar ordres de Linux des de Windows mitjançant un servidor intermediari wsl.exe (per exemple, wsl ls). Tot i que es tracta d'una millora important, aquesta opció pateix una sèrie d'inconvenients.

  • Addició omnipresent wsl tediós i antinatural.
  • Els camins de Windows als arguments no sempre funcionen perquè les barres invertides s'interpreten com a caràcters d'escapada en lloc de separadors de directoris.
  • Els camins de Windows als arguments no es tradueixen al punt de muntatge corresponent a WSL.
  • La configuració predeterminada no es respecta als perfils WSL amb àlies i variables d'entorn.
  • La finalització del camí de Linux no és compatible.
  • No s'admet la realització d'ordres.
  • No s'admet la finalització de l'argument.

Com a resultat, les ordres de Linux es tracten com a ciutadans de segona classe a Windows i són més difícils d'utilitzar que les ordres natives. Per igualar els seus drets, cal resoldre els problemes enumerats.

Embolcalls de funcions de PowerShell

Amb els embolcalls de funcions de PowerShell, podem afegir la finalització d'ordres i eliminar la necessitat de prefixos wsl, traduint camins de Windows en camins WSL. Requisits bàsics per a petxines:

  • Per a cada comanda de Linux hi ha d'haver un embolcall de funció amb el mateix nom.
  • El shell ha de reconèixer els camins de Windows passats com a arguments i convertir-los en camins WSL.
  • La closca hauria de cridar wsl amb l'ordre de Linux adequada a qualsevol entrada de canalització i passant qualsevol argument de línia d'ordres passat a la funció.

Com que aquest patró es pot aplicar a qualsevol ordre, podem abstraure la definició d'aquests embolcalls i generar-los dinàmicament a partir d'una llista d'ordres per importar.

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

Llista $command defineix les ordres d'importació. Aleshores generem dinàmicament un embolcall de funcions per a cadascun d'ells mitjançant l'ordre Invoke-Expression (primer eliminant qualsevol àlies que entre en conflicte amb la funció).

La funció itera sobre els arguments de la línia d'ordres, determina els camins de Windows mitjançant ordres Split-Path и Test-Pathi després converteix aquests camins en camins WSL. Executem els camins mitjançant una funció d'ajuda Format-WslArgument, que definirem més endavant. S'escapa de caràcters especials com espais i parèntesis que, d'altra manera, s'interpretarien malament.

Finalment, transmetem wsl entrada de pipeline i qualsevol argument de línia d'ordres.

Amb aquests embolcalls podeu trucar a les vostres ordres preferides de Linux d'una manera més natural sense afegir cap prefix wsl i sense preocupar-se de com es converteixen els camins:

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

El conjunt bàsic d'ordres es mostra aquí, però podeu crear un intèrpret d'ordres per a qualsevol ordre de Linux simplement afegint-lo a la llista. Si afegiu aquest codi al vostre perfil PowerShell, aquestes ordres estaran disponibles per a cada sessió de PowerShell, igual que les ordres natives!

Configuració per defecte

A Linux, és habitual definir àlies i/o variables d'entorn als perfils d'inici de sessió, establint paràmetres per defecte per a les ordres d'ús freqüent (per exemple, alias ls=ls -AFh o export LESS=-i). Un dels desavantatges del proxy mitjançant un shell no interactiu wsl.exe - que els perfils no es carreguen, de manera que aquestes opcions no estan disponibles per defecte (és a dir, ls en WSL i wsl ls es comportarà de manera diferent amb l'àlies definit anteriorment).

PowerShell proporciona $PSDefaultParameterValues, un mecanisme estàndard per definir paràmetres predeterminats, però només per a cmdlets i funcions avançades. Per descomptat, podem fer funcions avançades amb les nostres intèrprets d'ordres, però això introdueix complicacions innecessàries (per exemple, PowerShell correlaciona noms de paràmetres parcials (per exemple, -a correlaciona amb -ArgumentList), que entrarà en conflicte amb les ordres de Linux que prenen noms parcials com a arguments), i la sintaxi per definir valors per defecte no serà la més adequada (els arguments per defecte requereixen el nom del paràmetre a la clau, no només el nom de l'ordre). .

Tanmateix, amb una lleugera modificació a les nostres shells, podem implementar un model similar a $PSDefaultParameterValues, i habiliteu les opcions predeterminades per a les ordres de 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 ' ')
    }
}

Passant $WslDefaultParameterValues a la línia d'ordres, enviem paràmetres mitjançant wsl.exe. A continuació es mostra com afegir instruccions al vostre perfil de PowerShell per configurar la configuració predeterminada. Ara ho podem fer!

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

Atès que els paràmetres es modelen després $PSDefaultParameterValues, Tu pots és fàcil desactivar-los temporalment instal·lant la clau "Disabled" en sentit $true. Un avantatge addicional d'una taula hash independent és la possibilitat de desactivar-la $WslDefaultParameterValues per separat de $PSDefaultParameterValues.

Finalització de l'argument

PowerShell us permet registrar tràilers d'arguments mitjançant l'ordre Register-ArgumentCompleter. Bash té poder eines d'autocompleció programables. WSL us permet trucar a bash des de PowerShell. Si podem registrar les completacions d'arguments per als nostres embolcalls de funcions de PowerShell i cridar a bash per generar-les, obtenim la finalització completa de l'argument amb la mateixa precisió que el mateix 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]
    }
}

El codi és una mica dens sense entendre algunes de les funcions internes de bash, però bàsicament el que fem és això:

  • Registre d'un completador d'arguments per a tots els nostres embolcalls de funcions passant una llista $commands al paràmetre -CommandName per Register-ArgumentCompleter.
  • Mapem cada comanda a la funció de l'intèrpret d'ordres que fa servir bash per a la compleció automàtica (per definir les especificacions d'autocompleció, bash utilitza $F, abreviatura de complete -F <FUNCTION>).
  • Conversió d'arguments de PowerShell $wordToComplete, $commandAst и $cursorPosition al format esperat per les funcions d'autocompleció de bash segons les especificacions autocompleció programable xoc.
  • Composem una línia d'ordres per transferir-la wsl.exe, que garanteix que l'entorn estigui configurat correctament, crida a la funció d'autocompleció adequada i mostra els resultats de manera línia per línia.
  • Llavors truquem wsl amb la línia d'ordres, separem la sortida per separadors de línia i generem per a cadascun CompletionResults, ordenant-los i escapant caràcters com espais i parèntesis que, d'altra manera, s'interpretarien malament.

Com a resultat, els nostres intèrprets d'ordres de Linux utilitzaran exactament el mateix compliment automàtic que bash! Per exemple:

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

Cada autocompleció proporciona valors específics per a l'argument anterior, llegint dades de configuració com ara hosts coneguts de WSL!

<TAB> circularà pels paràmetres. <Ctrl + пробел> mostrarà totes les opcions disponibles.

A més, com que ara tenim l'autocompleció bash, podeu completar automàticament els camins de Linux directament a PowerShell!

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

En els casos en què l'emplenament automàtic de bash no produeix cap resultat, PowerShell torna als camins predeterminats de Windows del sistema. Així, a la pràctica, podeu utilitzar simultàniament tots dos camins a la vostra discreció.

Conclusió

Mitjançant PowerShell i WSL, podem integrar ordres de Linux a Windows com a aplicacions natives. No cal cercar compilacions Win32 o utilitats de Linux ni interrompre el vostre flux de treball anant a un shell de Linux. Només instal·lar WSL, configurar Perfil de PowerShell и llista les ordres que voleu importar! L'autocompleció ric per a paràmetres d'ordres de Linux i Windows i rutes de fitxers és una funcionalitat que ni tan sols està disponible a les ordres natives de Windows avui.

El codi font complet descrit anteriorment, així com les directrius addicionals per incorporar-lo al vostre flux de treball, estan disponibles aquí.

Quines ordres de Linux trobeu més útils? Quines altres coses habituals falten quan es treballa a Windows? Escriu als comentaris o a GitHub!

Font: www.habr.com

Afegeix comentari