Integrazione dei comandi Linux in Windows utilizzando PowerShell e WSL

Una tipica domanda degli sviluppatori Windows: “Perché non esiste ancora <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Che si tratti di un colpo potente less o strumenti familiari grep o sed, gli sviluppatori Windows desiderano un facile accesso a questi comandi nel loro lavoro quotidiano.

Sottosistema Windows per Linux (WSL) ha fatto un enorme passo avanti in questo senso. Ti consente di chiamare comandi Linux da Windows tramite proxy wsl.exe (Per esempio, wsl ls). Sebbene si tratti di un miglioramento significativo, questa opzione presenta una serie di svantaggi.

  • Aggiunta onnipresente wsl noioso e innaturale.
  • I percorsi Windows negli argomenti non sempre funzionano perché le barre rovesciate vengono interpretate come caratteri di escape anziché come separatori di directory.
  • I percorsi Windows negli argomenti non vengono tradotti nel punto di montaggio corrispondente in WSL.
  • Le impostazioni predefinite non vengono rispettate nei profili WSL con alias e variabili di ambiente.
  • Il completamento del percorso Linux non è supportato.
  • Il completamento del comando non è supportato.
  • Il completamento dell'argomento non è supportato.

Di conseguenza, i comandi Linux vengono trattati come cittadini di seconda classe in Windows e sono più difficili da usare rispetto ai comandi nativi. Per eguagliare i loro diritti, è necessario risolvere i problemi elencati.

Wrapper di funzioni PowerShell

Con i wrapper di funzioni PowerShell, possiamo aggiungere il completamento dei comandi ed eliminare la necessità di prefissi wsl, traducendo i percorsi Windows in percorsi WSL. Requisiti di base per le shell:

  • Per ogni comando Linux deve esserci un wrapper di funzione con lo stesso nome.
  • La shell deve riconoscere i percorsi Windows passati come argomenti e convertirli in percorsi WSL.
  • La shell dovrebbe chiamare wsl con il comando Linux appropriato a qualsiasi input della pipeline e passando qualsiasi argomento della riga di comando passato alla funzione.

Poiché questo modello può essere applicato a qualsiasi comando, possiamo astrarre la definizione di questi wrapper e generarli dinamicamente da un elenco di comandi da importare.

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

Elenco $command definisce i comandi di importazione. Quindi generiamo dinamicamente un wrapper di funzione per ciascuno di essi utilizzando il comando Invoke-Expression (rimuovendo prima eventuali alias che potrebbero entrare in conflitto con la funzione).

La funzione esegue l'iterazione sugli argomenti della riga di comando e determina i percorsi di Windows utilizzando i comandi Split-Path и Test-Pathe quindi converte questi percorsi in percorsi WSL. Eseguiamo i percorsi attraverso una funzione di supporto Format-WslArgument, che definiremo in seguito. Evita i caratteri speciali come spazi e parentesi che altrimenti verrebbero interpretati erroneamente.

Infine, trasmettiamo wsl input della pipeline e qualsiasi argomento della riga di comando.

Con questi wrapper puoi chiamare i tuoi comandi Linux preferiti in modo più naturale senza aggiungere un prefisso wsl e senza preoccuparsi di come vengono convertiti i percorsi:

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

Il set di comandi di base è mostrato qui, ma puoi creare una shell per qualsiasi comando Linux semplicemente aggiungendolo all'elenco. Se aggiungi questo codice al tuo profilo PowerShell, questi comandi saranno disponibili in ogni sessione di PowerShell, proprio come i comandi nativi!

Impostazioni predefinite

In Linux, è prassi comune definire alias e/o variabili d'ambiente nei profili di accesso, impostando parametri predefiniti per i comandi utilizzati di frequente (ad esempio, alias ls=ls -AFh o export LESS=-i). Uno degli svantaggi del proxy tramite una shell non interattiva wsl.exe - che i profili non vengono caricati, quindi queste opzioni non sono disponibili per impostazione predefinita (es. ls nel WSL e wsl ls si comporterà diversamente con l'alias definito sopra).

PowerShell fornisce $PSDefaultParameterValues, un meccanismo standard per la definizione dei parametri predefiniti, ma solo per cmdlet e funzioni avanzate. Naturalmente, possiamo creare funzioni avanzate dalle nostre shell, ma ciò introduce complicazioni inutili (ad esempio, PowerShell correla nomi di parametri parziali (ad esempio, -a è correlato con -ArgumentList), che entrerà in conflitto con i comandi Linux che accettano nomi parziali come argomenti), e la sintassi per definire i valori predefiniti non sarà la più appropriata (gli argomenti predefiniti richiedono il nome del parametro nella chiave, non solo il nome del comando) .

Tuttavia, con una leggera modifica alle nostre shell, possiamo implementare un modello simile a $PSDefaultParameterValuese abilita le opzioni predefinite per i comandi 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 ' ')
    }
}

Passando $WslDefaultParameterValues alla riga di comando, inviamo i parametri tramite wsl.exe. Di seguito viene illustrato come aggiungere istruzioni al profilo PowerShell per configurare le impostazioni predefinite. Ora possiamo farlo!

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

Poiché i parametri sono modellati su $PSDefaultParameterValues, Puoi è facile disabilitarli temporaneamente installando la chiave "Disabled" nel significato $true. Un ulteriore vantaggio di una tabella hash separata è la possibilità di disabilitarla $WslDefaultParameterValues separatamente da $PSDefaultParameterValues.

Completamento dell'argomento

PowerShell consente di registrare i trailer degli argomenti utilizzando il comando Register-ArgumentCompleter. Bash è potente strumenti di completamento automatico programmabili. WSL ti consente di chiamare bash da PowerShell. Se possiamo registrare i completamenti degli argomenti per i nostri wrapper di funzioni PowerShell e chiamare bash per generare i completamenti, otteniamo il completamento completo degli argomenti con la stessa precisione della bash stessa!

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

Il codice è un po' denso e non consente di comprendere alcune delle funzioni interne di bash, ma sostanzialmente ciò che facciamo è questo:

  • Registrazione di un completatore di argomenti per tutti i nostri wrapper di funzioni passando un elenco $commands nel parametro -CommandName per Register-ArgumentCompleter.
  • Mappiamo ogni comando alla funzione shell che bash usa per il completamento automatico (per definire le specifiche del completamento automatico, bash usa $F, abbreviazione di complete -F <FUNCTION>).
  • Conversione di argomenti di PowerShell $wordToComplete, $commandAst и $cursorPosition nel formato previsto dalle funzioni di completamento automatico di bash in base alle specifiche completamento automatico programmabile bash.
  • Componiamo una riga di comando a cui trasferire wsl.exe, che garantisce che l'ambiente sia configurato correttamente, chiama la funzione di completamento automatico appropriata e restituisce i risultati riga per riga.
  • Poi chiamiamo wsl con la riga di comando, separiamo l'output con separatori di riga e generiamo per ciascuno CompletionResults, ordinandoli ed evitando caratteri come spazi e parentesi che altrimenti verrebbero interpretati erroneamente.

Di conseguenza, le nostre shell dei comandi Linux utilizzeranno esattamente lo stesso completamento automatico di bash! Per esempio:

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

Ogni completamento automatico fornisce valori specifici dell'argomento precedente, leggendo dati di configurazione come host conosciuti da WSL!

<TAB> scorrerà i parametri. <Ctrl + пробел> mostrerà tutte le opzioni disponibili.

Inoltre, poiché ora è disponibile il completamento automatico di bash, puoi completare automaticamente i percorsi Linux direttamente in PowerShell!

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

Nei casi in cui il completamento automatico di bash non produce alcun risultato, PowerShell ripristina i percorsi Windows predefiniti del sistema. Pertanto, in pratica, puoi utilizzare entrambi i percorsi contemporaneamente a tua discrezione.

conclusione

Utilizzando PowerShell e WSL, possiamo integrare i comandi Linux in Windows come applicazioni native. Non è necessario cercare build Win32 o utilità Linux o interrompere il flusso di lavoro accedendo a una shell Linux. Appena installare WSL, configurare Profilo di PowerShell и elenca i comandi che desideri importare! Il completamento automatico completo per i parametri dei comandi Linux e Windows e i percorsi dei file è una funzionalità che oggi non è nemmeno disponibile nei comandi nativi di Windows.

È disponibile il codice sorgente completo sopra descritto, nonché linee guida aggiuntive per incorporarlo nel flusso di lavoro qui.

Quali comandi Linux trovi più utili? Quali altre cose comuni mancano quando si lavora in Windows? Scrivi nei commenti oppure su GitHub!

Fonte: habr.com

Aggiungi un commento