Integrazione di cumandamenti Linux in Windows cù PowerShell è WSL

Una quistione tipica da i sviluppatori di Windows: "Perchè ùn ci hè ancu micca <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Ch'ella sia un swipe putente less o arnesi familiari grep o sed, I sviluppatori di Windows volenu un accessu faciule à questi cumandamenti in u so travagliu di ogni ghjornu.

Sottosistema Windows per Linux (WSL) hà fattu un grande passu avanti in questu sensu. Permette di chjamà i cumandamenti di Linux da Windows per proxy wsl.exe (p.e. wsl ls). Ancu s'ellu hè una mellura significativa, sta opzione soffre di una quantità di svantaghji.

  • Addizione omnipresente wsl tediosa è antinaturale.
  • I percorsi di Windows in l'argumenti ùn funzionanu micca sempre perchè i backslash sò interpretati cum'è caratteri di escape piuttostu cà separatori di directory.
  • I percorsi di Windows in argumenti ùn sò micca tradutti à u puntu di muntagna currispundente in WSL.
  • I paràmetri predeterminati ùn sò micca rispettati in i profili WSL cù alias è variabili di l'ambiente.
  • U cumpletu di u percorsu Linux ùn hè micca supportatu.
  • Cumpiimentu di cumanda ùn hè micca supportatu.
  • U cumpletu di l'argumentu ùn hè micca supportatu.

In u risultatu, i cumandamenti Linux sò trattati cum'è citadini di seconda classe sottu Windows - è sò più difficiuli d'utilizà cà i cumandamenti nativi. Per equalizà i so diritti, hè necessariu di risolve i prublemi listati.

Wrapper di funzione PowerShell

Cù i wrappers di funzione PowerShell, pudemu aghjustà u cumpletu di cumandamenti è eliminà a necessità di prefissi wsl, traducendu i percorsi Windows in percorsi WSL. Requisiti basi per cunchiglia:

  • Per ogni cumandamentu Linux deve esse una funzione wrapper cù u stessu nome.
  • A cunchiglia deve ricunnosce i percorsi di Windows passati cum'è argumenti è cunvertisce in camini WSL.
  • A cunchiglia deve chjamà wsl cù u cumandamentu Linux appropritatu à qualsiasi input di pipeline è passendu qualsiasi argumenti di linea di cumanda passati à a funzione.

Siccomu stu mudellu pò esse appiicatu à qualsiasi cumandamentu, pudemu astrattu a definizione di sti wrappers è generà dinamicamente da una lista di cumandamenti per impurtà.

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

a lista $command definisce i cumandamenti d'importazione. Allora generà dinamicamente un wrapper di funzione per ognunu di elli usendu u cumandamentu Invoke-Expression (per prima sguassate qualsiasi alias chì anu cunflittu cù a funzione).

A funzione itera nantu à l'argumenti di a linea di cumanda, determina i percorsi di Windows utilizendu cumandamenti Split-Path и Test-Pathè poi cunvertisce sti camini in camini WSL. Eseguimu i camini attraversu una funzione d'aiutu Format-WslArgument, chì avemu da definisce dopu. Scappa caratteri spiciali cum'è spazii è parentesi chì altrimenti seranu malinterpretati.

Infine, trasmettemu wsl input di pipeline è qualsiasi argumenti di linea di cummanda.

Cù questi wrappers pudete chjamà i vostri cumandamenti Linux preferiti in una manera più naturali senza aghjunghje un prefissu wsl è senza preoccupari di cumu si cunvertisce i camini:

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

U settore di basi di cumandamenti hè mostratu quì, ma pudete creà una cunchiglia per qualsiasi cumandamentu Linux solu aghjunghjendu à a lista. Sè vo aghjunghje stu codice à u vostru prufilu PowerShell, sti cumandamenti seranu dispunibili per voi in ogni sessione di PowerShell, cum'è i cumandamenti nativi!

Impostazioni predefinite

In Linux, hè cumunu di definisce aliases è / o variabili di l'ambiente in i profili di login, stabilendu paràmetri predeterminati per i cumandamenti spessu usati (per esempiu, alias ls=ls -AFh o export LESS=-i). Unu di i svantaghji di proxy attraversu una cunchiglia non interattiva wsl.exe - chì i profili ùn sò micca caricati, cusì queste opzioni ùn sò micca dispunibuli per difettu (i.e. ls in WSL è wsl ls si cumportarà di manera diversa cù l'alias definitu sopra).

PowerShell furnisce $PSDefaultParameterValues, un mecanismu standard per definisce i paràmetri predeterminati, ma solu per i cmdlets è e funzioni avanzate. Di sicuru, pudemu fà funzioni avanzate fora di i nostri cunchiglia, ma questu introduce cumplicazioni innecessarii (per esempiu, PowerShell correlate i nomi di parametri parziali (per esempiu, -a correlate cù -ArgumentList), chì entre in cunflittu cù cumandamenti Linux chì piglianu nomi parziali cum'è argumenti), è a sintassi per definisce i valori predeterminati ùn serà micca u più apprupriatu (l'argumenti predeterminati necessitanu u nome di u paràmetru in a chjave, micca solu u nome di cumandamentu) .

Tuttavia, cù una ligera mudificazione à i nostri cunchiglia, pudemu implementà un mudellu simili à $PSDefaultParameterValues, è attivate l'opzioni predeterminate per i cumandamenti 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 ' ')
    }
}

Passendu $WslDefaultParameterValues à a linea di cummanda, mandemu paràmetri via wsl.exe. U seguitu mostra cumu aghjunghje struzzioni à u vostru prufilu PowerShell per cunfigurà i paràmetri predeterminati. Avà pudemu fà!

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

Siccomu i paràmetri sò modellati dopu $PSDefaultParameterValues, Poi hè faciule di disattivà elli temporaneamente installendu a chjave "Disabled" in significatu $true. Un benefiziu supplementu di una tavola hash separata hè a capacità di disattivà $WslDefaultParameterValues separatamente da $PSDefaultParameterValues.

Cumplementu di l'argumentu

PowerShell permette di registrà trailers d'argumenti cù u cumandamentu Register-ArgumentCompleter. Bash hà putente Strumenti di cumpletamentu automaticu programabili. WSL vi permette di chjamà bash da PowerShell. Se pudemu registrà u cumpletu di l'argumentu per i nostri wrappers di funzioni PowerShell è chjamà bash per generà e cumpletture, avemu un cumpletu di l'argumentu cumpletu cù a stessa precisione cum'è bash stessu!

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

U codice hè un pocu densu senza capisce alcune di e funzioni interne di bash, ma in fondu ciò chì facemu hè questu:

  • Registrazione di un argumentu cumpletu per tutti i nostri wrappers di funzione passendu una lista $commands in paràmetru -CommandName di Register-ArgumentCompleter.
  • Mapemu ogni cumandamentu à a funzione di shell chì bash usa per l'autocompletion (per definisce e specificazioni di autocompletion, bash usa $F, abbreviazione per complete -F <FUNCTION>).
  • Cunvertisce Argumenti PowerShell $wordToComplete, $commandAst и $cursorPosition in u formatu previstu da e funzioni di autocompletion di bash secondu e specificazioni cumpletamentu automaticu programmabile bacca.
  • Cumponemu una linea di cummanda per trasfiriri wsl.exe, chì assicura chì l'ambienti hè stallatu currettamente, chjama a funzione di cumpleta automatica adattata, è pruduce i risultati in linea per linea.
  • Allora chjamemu wsl cù a linea di cummanda, separemu l'output per separatori di linea è generà per ognunu CompletionResults, sortenduli è scappendu caratteri cum'è spazii è parentesi chì altrimenti seranu malinterpretati.

In u risultatu, i nostri cunchiglia di cumandamenti Linux utilizanu esattamente u listessu autocompletion cum'è bash! Per esempiu:

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

Ogni autocumplementu furnisce valori specifichi à l'argumentu precedente, leghjendu dati di cunfigurazione cum'è ospiti cunnisciuti da WSL!

<TAB> ciclu i paràmetri. <Ctrl + пробел> mostrarà tutte l'opzioni dispunibili.

In più, postu chì avemu avà bash autocompletion, pudete cumplettà automaticamente i percorsi Linux direttamente in PowerShell!

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

In i casi induve l'autocompletion bash ùn pruduce micca risultati, PowerShell torna à i percorsi predefiniti di u sistema di Windows. Cusì, in pratica, pudete aduprà simultaneamente e duie strade à a vostra discrezione.

cunchiusioni

Utilizendu PowerShell è WSL, pudemu integrà cumandamenti Linux in Windows cum'è applicazioni native. Ùn ci hè bisognu di cercà Win32 builds o utilità Linux o interrompe u vostru flussu di travagliu andendu in una cunchiglia Linux. Solu stallà WSL, cunfigurà Profilu PowerShell и lista i cumandamenti chì vulete impurtà! L'autocompletazione ricca per i paràmetri di cumandamenti di Linux è Windows è i percorsi di i schedari hè una funziunalità chì ùn hè ancu dispunibule in i cumandamenti nativi di Windows oghje.

U codice fonte sanu descrittu sopra, è ancu linee supplementari per incorpore in u vostru flussu di travagliu, hè dispunibule ccà.

Quale cumandamenti Linux truvate più utile? Chì altre cose cumuni mancanu quandu travaglia in Windows? Scrivite in i cumenti o nantu à GitHub!

Source: www.habr.com

Add a comment