«Avec PowerShell et WSL, nous pouvons intégrer des commandes Linux à Windows comme s'il s'agissait d'applications natives»,
selon un ingénieur Microsoft
Avec WSL 2, Microsoft a apporté une nouvelle version de l’architecture qui permet au sous-système Windows pour Linux d’exécuter des fichiers binaires ELF64 Linux sous Windows. Cette nouvelle architecture, qui utilise un véritable noyau Linux, modifie la façon dont ces binaires Linux interagissent avec Windows et le matériel de votre ordinateur, tout en offrant la même expérience utilisateur que dans WSL 1 (la version actuellement disponible en version stable). WSL 2 offre des performances de système de fichiers beaucoup plus rapides et une compatibilité totale des appels système, ce qui vous permet d'exécuter davantage d'applications comme Docker!
WSL 2 inclut donc un véritable noyau Linux qui vous permet d’exécuter davantage de logiciels Linux sous Windows et offrant de meilleures performances que WSL 1.
Cette nouvelle version de WSL 2 utilise les fonctionnalités Hyper-V pour créer une machine virtuelle légère avec un noyau Linux minimal. Il faut s’attendre à une meilleure compatibilité avec les logiciels Linux, y compris la prise en charge de Docker, et «une augmentation spectaculaire des performances du système de fichiers».
Mais Microsoft a voulu expliquer aux utilisateurs qu'ils pouvaient aller plus loin. Dans un billet de blog, Mike Battista, Senior Program Manager Windows Developer Platform, avance que:
«Une question récurrente chez les développeurs Windows est la suivante: "Pourquoi Windows n’a-t-il pas encore
Il a noté que le sous-système Windows pour Linux (WSL) constituait un énorme pas en avant en permettant aux développeurs d’appeler des commandes Linux à partir de Windows en les mettant en proxy par le biais de wsl.exe (par exemple, wsl ls). Cependant, il a relevé les limites de cette expérience à plusieurs niveaux, notamment:
«Le résultat de ces lacunes est que les commandes Linux se sentent comme des citoyens de deuxième classe vis-à-vis de Windows et sont plus difficiles à utiliser qu’elles ne devraient l’être. Pour qu'une commande ressemble à une commande Windows native, nous devons résoudre ces problèmes».
506224
Encapsuleurs de fonctions PowerShell
Mike affirme que nous pouvons supprimer le besoin de préfixer les commandes par wsl, gérer la traduction des chemins Windows en chemins WSL et prendre en charge la complétion des commandes avec les wrappers de fonctions PowerShell. Les exigences de base des wrapper sont les suivantes:
Ce modèle pouvant être appliqué à n’importe quelle commande, nous pouvons résumer la définition de ces wrappers et les générer de manière dynamique à 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 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 ' ')
}
}
"@
}
La liste $command définit les commandes à importer. Ensuite, nous générons dynamiquement l'encapsuleur de fonction pour chacune à l'aide de la commande Invoke-Expression (en supprimant tout d'abord les alias susceptibles d'entrer en conflit avec la fonction).
La fonction parcourt les arguments de ligne de commande, identifie les chemins Windows à l'aide des commandes Split-Path et Test-Path, puis convertit ces chemins en chemins WSL. Nous exécutons les chemins via une fonction d'assistance que nous définirons plus tard, appelée Format-WslArgument, qui permet d'échapper les caractères spéciaux tels que les espaces et les parenthèses qui seraient autrement mal interprétés.
Enfin, nous transmettons les entrées de pipeline et tous les arguments de ligne de commande à wsl.
Avec ces wrappers 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 ni à nous préoccuper de la façon dont les chemins Windows sont traduits en chemins WSL:
Un ensemble de commandes est présenté ici, mais vous pouvez générer un wrapper pour toute commande Linux simplement en l'ajoutant à la liste. Si vous ajoutez ce code à votre profil PowerShell, ces commandes seront disponibles dans chaque session PowerShell, tout comme les commandes natives!
Paramètres par défaut
Dans Linux, il est courant de définir des alias et/ou des variables d’environnement dans les profils de connexion pour définir les paramètres par défaut des commandes que vous utilisez fréquemment (par exemple, alias ls = ls -AFh ou export LESS = -i). L’un des inconvénients de la procuration par l’intermédiaire d’un shell non interactif via wsl.exe est que les profils de connexion ne sont pas chargés; ces paramètres par défaut ne sont donc pas disponibles (c’est-à-dire que ls dans WSL et wsl ls se comportaient différemment avec l’alias défini ci-dessus).
PowerShell fournit $PSDefaultParameterValues, un mécanisme standard permettant de définir les valeurs de paramètre par défaut, mais uniquement pour les applets de commande et les fonctions avancées. Convertir nos wrappers de fonctions en fonctions avancées est possible, mais introduit des complications (par exemple, PowerShell fait correspondre les noms de paramètres partiels - -a peut correspondre à -ArgumentList - ce qui entre en conflit avec les commandes Linux acceptant les noms partiels comme arguments), et la syntaxe permettant de définir les valeurs par défaut serait moins qu'idéal pour ce scénario (nécessitant le nom d'un paramètre dans la clé pour définir les arguments par défaut plutôt que le 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 ' '), "")[`$WslDefaultParameterValues.Disabled -eq `$true]
if (`$input.MoveNext()) {
`$input.Reset()
`$input | wsl.exe$_ `$defaultArgs (`$args -split ' ')
} else {
wsl.exe$_ `$defaultArgs (`$args -split ' ')
}
}
En transmettant $WslDefaultParameterValues à la ligne de commande que nous envoyons via wsl.exe, vous pouvez maintenant ajouter des instructions comme ci-dessous à votre profil PowerShell pour configurer les paramètres par défaut!
$WslDefaultParameterValues["grep"] = "-E"
$WslDefaultParameterValues["less"] = "-i"
$WslDefaultParameterValues["ls"] = "-AFh --group-directories-first"
Étant donné que ceci est modélisé selon $PSDefaultParameterValues, vous pouvez les désactiver temporairement facilement en définissant la clé "Disabled" sur$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 des arguments
PowerShell vous permet d'inscrire d'activer la complétion d'arguments à l'aide de la commande Register-ArgumentCompleter. Bash dispose de puissantes fonctions de complétion programmables. WSL vous permet d’appeler en bash depuis PowerShell. Si nous pouvons activer la complétion d'arguments pour nos wrappers de fonctions PowerShell et faire des appels bash pour générer les complétions, nous pouvons obtenir une complétion d'arguments enrichie 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[$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'/
$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.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
[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($arg, $interactive) {
if ($interactive -and$arg.Contains(" ")) {
return "'$arg'"
} else {
return ($arg -replace " ", "\ ") -replace "([()|])", ('\$1', '`$1')[$interactive]
}
}
En gros:
Le résultat final de ceci est maintenant que nos wrappers de commandes Linux utiliseront exactement la même finition que celle utilisée par bash! Par exemple:
ssh -c
Chaque complétion va fournir des valeurs spécifiques à l'argument précédent, lisant des données de configuration comme des hôtes connus à partir de WSL!
De plus, étant donné que la complétion bash est désormais supportée, vous pouvez résoudre les chemins Linux directement dans PowerShell!
Dans les cas où la complétion bash ne renvoie aucun résultat, PowerShell reprend sa complétion par défaut, ce qui résoudra les chemins Windows, ce qui vous permettra effectivement de résoudre à la fois les chemins Linux et Windows.
Mike conclut en disant que: «avec PowerShell et WSL, nous pouvons intégrer des commandes Linux à Windows comme s'il s'agissait d'applications natives. Nul besoin de chercher des versions Win32 d'utilitaires Linux ni d'être obligé d'interrompre votre flux de travail pour accéder à un shell Linux. Installez simplement WSL, configurez votre profil PowerShell et répertoriez les commandes que vous souhaitez importer! La complétion riche des arguments sur les options de commande et les chemins de fichiers Linux et Windows présentée ici est une expérience que même les commandes Windows natives ne fournissent pas aujourd’hui».
:fleche: code source complet de ce qui a été décrit ici ainsi qu'un guide pour vous en servir dans votre workflow
Source: Microsoft
Et vous?
:fleche: Avez-vous déjà souhaité intégrer des commandes Linux à Windows?
:fleche: Que pensez-vous de l'approche de cet ingénieur?
:fleche: Allez-vous l'essayer ou préférez-vous attendre la disponibilité d'un utilitaire spécifique qui va faire ce travail?
Voir aussi:
:fleche: Comment profiter de Linux sur Windows10 sans WSL sur le Microsoft Store avec Dark Moon?
:fleche: Des ingénieurs demandent à ce que soient ajoutés les opérateurs de type Bash && et || à PowerShell dans une RFC
:fleche: Microsoft publie le code source du noyau Linux léger utilisé dans WSL2 sous licence GPL version 2
:fleche: Docker Desktop pour Windows10 va bientôt passer à WSL2, quels sont les avantages pour les développeurs?
:fleche: Microsoft avance que la prochaine itération de PowerShell Core sera baptisée PowerShell 7, l'éditeur explique pourquoi il envisage de supprimer "Core"
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.