Intégrez les commandes Linux dans Windows avec PowerShell et WSL

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.

Le Sous-système Windows pour Linux (WSL) 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é wsl est 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 wsl avec 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 bash
  • less -i $profile.CurrentUserAllHosts
  • ls -Al C:Windows | less
  • grep -Ein error *.log
  • tail -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 profil 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 $PSDefaultParameterValues, 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 facilement les désactiver 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. WSL permet d'appeler bash depuis PowerShell. Si nous pouvons enregistrer des compléteurs d'arguments pour nos fonctions PowerShell et appeler bash pour générer des complétions, nous obtiendrons un achèvement automatique des arguments avec la même précision que dans bash lui-même !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 -CommandName Nous 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, utiliser pour Register-ArgumentCompleter.
  • $F , abréviation decomplete -F Nous transformons les arguments PowerShell).
  • $wordToComplete $commandAst, $cursorPosition et au format attendu par les fonctions d'achèvement automatique bash selon les spécifications de l'achèvement automatique programmable bash. 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 wsl CompletionResults , 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 installer WSL, configurer le profil PowerShell et énumérer les commandes que vous souhaitez importer! 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 ici.

Quelles commandes Linux trouvez-vous les plus utiles ? Quelles autres commodités manquez-vous lors de l'utilisation de Windows ? Écrivez vos commentaires ou sur GitHub!

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster