Une question typique des développeurs sous Windows : « Pourquoi n'y a-t-il toujours pas ?». Будь то мощное пролистывание less ou des outils familiers grep ou sed, les développeurs sous Windows souhaitent un accès facile à ces commandes dans leur travail quotidien.
a fait un grand pas en avant à cet égard. Il permet d'appeler des commandes Linux depuis Windows en les faisant passer par wsl.exe (par exemple, wsl ls). Bien que ce soit une amélioration significative, cette option souffre de plusieurs inconvénients.
- L'ajout généralisé
wslest pénible et non naturel. - Les chemins 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 Windows dans les arguments ne sont pas traduits en points de montage appropriés dans WSL.
- Les paramètres par défaut dans les profils WSL avec des alias et des variables d'environnement ne sont pas pris en compte.
- L'achèvement des chemins Linux n'est pas pris en charge.
- L'achèvement des commandes n'est pas pris en charge.
- L'achèvement des arguments n'est pas pris en charge.
En conséquence, les commandes Linux sont perçues sous Windows comme des citoyens de seconde zone - et il est plus difficile de les utiliser que les commandes natives. Pour les mettre sur un pied d'égalité, il faut résoudre les problèmes énumérés.
Les fonctions de shell PowerShell
Avec les fonctions de shell PowerShell, nous pouvons ajouter l'auto-complétion des commandes et éliminer la nécessité de préfixes wsl, en traduisant les chemins Windows en chemins WSL. Les exigences principales pour les fonctions de shell sont :
- Pour chaque commande Linux, il doit y avoir une fonction de shell avec le même nom.
- La fonction doit reconnaître les chemins Windows fournis en tant qu'arguments et les convertir en chemins WSL.
- La fonction doit appeler
wslavec la commande Linux appropriée à chaque entrée du pipeline et passer tous les arguments de ligne de commande fournis à la fonction.
Puisque ce modèle peut être appliqué à n'importe quelle commande, nous pouvons abstraire la définition de ces fonctions 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 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. Nous générons ensuite dynamiquement un wrapper de fonction pour chacune d'elles en utilisant la commande Invoke-Expression (en supprimant d'abord tous les alias qui pourraient entrer en conflit avec la fonction).
La fonction parcourt les arguments de ligne de commande, identifie les chemins Windows avec les commandes Split-Path et Test-Path, puis convertit ces chemins en chemins WSL. Nous passons les chemins par une fonction auxiliaire Format-WslArgument, que nous définirons plus tard. Elle échappe les caractères spéciaux, tels que les espaces et les parenthèses, qui seraient autrement mal interprétés.
Enfin, nous passons wsl les entrées du pipeline et tous les arguments de la ligne de commande.
Avec de tels wrappers, il est possible d'appeler vos commandes Linux préférées de manière plus naturelle, sans ajouter de préfixe wsl et sans se soucier de la conversion des chemins :
man bashless -i $profile.CurrentUserAllHostsls -Al C:Windows | lessgrep -Ein error *.logtail -f *.log
Voici un ensemble de commandes de base, mais vous pouvez créer un wrapper pour n'importe quelle commande Linux, il suffit de l'ajouter à la liste. Si vous ajoutez ce code à votre PowerShell, ces commandes vous seront accessibles dans chaque session PowerShell, tout comme les commandes natives !
Paramètres par défaut
Sous Linux, il est courant de définir des alias et/ou des variables d'environnement dans les profils (login profile), en spécifiant des paramètres par défaut pour les commandes fréquemment utilisées (par exemple, alias ls=ls -AFh ou export LESS=-i). L'un des inconvénients du proxy via un shell non interactif wsl.exe est que les profils ne se chargent pas, donc ces paramètres par défaut ne sont pas disponibles (c'est-à-dire que ls dans WSL et wsl ls se comporteront différemment avec l'alias défini ci-dessus).
PowerShell fournit , un mécanisme standard pour définir des paramètres par défaut, mais uniquement pour les cmdlets et les fonctions avancées. Bien sûr, il est possible de transformer nos shells en fonctions avancées, mais cela introduit des complications supplémentaires (ainsi, PowerShell fait correspondre des noms de paramètres partiels (par exemple, -a correspond à -ArgumentList), ce qui pourrait entrer en conflit avec des commandes Linux acceptant des 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 des arguments par défaut, il faut le nom du paramètre dans la clé, et non seulement le nom de la commande).
Cependant, avec quelques modifications de nos shells, nous pouvons intégrer un modèle similaire $PSDefaultParameterValues, et inclure des 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 passant $WslDefaultParameterValues dans l'invite de commandes, nous envoyons des paramètres via wsl.exe. Ci-dessous, il est montré comment ajouter des instructions au 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" Puisque les paramètres sont modélisés après $PSDefaultParameterValues, vous pouvez temporairement en définissant la clé "Disabled" à la valeur $true. Un autre avantage d'une table de hachage distincte est la possibilité de désactiver $WslDefaultParameterValues indépendamment de $PSDefaultParameterValues.
L'achèvement automatique des arguments
PowerShell permet d'enregistrer des compléteurs d'arguments à l'aide de la commande Register-ArgumentCompleter. Bash a des moyens programmables puissants pour l'achèvement automatique. Le code est un peu dense sans comprendre certaines fonctions internes de bash, mais en gros, nous faisons ceci :
# 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]
}
}Nous enregistrons le compléteur d'arguments pour toutes nos enveloppes de fonctions en passant la liste
- $commands
au paramètre-CommandNameNous faisons correspondre chaque commande avec la fonction d’enveloppe que bash utilise pour l'achèvement automatique (pour déterminer les spécifications d'achèvement automatique dans bash, utiliserpourRegister-ArgumentCompleter. - $F
, abréviation decomplete -FNous transformons les arguments PowerShell). - $wordToComplete
$commandAst,$cursorPositionetau format attendu par les fonctions d'achèvement automatique bash selon les spécificationsde l'achèvement automatique programmable Nous formons une ligne de commande à transmettre à - , qui configure correctement l'environnement, appelle la fonction d'achèvement automatique correspondante et affiche les résultats en les séparant par ligne.
wsl.exeNous appelons ensuite - avec la ligne de commande, séparons la sortie par des délimiteurs de ligne et générons pour chaque
wslCompletionResults, en les triant et en échappant des caractères tels que les espaces et les parenthèses qui sinon seraient mal interprétés.En conséquence, nos enveloppes de commande Linux utiliseront exactement le même achèvement automatique que dans bash ! Par exemple :
ssh -c -J -m -O -o -Q -w -b
ssh -c -J -m -O -o -Q -w -b
Chaque autocomplétion fournit des valeurs spécifiques au précédent argument en lisant des données de configuration, telles que les hôtes connus, depuis WSL !
<TAB> itérera à travers les paramètres de manière cyclique. <Ctrl + пробел> affichera toutes les options disponibles.
De plus, puisque l'autocomplétion bash fonctionne maintenant, vous pouvez autocompléter des chemins Linux directement dans PowerShell !
less /etc/ls /usr/share/vim ~/ .bash
Dans les cas où l'autocomplétion bash ne donne aucun résultat, PowerShell revient au système par défaut avec les chemins Windows. Ainsi, vous pouvez en pratique utiliser à la fois les chemins Linux et Windows comme bon vous semble.
Conclusion
Avec PowerShell et WSL, nous pouvons intégrer des commandes Linux dans Windows comme des applications natives. Pas besoin de chercher des builds Win32 ou des utilitaires Linux ou d'interrompre le flux de travail en passant au shell Linux. Il suffit de , configurer et ! Une autocomplétion riche pour les paramètres des commandes et les chemins de fichiers Linux et Windows est une fonctionnalité qui n'est même pas présente dans les commandes Windows natives aujourd'hui.
Le code source complet décrit ci-dessus, ainsi que des recommandations supplémentaires pour l'intégrer dans votre flux de travail, sont disponibles .
Quelles commandes Linux trouvez-vous les plus utiles ? Quelles autres commodités manquez-vous lors de l'utilisation de Windows ? Écrivez vos commentaires ou !
Source : habr.com
