Integrando i comandi Linux in Windows tramite PowerShell e WSL

Una domanda ricorrente tra gli sviluppatori Windows è: «Perché qui non c'è ancora <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?». Будь то мощное пролистывание less o strumenti familiari grep o sed, gli sviluppatori Windows desiderano avere un facile accesso a questi comandi nel loro lavoro quotidiano.

Il sottosistema Windows per Linux (WSL) ha fatto passi da gigante in questo senso. Permette di invocare comandi Linux da Windows, inoltrandoli tramite wsl.exe (ad esempio, wsl ls). Sebbene si tratti di un miglioramento significativo, questa opzione presenta diversi svantaggi.

  • L'aggiunta ubiqua di wsl è faticosa e innaturale.
  • I percorsi Windows negli argomenti non sempre funzionano, poiché le barre inverse vengono interpretate come caratteri di escape e non come delimitatori di directory.
  • I percorsi Windows negli argomenti non vengono tradotti nel relativo punto di montaggio in WSL.
  • Non vengono considerati i parametri predefiniti nei profili WSL con alias e variabili d'ambiente.
  • Non è supportato il completamento dei percorsi Linux.
  • Non è supportato il completamento dei comandi.
  • Non è supportato il completamento degli argomenti.

Le comandi Linux sono percepite in Windows come cittadini di seconda classe, rendendole più difficili da utilizzare rispetto ai comandi nativi. Per equalizzarle, è necessario risolvere i problemi elencati.

Interfacce delle funzioni PowerShell

Utilizzando le interfacce delle funzioni PowerShell possiamo aggiungere il completamento automatico dei comandi ed eliminare la necessità di prefissi wsl, traducendo i percorsi di Windows in percorsi WSL. I requisiti principali per le interfacce sono:

  • Per ogni comando Linux deve esserci un'interfaccia funzione con lo stesso nome.
  • L'interfaccia deve riconoscere i percorsi di Windows passati come argomenti e convertirli nei percorsi WSL.
  • L'interfaccia deve invocare wsl il comando Linux corrispondente per qualsiasi input della pipeline, passando eventuali argomenti della riga di comando forniti alla funzione.

Poiché questo modello può essere applicato a qualsiasi comando, possiamo astrarre la definizione di queste interfacce e generarle 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 da importare. Poi generiamo dinamicamente un wrapper di funzione per ognuno di essi utilizzando il comando Invoke-Expression (rimuovendo prima eventuali alias che potrebbero confliggere con la funzione).

La funzione elenca gli argomenti della riga di comando, determina i percorsi di Windows tramite comandi Split-Path e Test-Path, e quindi converte questi percorsi in percorsi WSL. Passiamo i percorsi attraverso una funzione di supporto Format-WslArgument, che definiremo in seguito. Essa esegue l'escaping dei caratteri speciali, come spazi e parentesi, che altrimenti verrebbero interpretati in modo errato.

Infine, passiamo wsl l'input della pipeline e qualsiasi argomento della riga di comando.

Con queste incapsulazioni, puoi chiamare i tuoi comandi Linux preferiti in modo più naturale, senza aggiungere un prefisso wsl e senza preoccuparti di come vengono convertiti i percorsi:

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

Ecco un set di comandi di base, ma puoi creare un incapsulamento per qualsiasi comando Linux, semplicemente aggiungendolo all'elenco. Se aggiungi questo codice al tuo il profilo PowerShell, questi comandi saranno disponibili per te in ogni sessione di PowerShell, proprio come i comandi nativi!

Impostazioni predefinite

In Linux è consuetudine definire alias e/o variabili d'ambiente nei profili (login profile), impostando parametri predefiniti per i comandi utilizzati frequentemente (ad esempio, alias ls=ls -AFh o export LESS=-i). Uno dei difetti del proxy attraverso una shell non interattiva wsl.exe è che i profili non vengono caricati, quindi questi parametri non sono disponibili per impostazione predefinita (cioè, ls in WSL e wsl ls si comporteranno in modo diverso rispetto all'alias definito sopra).

PowerShell fornisce $PSDefaultParameterValues, un meccanismo standard per definire i parametri predefiniti, ma solo per i cmdlet e le funzioni avanzate. Certamente, possiamo rendere le nostre shell funzioni avanzate, ma ciò introduce complicazioni superflue (quindi, PowerShell associa i nomi parziali dei parametri (ad esempio, -a si associa a -ArgumentList), che confliggeranno con i comandi Linux che accettano nomi parziali come argomenti), e la sintassi per definire i valori predefiniti non sarà la più adatta (per definire gli argomenti predefiniti è necessario il nome del parametro nella chiave, non solo il nome del comando).

Tuttavia, con una piccola modifica alle nostre shell possiamo implementare un modello simile $PSDefaultParameterValues, e abilitare i parametri predefiniti per i comandi Linux!

funzione globale:$_() {
    …
 
    `$defaultArgs = ((`$WslDefaultParameterValues.$_ -split ' '), "")[`$WslDefaultParameterValues.Disabled -eq `$true]
    se (`$input.MoveNext()) {
        `$input.Reset()
        `$input | wsl.exe $_ `$defaultArgs (`$args -split ' ')
    } altrimenti {
        wsl.exe $_ `$defaultArgs (`$args -split ' ')
    }
}

Passando $WslDefaultParameterValues alla riga di comando, stiamo inviando i parametri attraverso wsl.exe. Qui sotto è mostrato come aggiungere istruzioni nel profilo PowerShell per configurare i parametri predefiniti. Ora possiamo farlo!

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

Poiché i parametri sono modellati dopo $PSDefaultParameterValues, puoi disabilitarli facilmente temporaneamente impostando la chiave "Disabled" su $true. Un ulteriore vantaggio di una tabella hash separata è la possibilità di disabilitare $WslDefaultParameterValues indipendentemente da $PSDefaultParameterValues.

Il completamento automatico degli argomenti

PowerShell consente di registrare i completatori di argomenti tramite il comando Register-ArgumentCompleter. In Bash ci sono potenti strumenti programmabili per il completamento automatico. WSL consente di chiamare bash da PowerShell. Se possiamo registrare i completamenti degli argomenti per le nostre funzioni PowerShell e chiamare bash per generare i completamenti, otterremo un'autocompletamento completo degli argomenti con la stessa precisione di 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]
    }
}

Il codice è un po' denso senza comprendere alcune funzioni interne di bash, ma principalmente facciamo quanto segue:

  • Registriamo il completamento degli argomenti per tutti i nostri wrapper di funzione, passando l'elenco $commands nel parametro -CommandName per Register-ArgumentCompleter.
  • Associamo ogni comando alla funzione del terminale utilizzata da bash per il completamento automatico (per determinare le specifiche di completamento automatico in bash si usa $F, abbreviazione di complete -F).
  • Convertiamo gli argomenti PowerShell $wordToComplete, $commandAst e $cursorPosition in un formato atteso dalle funzioni di completamento automatico di bash secondo le specifiche del completamento automatico programmabile bash.
  • Costruiamo la riga di comando da passare a wsl.exe, che fornisce la corretta configurazione dell'ambiente, chiama la funzione di completamento appropriata e restituisce i risultati separati per riga.
  • Poi chiamiamo wsl con la riga di comando, separiamo le uscite con delimitatori di riga e generiamo per ciascuna CompletionResults, ordinandole e schermando caratteri come spazi e parentesi, che altrimenti verrebbero fraintesi.

In questo modo, le nostre shell di comando Linux utilizzeranno lo stesso completamento automatico presente in bash! Ad esempio:

  • ssh -c -J -m -O -o -Q -w -b

Ogni completamento automatico fornisce valori specifici per l'argomento precedente, leggendo dati di configurazione come gli host noti da WSL!

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

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

  • less /etc/
  • ls /usr/share/
  • vim ~/.*bash

Nei casi in cui il completamento automatico di bash non restituisce risultati, PowerShell torna ai percorsi di sistema predefiniti di Windows. In questo modo, puoi utilizzare praticamenete entrambi i tipi di percorsi a tua discrezione.

Conclusione

Con PowerShell e WSL, possiamo integrare i comandi Linux in Windows come applicazioni native. Non c'è bisogno di cercare build Win32 o strumenti Linux, né di interrompere il flusso di lavoro passando alla shell Linux. Basta installare WSL, configurare il profilo PowerShell e elencare i comandi che desideri importare! Un eccellente completamento automatico per le opzioni dei comandi e i percorsi file di Linux e Windows è una funzionalità che attualmente non è disponibile anche nei comandi nativi di Windows.

Il codice sorgente completo sopra descritto, insieme a ulteriori consigli su come integrarlo nel flusso di lavoro, è disponibile qui.

Quali comandi Linux trovi più utili? Cosa manca ancora per rendere il lavoro in Windows più familiare? Scrivi nei commenti oppure su GitHub!

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster