.

Avatar

Mike

Le 26 septembre, 2019

Une question courante des développeurs Windows est  » pourquoi Windows n’a pas encore <INSERT FAVORITE LINUX COMMAND HERE> ? ». Qu’ils aspirent à un pager puissant comme less ou qu’ils veuillent utiliser des commandes familières comme grep ou sed, les développeurs Windows souhaitent un accès facile à ces commandes dans le cadre de leur flux de travail principal.

Le sous-système Windows pour Linux (WSL) a constitué une avancée considérable à cet égard, permettant aux développeurs d’appeler des commandes Linux depuis Windows en les mandatant par wsl.exe (par exemple wsl ls). Bien qu’il s’agisse d’une amélioration significative, l’expérience manque à plusieurs égards :

  • Préfixer les commandes avec wsl est fastidieux et peu naturel
  • Les chemins Windows passés en tant qu’arguments ne sont pas souvent résolus en raison des barres obliques inversées qui sont interprétées comme des caractères d’échappement plutôt que des séparateurs de répertoire
  • Les chemins Windows passés en tant qu’arguments ne sont pas souvent résolus parce qu’ils ne sont pas traduits vers le point de montage approprié. point de montage approprié au sein de WSL
  • Les paramètres par défaut définis dans les profils de connexion WSL avec les alias et les variables d’environnement ne sont pas honorés
  • La complétion de chemin Linux n’est pas prise en charge
  • La complétion de commande n’est pas prise en charge
  • La complétion d’argument n’est pas prise en charge

Le résultat de ces lacunes est que les commandes Linux se sentent comme des citoyens de secondecitoyens de seconde classe par rapport à Windows et sont plus difficiles à utiliser qu’elles ne devraient l’être. Pour qu’une commande se sente comme une commande Windows native, nous devrons résoudre ces problèmes.

PowerShell Function Wrappers

Nous pouvons supprimer le besoin de préfixer les commandes avec wsl, gérer la traduction des chemins Windows en chemins WSL et prendre en charge la complétion des commandes avec des wrappers de fonctions PowerShell. Les exigences de base des wrappers sont :

  • Il devrait y avoir un wrapper de fonction par commande Linux avec le même nom que la commande
  • Le wrapper devrait reconnaître les chemins Windows passés comme arguments et les traduire en chemins WSL
  • Le wrapper devrait invoquer wsl avec la commande Linux correspondante, en canalisant toute entrée de pipeline et en transmettant tout argument de ligne de commande passé à la fonction

Puisque ce modèle peut être appliqué à n’importe quelle commande, nous pouvons abstraire la définition de ces wrappers et les générer dynamiquement à partir d’une liste de commandes à importer.

# 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 liste $command définit les commandes à importer. Ensuite, nous générons dynamiquement le wrapper de fonction pour chacune d’entre elles à l’aide de la commande Invoke-Expression (en supprimant d’abord tous les alias qui entreraient en conflit avec la fonction).

La fonction boucle sur les arguments de la ligne de commande, identifie les chemins Windows à l’aide des commandes Split-Path et Test-Path, puis convertit ces chemins en chemins WSL. Nous faisons passer les chemins par une fonction d’aide que nous définirons plus tard, appelée Format-WslArgument, qui échappe les caractères spéciaux comme les espaces et les parenthèses qui seraient autrement mal interprétés.

Enfin, nous faisons passer l’entrée du pipeline et tout argument de ligne de commande par wsl.

Avec ces enveloppes de fonctions en place, nous pouvons maintenant appeler nos commandes Linux préférées de manière plus naturelle sans avoir à les préfixer avec wsl ou à nous soucier de la manière dont les chemins Windows sont traduits en chemins WSL :

  • man bash
  • less -i $profile.CurrentUserAllHosts
  • ls -Al C:\Windows\ | less
  • .

  • grep -Ein error *.log
  • tail -f *.log

Un jeu de commandes de démarrage est présenté ici, mais vous pouvez générer un wrapper pour n’importe quelle commande Linux en l’ajoutant simplement à la liste. Si vous ajoutez ce code à votre profil PowerShell, ces commandes seront à votre disposition dans chaque session PowerShell, tout comme les commandes natives !

Paramètres par défaut

Il est courant sous Linux de définir des alias et/ou des variables d’environnement au sein des profils de connexion afin de définir des paramètres par défaut pour les commandes que vous utilisez fréquemment (par exemple alias ls=ls -AFh ou export LESS=-i). L’un des inconvénients de la procuration à travers un shell non interactif via wsl.exe est que les profils de connexion ne sont pas chargés, de sorte que ces paramètres par défaut ne sont pas disponibles (ex. ls dans WSL et wsl ls se comporteraient différemment avec l’alias défini ci-dessus).

PowerShell fournit $PSDefaultParameterValues, un mécanisme standard pour définir les valeurs des paramètres par défaut, mais uniquement pour les cmdlets et les fonctions avancées. Transformer nos wrappers de fonctions en fonctions avancées est possible mais introduit des complications (par exemple, PowerShell fait correspondre des noms de paramètres partiels (comme faire correspondre -a pour -ArgumentList), ce qui entrera en conflit avec les commandes Linux qui acceptent les noms partiels comme arguments), et la syntaxe pour définir les valeurs par défaut serait moins qu’idéale pour ce scénario (nécessitant le nom d’un paramètre dans la clé pour définir les arguments par défaut par opposition au simple nom de la commande).

Avec une petite modification de nos wrappers de fonctions, nous pouvons introduire un modèle similaire à $PSDefaultParameterValues et activer les paramètres par défaut pour les commandes Linux !

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

En passant $WslDefaultParameterValues en bas dans la ligne de commande que nous envoyons par wsl.exe, vous pouvez maintenant ajouter des déclarations comme ci-dessous à votre profil PowerShell pour configurer les paramètres par défaut !

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

Comme cela est modelé après $PSDefaultParameterValues, vous pouvez les désactiver temporairement facilement en définissant la clé "Disabled"$true. Une table de hachage séparée présente l’avantage supplémentaire de pouvoir désactiver $WslDefaultParameterValues séparément de $PSDefaultParameterValues.

Complétion d’arguments

PowerShell vous permet d’enregistrer des compléteurs d’arguments avec la commande Register-ArgumentCompleter. Bash dispose de puissantes facilités de complétion programmables. WSL vous permet d’appeler dans bash depuis PowerShell. Si nous pouvons enregistrer des compléteurs d’arguments pour nos wrappers de fonctions PowerShell et faire appel à bash pour générer les complétions, nous pouvons obtenir une complétion d’arguments riche avec la même fidélité que dans bash lui-même !

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

Le code est un peu dense sans une compréhension de certains internes de bash, mais en gros :

  • Nous enregistrons le compléteur d’arguments pour tous nos wrappers de fonctions en passant la liste $commands au paramètre -CommandName de Register-ArgumentCompleter
  • Nous faisons correspondre chaque commande à la fonction shell que bash utilise pour la compléter ($F qui est nommée d’après complete -F <FUNCTION> utilisée pour définir les spécifications de complétion dans bash)
  • Nous convertissons la $wordToComplete de PowerShell, $commandAst, et $cursorPosition arguments dans le format attendu par les fonctions de complétion bash selon la spécification de complétion programmable bash
  • Nous construisons une ligne de commande que nous pouvons passer à wsl.exe qui assure que l’environnement de complétion est configuré correctement, invoque la fonction de complétion appropriée, puis sort une chaîne contenant les résultats de la complétion séparés par de nouvelles lignes
  • Nous invoquons ensuite wsl avec la ligne de commande, divise la chaîne de sortie sur le nouveau séparateur de ligne, puis générons CompletionResults pour chacun, en les triant et en échappant les caractères comme les espaces et les parenthèses qui seraient autrement mal interprétés

Le résultat final de ceci est que maintenant nos wrappers de commande Linux utiliseront exactement la même complétion que celle utilisée par bash ! Par exemple:

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

Chaque complétion fournira des valeurs spécifiques à l’argument qui la précède, en lisant les données de configuration comme les hôtes connus au sein de WSL!

<TAB> fera défiler les options. <Ctrl + Space> montrera toutes les options disponibles.

En outre, puisque la complétion bash est maintenant en charge, vous pouvez résoudre les chemins Linux directement dans PowerShell !

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

Dans les cas où la complétion bash ne renvoie aucun résultat, PowerShell se rabat sur sa complétion par défaut qui résoudra les chemins Windows, ce qui vous permet effectivement de résoudre à volonté les chemins Linux et les chemins Windows.

Conclusion

Avec PowerShell et WSL, nous pouvons intégrer des commandes Linux dans Windows comme s’il s’agissait d’applications natives. Plus besoin de chercher des versions Win32 d’utilitaires Linux ou d’être obligé d’interrompre son flux de travail pour se plonger dans un shell Linux. Il suffit d’installer WSL, de configurer votre profil PowerShell et de lister les commandes que vous souhaitez importer ! La riche complétion d’arguments montrée ici, à la fois des options de commande et des chemins de fichiers Linux et Windows, est une expérience que même les commandes Windows natives ne fournissent pas aujourd’hui.

Le code source complet décrit ci-dessus ainsi que des conseils supplémentaires pour l’incorporer dans votre flux de travail sont disponibles à l’adresse https://github.com/mikebattista/PowerShell-WSL-Interop.

Quelles commandes Linux trouvez-vous les plus utiles ? Quelles sont les autres parties de votre flux de travail de développeur qui vous font défaut sous Windows ?

Laissez-nous savoir dans les commentaires ci-dessous ou sur GitHub !

Avatar
Mike Battista

Responsable de programme senior, Windows Developer Platform

Suivre

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *