Nous intégrons des commandes Linux dans Windows à l'aide de PowerShell et WSL

Une question typique d'un développeur Windows: «Pourquoi n'est-il toujours pas ici < LINUX> ?». Qu'il s'agisse d'un défilement less puissant ou des outils grep ou sed habituels, les développeurs Windows souhaitent accéder facilement à ces commandes dans leur travail quotidien.

Le sous-système Windows pour Linux (WSL) a fait un grand pas en avant à cet égard. Il vous permet d'appeler des commandes Linux à partir de Windows, en les procurant par wsl.exe via wsl.exe (par exemple, wsl ls ). Bien qu'il s'agisse d'une amélioration significative, cette option souffre d'un certain nombre d'inconvénients.

  • L'ajout omniprésent de wsl fatigant et contre nature.
  • Les chemins d'accès Windows dans les arguments ne fonctionnent pas toujours, car les barres obliques inverses sont interprétées comme des caractères d'échappement et non comme des séparateurs de répertoires.
  • Les chemins d'accès Windows dans les arguments ne se traduisent pas au point de montage correspondant dans WSL.
  • Les paramètres par défaut des profils WSL avec alias et variables d'environnement ne sont pas pris en compte.
  • L'achèvement du chemin Linux n'est pas pris en charge.
  • L'achèvement de la commande n'est pas pris en charge.
  • La complétion d'argument n'est pas prise en charge.

En conséquence, les commandes Linux sont perçues sous Windows comme des citoyens de seconde classe - et elles sont plus difficiles à utiliser que les équipes natives. Pour égaliser leurs droits, vous devez résoudre ces problèmes.

Coques PowerShell


À l'aide des wrappers de fonctions PowerShell, nous pouvons ajouter l'achèvement de la commande et éliminer le besoin de préfixes wsl en traduisant les chemins Windows en chemins WSL. Exigences de base pour les coques:

  • Chaque commande Linux doit avoir un shell de la fonction avec le même nom.
  • Le shell doit reconnaître les chemins Windows passés comme arguments et les convertir en chemins WSL.
  • Le shell doit appeler wsl avec la commande Linux appropriée à n'importe quelle entrée de pipeline et transmettre tous les arguments de ligne de commande passés à la fonction.

Puisque ce modèle peut être appliqué à n'importe quelle commande, nous pouvons résumer la définition de ces shells et les générer dynamiquement à partir de la liste des 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 Ignore function global:$_() { for (`$i = 0; `$i -lt `$args.Count; `$i++) { # If a path is absolute with a qualifier (eg 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 ' ') } } "@ } 

La liste de $command définit les commandes à importer. Ensuite, nous générons dynamiquement un wrapper de fonction pour chacun d'eux en utilisant la commande Invoke-Expression (en supprimant d'abord tous les alias qui entreront en conflit avec la fonction).

La fonction parcourt les arguments de la ligne de commande, détermine les chemins d'accès Windows à l'aide des commandes Split-Path et Test-Path , puis convertit ces chemins d'accès en chemins WSL. Nous Format-WslArgument les chemins via la fonction d'assistance Format-WslArgument , que nous définirons plus tard. Il échappe aux caractères spéciaux, tels que les espaces et les crochets, qui seraient autrement mal interprétés.

Enfin, nous transmettons l'entrée du wsl et tous les arguments de ligne de commande à wsl .

En utilisant ces wrappers, vous pouvez appeler vos commandes Linux préférées de manière plus naturelle sans ajouter le préfixe wsl et sans vous soucier de la façon dont les chemins sont convertis:

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

Le jeu de commandes de base est montré ici, mais vous pouvez créer un shell pour n'importe quelle commande Linux en l'ajoutant simplement à la liste. Si vous ajoutez ce code à votre profil PowerShell, ces commandes seront disponibles pour vous dans chaque session PowerShell, tout comme les commandes natives!

Options par défaut


Sous Linux, il est habituel de définir des alias et / ou des variables d'environnement dans les profils (profil de connexion), en définissant les paramètres par défaut pour les commandes fréquemment utilisées (par exemple, l' alias ls=ls -AFh ou l' export LESS=-i ). L'un des inconvénients du proxy via le shell wsl.exe non interactif est que les profils ne sont pas chargés, par conséquent, ces options ne sont pas disponibles par défaut (c'est-à-dire que ls dans WSL et wsl ls se comporteront différemment avec l'alias défini ci-dessus).

PowerShell fournit $ PSDefaultParameterValues , un mécanisme standard pour définir les paramètres par défaut, mais uniquement pour les applets de commande et les fonctions avancées. Bien sûr, vous pouvez créer des fonctions avancées à partir de nos shells, mais cela introduit des complications inutiles (par exemple, PowerShell correspond aux noms de paramètres partiels (par exemple, -a correspond à -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 ne sera pas la plus appropriée (pour définir les arguments par défaut, le nom du paramètre dans la clé est requis, et pas seulement le nom de la commande).

Cependant, avec une légère modification de nos shells, nous pouvons implémenter un modèle similaire à $PSDefaultParameterValues et activer les options par défaut pour les commandes 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 ' ') } } 

En $WslDefaultParameterValues à la ligne de commande, nous envoyons les paramètres via wsl.exe . Ce qui suit montre comment ajouter des instructions à un profil PowerShell pour configurer les paramètres par défaut. Maintenant, nous pouvons le faire!

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

Étant donné que les paramètres sont modélisés d'après $PSDefaultParameterValues , vous pouvez facilement les désactiver temporairement en définissant la clé "Disabled" sur $true . Un avantage supplémentaire d'une table de hachage distincte est la possibilité de désactiver $WslDefaultParameterValues séparément de $PSDefaultParameterValues .

Achèvement de l'argument


PowerShell permet d'enregistrer les terminateurs d'arguments à l'aide de la commande Register-ArgumentCompleter . Bash dispose de puissants outils de complétion programmables . WSL vous permet d'appeler bash à partir de PowerShell. Si nous pouvons enregistrer les terminateurs d'arguments pour nos wrappers de fonctions PowerShell et appeler bash pour créer les terminaisons, alors nous obtenons l'achèvement complet des arguments avec la même précision 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[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] } } 

Le code est un peu serré sans comprendre certains des internes bash, mais en gros, nous faisons ce qui suit:

  • Nous enregistrons le finaliseur d'arguments pour tous nos wrappers de fonctions en passant la liste des $commands au paramètre -CommandName pour Register-ArgumentCompleter .
  • Nous mappons chaque commande à la fonction shell que bash utilise pour l'autocomplétion (bash utilise $F pour définir les spécifications d'autocomplétion, abréviation de complete -F <FUNCTION> ).
  • Convertissez les arguments PowerShell $wordToComplete , $commandAst et $cursorPosition au format attendu par les fonctions d'achèvement bash selon les spécifications d' achèvement programmables bash.
  • Nous wsl.exe la ligne de commande pour le transfert vers wsl.exe , qui garantit le paramètre d'environnement correct, appelle la fonction de saisie semi-automatique appropriée et affiche les résultats avec des sauts de ligne.
  • Ensuite, nous appelons wsl avec la ligne de commande, séparons la sortie avec des séparateurs de ligne et générons des CompletionResults pour chacun, en les triant et en échappant les caractères tels que les espaces et les crochets qui seraient autrement mal interprétés.

Par conséquent, nos interpréteurs de commandes Linux utiliseront exactement la même autocomplétion que dans bash! Par exemple:

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

Chaque saisie semi-automatique fournit des valeurs spécifiques à l'argument précédent, en lisant les données de configuration, telles que les hôtes connus, à partir de WSL!

<TAB> fera défiler les paramètres. <Ctrl + > affichera toutes les options disponibles.

De plus, comme la saisie semi-automatique de bash fonctionne maintenant avec nous, vous pouvez compléter automatiquement les chemins Linux directement dans PowerShell!

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

Dans les cas où l'autocomplétion bash échoue, PowerShell revient au système par défaut avec les chemins Windows. Ainsi, dans la pratique, vous pouvez utiliser simultanément ces moyens et d'autres à votre discrétion.

Conclusion


Avec PowerShell et WSL, nous pouvons intégrer des commandes Linux dans Windows en tant qu'applications natives. Il n'est pas nécessaire de rechercher les versions Win32 ou les utilitaires Linux ou d'interrompre le flux de travail en basculant vers le shell Linux. Installez simplement WSL , configurez votre profil PowerShell et répertoriez les commandes que vous souhaitez importer ! La riche saisie semi-automatique des paramètres de commande et de chemin d'accès pour les fichiers Linux et Windows est une fonctionnalité que l'on ne trouve même pas dans les commandes Windows natives aujourd'hui.

Le code source complet décrit ci-dessus, ainsi que des recommandations supplémentaires pour l'inclure dans le flux de travail sont disponibles ici .

Quelles commandes Linux trouvez-vous les plus utiles? Quelles sont les autres choses familières qui manquent lorsque vous travaillez sur Windows? Écrivez dans les commentaires ou sur GitHub !

Source: https://habr.com/ru/post/fr469257/


All Articles