Avatar

Mike

26 settembre, 2019

Una domanda comune degli sviluppatori Windows è “perché Windows non ha ancora <INSERT FAVORITE LINUX COMMAND HERE>?”. Sia che desiderino un pager potente come less o che vogliano usare comandi familiari come grep o sed, gli sviluppatori Windows desiderano un facile accesso a questi comandi come parte del loro flusso di lavoro principale.

Il Windows Subsystem for Linux (WSL) è stato un enorme passo avanti in questo senso, consentendo agli sviluppatori di richiamare i comandi Linux da Windows facendoli passare attraverso wsl.exe (ad esempio wsl ls). Pur essendo un miglioramento significativo, l’esperienza è carente in diversi modi:

  • Il prefisso dei comandi con wsl è noioso e innaturale
  • I percorsi Windows passati come argomenti spesso non si risolvono a causa dei backslash che vengono interpretati come caratteri di escape piuttosto che come separatori di directory
  • I percorsi Windows passati come argomenti spesso non si risolvono a causa della mancata traduzione nel punto di punto di montaggio appropriato all’interno della WSL
  • I parametri predefiniti definiti nei profili di login WSL con alias e variabili d’ambiente non vengono onorati
  • Il completamento del percorso Linux non è supportato
  • Il completamento dei comandi non è supportato
  • Il completamento degli argomenti non è supportato

Il risultato di queste carenze è che i comandi Linux si sentono come cittadini di secondadi seconda classe rispetto a Windows e sono più difficili da usare di quanto dovrebbero essere. Affinché un comando si senta come un comando nativo di Windows, dovremo affrontare questi problemi.

Wrapper di funzioni PowerShell

Possiamo rimuovere la necessità di prefissare i comandi con wsl, gestire la traduzione dei percorsi di Windows in percorsi WSL e supportare il completamento dei comandi con wrapper di funzioni PowerShell. I requisiti di base dei wrapper sono:

  • Ci dovrebbe essere un wrapper di funzione per ogni comando Linux con lo stesso nome del comando
  • Il wrapper dovrebbe riconoscere i percorsi Windows passati come argomenti e tradurli in percorsi WSL
  • Il wrapper dovrebbe invocare wsl con il corrispondente comando Linux, convogliando qualsiasi input della pipeline e passando qualsiasi argomento della linea di comando passato alla funzione

Siccome questo template può essere applicato a qualsiasi comando, possiamo astrarre la definizione di questi wrapper e generarli dinamicamente da una lista 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 Ignorefunction 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 -IsAbsolute -ErrorAction Ignore) { `$args = Format-WslArgument (wsl.exe wslpath (`$args -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 -ErrorAction Ignore) { `$args = Format-WslArgument (`$args -replace "\\", "/") } } if (`$input.MoveNext()) { `$input.Reset() `$input | wsl.exe $_ (`$args -split ' ') } else { wsl.exe $_ (`$args -split ' ') }}"@}

La lista $command definisce i comandi da importare. Poi generiamo dinamicamente il wrapper della funzione per ognuno usando il comando Invoke-Expression (rimuovendo prima qualsiasi alias che andrebbe in conflitto con la funzione).

La funzione esegue un loop attraverso gli argomenti della riga di comando, identifica i percorsi di Windows usando i comandi Split-Path e Test-Path, poi converte questi percorsi in percorsi WSL. Facciamo passare i percorsi attraverso una funzione helper che definiremo più avanti chiamata Format-WslArgument che sfugge ai caratteri speciali come spazi e parentesi che altrimenti verrebbero male interpretati.

Infine, passiamo l’input della pipeline e qualsiasi argomento della linea di comando attraverso wsl.

Con questi wrapper di funzioni in atto, possiamo ora chiamare i nostri comandi Linux preferiti in modo più naturale senza doverli prefissare con wsl o preoccuparci di come i percorsi di Windows siano tradotti in percorsi WSL:

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

Un set iniziale di comandi è mostrato qui, ma è possibile generare un wrapper per qualsiasi comando Linux semplicemente aggiungendolo alla lista. Se aggiungi questo codice al tuo profilo PowerShell, questi comandi saranno disponibili in ogni sessione PowerShell proprio come i comandi nativi!

Parametri di default

È comune in Linux definire alias e/o variabili d’ambiente all’interno dei profili di login per impostare parametri di default per i comandi che usi frequentemente (ad esempio alias ls=ls -AFh o export LESS=-i). Uno degli svantaggi del proxy attraverso una shell non interattiva tramite wsl.exe è che i profili di login non vengono caricati, quindi questi parametri predefiniti non sono disponibili (es. ls all’interno di WSL e wsl ls si comporterebbe diversamente con l’alias definito sopra).

PowerShell fornisce $PSDefaultParameterValues, un meccanismo standard per definire i valori dei parametri predefiniti, ma solo per cmdlets e funzioni avanzate. Trasformare i nostri function wrapper in funzioni avanzate è possibile, ma introduce delle complicazioni (ad esempio PowerShell abbina nomi di parametri parziali (come abbinare -a per -ArgumentList) che andranno in conflitto con i comandi Linux che accettano i nomi parziali come argomenti), e la sintassi per definire i valori di default sarebbe meno che ideale per questo scenario (richiedendo il nome di un parametro nella chiave per definire gli argomenti di default invece del solo nome del comando).

Con una piccola modifica ai nostri wrapper di funzione, possiamo introdurre un modello simile a $PSDefaultParameterValues e abilitare i parametri predefiniti per i comandi Linux!

function global:$_() { … `$defaultArgs = ((`$WslDefaultParameterValues.$_ -split ' '), "") if (`$input.MoveNext()) { `$input.Reset() `$input | wsl.exe $_ `$defaultArgs (`$args -split ' ') } else { wsl.exe $_ `$defaultArgs (`$args -split ' ') }}

Passando $WslDefaultParameterValues nella linea di comando che inviamo attraverso wsl.exe, è ora possibile aggiungere dichiarazioni come questa al vostro profilo PowerShell per configurare parametri predefiniti!

$WslDefaultParameterValues = "-E"$WslDefaultParameterValues = "-i"$WslDefaultParameterValues = "-AFh --group-directories-first" 

Siccome questo è modellato dopo $PSDefaultParameterValues, potete disabilitarli temporaneamente facilmente impostando la chiave "Disabled" a $true. Una tabella hash separata ha l’ulteriore vantaggio di poter disabilitare $WslDefaultParameterValues separatamente da $PSDefaultParameterValues.

Completamento degli argomenti

PowerShell permette di registrare completatori di argomenti con il comando Register-ArgumentCompleter. Bash ha potenti strutture di completamento programmabili. WSL permette di chiamare in bash da PowerShell. Se possiamo registrare dei completatori di argomenti per i nostri wrapper di funzioni PowerShell e chiamare bash per generare i completamenti, possiamo ottenere un ricco completamento di argomenti con la stessa fedeltà di 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.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.Extent if ($cursorPosition -lt $extent.EndColumnNumber) { # The cursor is in the middle of a word to complete. $previousWord = $commandAst.CommandElements.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.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.Extent $previousExtent = $commandAst.CommandElements.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.Extent.Text $COMP_CWORD -= 1 } # Build the command to pass to WSL. $command = $commandAst.CommandElements.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 + $_) $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 ::new($completionText, $listItemText, 'ParameterName', $completionText) }}# Helper function to escape characters in arguments passed to WSL that would otherwise be misinterpreted.function global:Format-WslArgument($arg, $interactive) { if ($interactive -and $arg.Contains(" ")) { return "'$arg'" } else { return ($arg -replace " ", "\ ") -replace "()", ('\$1', '`$1') }}

Il codice è un po’ denso senza una comprensione di alcuni elementi interni di bash, ma fondamentalmente:

  • Registriamo il completatore di argomenti per tutti i nostri wrapper di funzioni passando la lista $commands al parametro -CommandName di Register-ArgumentCompleter
  • Mappiamo ogni comando alla funzione di shell che bash usa per completarlo ($F che prende il nome da complete -F <FUNCTION> usato per definire le specifiche di completamento in bash)
  • Convertiamo il $wordToComplete di PowerShell, $commandAst, e $cursorPosition argomenti di PowerShell nel formato previsto dalle funzioni di completamento di bash secondo la specifica di completamento programmabile di bash
  • Costruiamo una linea di comando che possiamo passare a wsl.exe che assicura che l’ambiente di completamento sia impostato correttamente, invoca la funzione di completamento appropriata, quindi emette una stringa contenente i risultati del completamento separati da nuove linee
  • Invochiamo quindi wsl con la linea di comando, dividiamo la stringa di output sul nuovo separatore di linea, poi generiamo CompletionResults per ognuno di essi, ordinandoli, e facendo l’escape di caratteri come spazi e parentesi che altrimenti sarebbero mal interpretati

Il risultato finale di questo è che ora i nostri wrapper di comando Linux useranno lo stesso esatto completamento che usa bash! Per esempio:

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

Ogni completamento fornirà valori specifici all’argomento che lo precede, leggendo i dati di configurazione come gli host conosciuti all’interno della WSL!

<TAB> passerà in rassegna le opzioni. <Ctrl + Space> mostrerà tutte le opzioni disponibili.

Inoltre, poiché il completamento bash è ora in carica, è possibile risolvere i percorsi Linux direttamente all’interno di PowerShell!

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

Nei casi in cui il completamento di bash non restituisce alcun risultato, PowerShell ritorna al suo completamento predefinito che risolverà i percorsi di Windows, permettendovi effettivamente di risolvere sia i percorsi Linux che quelli Windows a piacimento.

Conclusione

Con PowerShell e WSL, possiamo integrare i comandi Linux in Windows proprio come se fossero applicazioni native. Non c’è bisogno di andare in giro a cercare le build Win32 delle utility Linux o essere costretti a interrompere il flusso di lavoro per entrare in una shell Linux. Basta installare WSL, impostare il tuo profilo PowerShell, ed elencare i comandi che vuoi importare! Il ricco completamento degli argomenti mostrato qui di entrambe le opzioni di comando e dei percorsi dei file Linux e Windows è un’esperienza che nemmeno i comandi nativi di Windows forniscono oggi.

Il codice sorgente completo descritto sopra così come la guida aggiuntiva per incorporarlo nel tuo flusso di lavoro è disponibile a https://github.com/mikebattista/PowerShell-WSL-Interop.

Quali comandi Linux trovi più utili? Quali altre parti del tuo flusso di lavoro di sviluppatore trovi carenti su Windows?

Facci sapere nei commenti qui sotto o su GitHub!

Avatar
Mike Battista

Senior Program Manager, Windows Developer Platform

Segui

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *