Intégration de commandes Linux dans Windows à l'aide de PowerShell et WSL

Une question typique des développeurs Windows : « Pourquoi n'y a-t-il toujours pas <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?. Qu'il s'agisse d'un coup puissant less ou des outils familiers grep ou sed, les développeurs Windows souhaitent accéder facilement à ces commandes dans leur travail quotidien.

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

  • Ajout omniprésent wsl fastidieux et contre nature.
  • 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 plutôt que comme des séparateurs de répertoires.
  • Les chemins Windows dans les arguments ne sont pas traduits vers le point de montage correspondant dans WSL.
  • Les paramètres par défaut ne sont pas respectés dans les profils WSL avec alias et variables d'environnement.
  • La complétion du chemin Linux n'est pas prise en charge.
  • L'achèvement des commandes n'est pas pris en charge.
  • La complétion des arguments n’est pas prise en charge.

En conséquence, les commandes Linux sont traitées comme des citoyens de seconde zone sous Windows et sont plus difficiles à utiliser que les commandes natives. Pour égaliser leurs droits, il est nécessaire de résoudre les problèmes énumérés.

Wrappers de fonctions PowerShell

Avec les wrappers de fonctions PowerShell, nous pouvons ajouter la complétion des commandes et éliminer le besoin de préfixes wsl, traduisant les chemins Windows en chemins WSL. Exigences de base pour les coques :

  • Pour chaque commande Linux, il doit y avoir un wrapper de fonction portant le même nom.
  • Le shell doit reconnaître les chemins Windows passés en arguments et les convertir en chemins WSL.
  • Le shell devrait appeler wsl avec la commande Linux appropriée à n’importe quelle entrée de pipeline et en transmettant tous les arguments de ligne de commande transmis à la fonction.

Puisque ce modèle peut être appliqué à n’importe quelle commande, nous pouvons faire abstraction de 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 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 ' ')
    }
}
"@
}

Liste $command définit les commandes d'importation. Nous générons ensuite dynamiquement un wrapper de fonction pour chacun d'eux à l'aide de 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, détermine les chemins Windows à l'aide de commandes Split-Path и Test-Pathpuis convertit ces chemins en chemins WSL. Nous parcourons les chemins via une fonction d'assistance Format-WslArgument, que nous définirons plus tard. Il échappe aux caractères spéciaux tels que les espaces et les parenthèses qui autrement seraient mal interprétés.

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

Avec ces wrappers, vous pouvez 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 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

L'ensemble de commandes de base est présenté 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 à 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 de connexion, en définissant 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 - que les profils ne sont pas chargés, donc ces options ne sont pas disponibles par défaut (c'est à dire ls en WSL et wsl ls se comportera 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, nous pouvons créer des fonctions avancées à partir de nos shells, mais cela introduit des complications inutiles (par exemple, PowerShell corrèle les noms de paramètres partiels (par exemple, -a est en corrélation avec -ArgumentList), qui entrera en conflit avec les commandes Linux qui prennent des noms partiels comme arguments), et la syntaxe de définition des valeurs par défaut ne sera pas la plus appropriée (les arguments par défaut nécessitent le nom du paramètre dans la clé, 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 activez 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 ' ')
    }
}

Qui passe $WslDefaultParameterValues à la ligne de commande, nous envoyons des paramètres via wsl.exe. Ce qui suit montre comment ajouter des instructions à votre 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 d’après $PSDefaultParameterValues, Vous pouvez c'est facile de les désactiver temporairement en installant la clé "Disabled" dans le sens $true. Un avantage supplémentaire d'une table de hachage séparée est la possibilité de désactiver $WslDefaultParameterValues en dehors de $PSDefaultParameterValues.

Achèvement des arguments

PowerShell vous permet d'enregistrer les fins d'arguments à l'aide de la commande Register-ArgumentCompleter. Bash a un puissant outils d'auto-complétion programmables. WSL vous permet d'appeler bash depuis PowerShell. Si nous pouvons enregistrer les complétions d’arguments pour nos wrappers de fonctions PowerShell et appeler bash pour générer les complétions, nous obtiendrons la complétion complète des arguments avec la même précision que 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 dense sans comprendre certaines fonctions internes de bash, mais en gros ce que nous faisons est ceci :

  • Enregistrer un complément d'argument pour tous nos wrappers de fonctions en passant une liste $commands en paramètre -CommandName pour Register-ArgumentCompleter.
  • Nous mappons chaque commande à la fonction shell que bash utilise pour l'auto-complétion (pour définir les spécifications d'auto-complétion, bash utilise $F, abréviation de complete -F <FUNCTION>).
  • Conversion d'arguments PowerShell $wordToComplete, $commandAst и $cursorPosition dans le format attendu par les fonctions d'auto-complétion de bash selon les spécifications complétion automatique programmable frapper.
  • Nous composons une ligne de commande vers laquelle transférer wsl.exe, qui garantit que l'environnement est correctement configuré, appelle la fonction d'auto-complétion appropriée et affiche les résultats ligne par ligne.
  • Ensuite, nous appelons wsl avec la ligne de commande, nous séparons la sortie par des séparateurs de ligne et générons pour chaque CompletionResults, en les triant et en échappant aux caractères tels que les espaces et les parenthèses qui autrement seraient mal interprétés.

En conséquence, nos shells de commandes Linux utiliseront exactement la même saisie semi-automatique que bash ! Par exemple:

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

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

<TAB> fera défiler les paramètres. <Ctrl + пробел> affichera toutes les options disponibles.

De plus, puisque nous avons désormais la saisie semi-automatique bash, 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ù la saisie semi-automatique bash ne produit aucun résultat, PowerShell revient aux chemins Windows par défaut du système. Ainsi, en pratique, vous pouvez utiliser simultanément les deux chemins à votre guise.

Conclusion

Grâce à 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 des versions Win32 ou des utilitaires Linux ou d'interrompre votre flux de travail en accédant à un shell Linux. Juste installer WSL, configurez Profil PowerShell и listez les commandes que vous souhaitez importer! La saisie semi-automatique riche pour les paramètres de commande et les chemins de fichiers Linux et Windows est une fonctionnalité qui n'est même pas disponible dans les commandes Windows natives aujourd'hui.

Le code source complet décrit ci-dessus, ainsi que des directives supplémentaires pour l'intégrer dans votre flux de travail, sont disponibles ici.

Quelles commandes Linux trouvez-vous les plus utiles ? Quelles autres choses courantes manquent lorsque vous travaillez sous Windows ? Écrivez dans les commentaires ou sur GitHub!

Source: habr.com

Ajouter un commentaire