Integrarea comenzilor Linux în Windows folosind PowerShell și WSL

O întrebare tipică a dezvoltatorilor Windows: „De ce încă nu există <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Fie că este o glisare puternică less sau instrumente familiare grep sau sed, dezvoltatorii Windows doresc acces ușor la aceste comenzi în munca lor de zi cu zi.

Subsistem Windows pentru Linux (WSL) a făcut un mare pas înainte în acest sens. Vă permite să apelați comenzi Linux din Windows prin proxy wsl.exe (De exemplu, wsl ls). Deși aceasta este o îmbunătățire semnificativă, această opțiune suferă de o serie de dezavantaje.

  • Adăugare omniprezentă wsl plictisitor și nefiresc.
  • Căile Windows în argumente nu funcționează întotdeauna, deoarece barele oblice inverse sunt interpretate ca caractere de escape, mai degrabă decât ca separatori de directoare.
  • Căile Windows în argumente nu sunt traduse în punctul de montare corespunzător în WSL.
  • Setările implicite nu sunt respectate în profilurile WSL cu aliasuri și variabile de mediu.
  • Finalizarea căii Linux nu este acceptată.
  • Finalizarea comenzii nu este acceptată.
  • Finalizarea argumentului nu este acceptată.

Drept urmare, comenzile Linux sunt tratate ca cetățeni de clasa a doua sub Windows și sunt mai dificil de utilizat decât comenzile native. Pentru a le egaliza drepturile, este necesar să se rezolve problemele enumerate.

Învelișuri ale funcției PowerShell

Cu pachetele de funcții PowerShell, putem adăuga completarea comenzii și eliminăm necesitatea prefixelor wsl, traducând căile Windows în căi WSL. Cerințe de bază pentru scoici:

  • Pentru fiecare comandă Linux trebuie să existe un wrapper de funcție cu același nume.
  • Shell-ul trebuie să recunoască căile Windows transmise ca argumente și să le convertească în căi WSL.
  • Shell ar trebui să sune wsl cu comanda Linux corespunzătoare la orice intrare în conductă și transmiterea oricăror argumente din linia de comandă transmise funcției.

Deoarece acest model poate fi aplicat oricărei comenzi, putem abstra definiția acestor wrapper-uri și le putem genera dinamic dintr-o listă de comenzi de importat.

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

listă $command definește comenzile de import. Apoi generăm dinamic un wrapper de funcție pentru fiecare dintre ele folosind comanda Invoke-Expression (înlăturând mai întâi orice alias care ar intra în conflict cu funcția).

Funcția iterează peste argumentele liniei de comandă, determină căile Windows folosind comenzi Split-Path и Test-Pathși apoi convertește aceste căi în căi WSL. Rulem căile printr-o funcție de ajutor Format-WslArgument, pe care o vom defini mai târziu. Ea scapă de caractere speciale, cum ar fi spații și paranteze, care altfel ar fi interpretate greșit.

În sfârșit, transmitem wsl intrare pipeline și orice argument de linie de comandă.

Cu aceste wrapper-uri puteți apela comenzile Linux preferate într-un mod mai natural, fără a adăuga un prefix wsl și fără să vă faceți griji despre cum sunt convertite căile:

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

Setul de bază de comenzi este afișat aici, dar puteți crea un shell pentru orice comandă Linux prin simpla adăugare a acesteia la listă. Dacă adăugați acest cod la dvs profil PowerShell, aceste comenzi vă vor fi disponibile în fiecare sesiune PowerShell, la fel ca și comenzile native!

Setări implicite

În Linux, este obișnuit să se definească aliasuri și/sau variabile de mediu în profilurile de conectare, setând parametri impliciti pentru comenzile utilizate frecvent (de exemplu, alias ls=ls -AFh sau export LESS=-i). Unul dintre dezavantajele proxy-ului printr-un shell non-interactiv wsl.exe - că profilurile nu sunt încărcate, deci aceste opțiuni nu sunt disponibile implicit (de ex. ls în WSL și wsl ls se va comporta diferit cu aliasul definit mai sus).

PowerShell oferă $PSDefaultParameterValues, un mecanism standard pentru definirea parametrilor impliciti, dar numai pentru cmdlet-uri și funcții avansate. Desigur, putem face funcții avansate din shell-urile noastre, dar acest lucru introduce complicații inutile (de exemplu, PowerShell corelează numele parțiale de parametri (de exemplu, -a se coreleaza cu -ArgumentList), care va intra în conflict cu comenzile Linux care iau nume parțiale drept argumente), iar sintaxa pentru definirea valorilor implicite nu va fi cea mai potrivită (argumentele implicite necesită numele parametrului din cheie, nu doar numele comenzii) .

Cu toate acestea, cu o ușoară modificare a shell-urilor noastre, putem implementa un model similar cu $PSDefaultParameterValuesși activați opțiunile implicite pentru comenzile 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 ' ')
    }
}

Trecere $WslDefaultParameterValues la linia de comandă, trimitem parametrii prin wsl.exe. Următoarele arată cum să adăugați instrucțiuni la profilul dvs. PowerShell pentru a configura setările implicite. Acum o putem face!

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

Deoarece parametrii sunt modelați după $PSDefaultParameterValuespoti este ușor să le dezactivați temporar prin instalarea cheii "Disabled" în sens $true. Un avantaj suplimentar al unui tabel hash separat este capacitatea de a dezactiva $WslDefaultParameterValues separat de $PSDefaultParameterValues.

Finalizarea argumentului

PowerShell vă permite să înregistrați trailere cu argumente folosind comanda Register-ArgumentCompleter. Bash are putere instrumente programabile de completare automată. WSL vă permite să apelați bash din PowerShell. Dacă putem înregistra completări de argument pentru wrapper-urile noastre cu funcția PowerShell și putem apela bash pentru a genera completările, obținem finalizarea completă a argumentului cu aceeași precizie ca și bash în sine!

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

Codul este puțin dens, fără a înțelege unele dintre funcțiile interne ale bash, dar în principiu ceea ce facem este următorul:

  • Înregistrarea unui completator de argumente pentru toate pachetele noastre de funcții prin transmiterea unei liste $commands în parametru -CommandName pentru Register-ArgumentCompleter.
  • Mapăm fiecare comandă la funcția shell pe care o folosește bash pentru completare automată (pentru a defini specificațiile de completare automată, bash folosește $F, prescurtare pentru complete -F <FUNCTION>).
  • Conversia argumentelor PowerShell $wordToComplete, $commandAst и $cursorPosition în formatul așteptat de funcțiile de completare automată ale bash, conform specificațiilor completare automată programabilă bash.
  • Compunem o linie de comandă la care să o transferăm wsl.exe, care asigură configurarea corectă a mediului, apelează funcția de completare automată corespunzătoare și emite rezultatele în mod linie cu linie.
  • Apoi sunăm wsl cu linia de comandă, separăm rezultatul prin separatori de linie și generăm pentru fiecare CompletionResults, sortându-le și evadând caractere, cum ar fi spații și paranteze, care altfel ar fi interpretate greșit.

Ca rezultat, shell-urile noastre de comandă Linux vor folosi exact aceeași completare automată ca și bash! De exemplu:

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

Fiecare completare automată furnizează valori specifice argumentului anterior, citind date de configurare precum gazde cunoscute de la WSL!

<TAB> va parcurge parametrii. <Ctrl + пробел> va afișa toate opțiunile disponibile.

În plus, deoarece avem acum completare automată bash, puteți completa automat căile Linux direct în PowerShell!

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

În cazurile în care completarea automată bash nu produce niciun rezultat, PowerShell revine la căile Windows implicite ale sistemului. Astfel, în practică, poți folosi simultan ambele căi la discreția ta.

Concluzie

Folosind PowerShell și WSL, putem integra comenzi Linux în Windows ca aplicații native. Nu este nevoie să căutați versiuni Win32 sau utilitare Linux sau să vă întrerupeți fluxul de lucru accesând un shell Linux. Doar instalați WSL, configurați Profil PowerShell и enumerați comenzile pe care doriți să le importați! Completarea automată bogată pentru parametrii de comandă Linux și Windows și căile fișierelor este o funcționalitate care nici măcar nu este disponibilă în comenzile Windows native astăzi.

Codul sursă complet descris mai sus, precum și instrucțiuni suplimentare pentru încorporarea acestuia în fluxul de lucru, sunt disponibile aici.

Care comenzi Linux vi se pare cele mai utile? Ce alte lucruri comune lipsesc atunci când lucrați în Windows? Scrieți în comentarii sau pe GitHub!

Sursa: www.habr.com

Adauga un comentariu