| Jacques's profilejanelPhotosBlogLists | Help |
|
June 20 Un petit ping pour la route?La commande ping marche sous PowerShell, alors pourquoi s'embêter à écrire un script pour reproduire son fonctionnement?
Au départ, je voulais une solution au problème suivant: comment puis-je tester ma connexion à Internet dans un script, et orienter la suite de mon script selon le résultat du test? La solution avec la commande ping standard passe par l'examen des lignes retournées par la commande. Par exemple, je pourrais faire:
set Internet=OK
ping %1|find "Reply"
if errorlevel=1 set Internet=NOK .. Cette méthode a le mérite de marcher... sur mon poste. Pour la plupart d'entre vous, elle ne marchera pas. La raison en est que vous avez sans doute un Windows en français, et les messages de la commande ping sur votre poste parlent sans doute de Réponse plutôt que de Reply. Il vous faudra donc tester votre propre commande ping et adapter le filtre find "Reply" au résultat que vous obtiendrez. Cela veut dire que la portabilité de ma solution est très moyenne dans un environnement multilingue.
De plus, si je veux récupérer d'autres infos au passage (comme le nom d'hôte correspondant à l'adresse testée, ou le temps de réponse) il va falloir multiplier les commandes d'analyse du résultat, alourdissant d'autant le script.
PowerShell a un accès direct aux objets .NET. Or dans le NET Framework il existe une classe System.Net.NetworkInformation.Ping qui permet aux applications de tester la présence sur le réseau d'un hôte distant. Cette classe a une méthode Send() qui correspond à ce que fait la commande ping. La méthode Send() retourne un objet System.Net.NetworkInformation.PingReply qui contient notamment la propriété Status correspondant au résultat de la requête.
En s'appuyant sur ces éléments on pourra donc écrire quelque chose comme ça:
switch ((test-host www.yahoo.com).status) {
"Success" { ... }
"TimedOut" { ... }
..
}
La page de MSDN sur l’énumération System.Net.NetworkInformation.IPStatus contient la liste complète des valeurs possibles pour la propriété Status.
La commande test-host www.yahoo.com est un appel à mon script, dont voici la version complète:
--- test-host.ps1
param (
[string]$remotehost, [int]$timeout = 120, [switch]$resolve, [int]$TTL = 128, [switch]$DontFragment, [int]$buffersize = 32 ) $options = new system.net.networkinformation.pingoptions
$options.TTL = $TTL $options.DontFragment = $DontFragment $buffer=([system.text.encoding]::ASCII).getbytes("a"*$buffersize) $ping = new system.net.networkinformation.ping $reply = $ping.Send($remotehost,$timeout,$buffer,$options) if ($resolve) { $hostname = ([System.Net.Dns]::GetHostEntry($remotehost)).hostname $reply = add-member -input $reply NoteProperty HostName $hostname -passthru } return $reply Ce script permet de passer tous les paramètres possibles à la méthode Send(), y compris les propriétés TTL et DontFragment de l'objet System.Net.NetworkInformation.PingOptions passé en argument de la méthode.
Je vais simplement m'arrêter sur un paramètre pour lequel on devra utiliser une autre classe et intégrer le résultat à l'objet retourné. Il s'agit du nom d'hôte. Cette information peut être utile lorsqu'on veut tester une adresse IP et savoir à quel nom DNS elle correspond. Le paramètre –resolve permet de forcer cette résolution DNS et d’inclure le résultat dans l’objet retourné. On pourra alors écrire:
$reply = test-host 192.168.0.112 -resolve
switch ($reply.status) {
"Success" { "L'ordinateur est en ligne sous le nom $($reply.hostname)" }
"TimedOut" { "L'ordinateur n'est pas en ligne" }
..
}
La propriété $reply.hostname n'est pas une propriété standard de la classe System.Net.NetworkInformation.PingReply. Je récupère l'information séparément avec la méthode GetHostEntry() de la classe System.Net.Dns, et j'ajoute le résultat à l'objet $reply grâce à la commandelette add-member:
$hostname = ([System.Net.Dns]::GetHostEntry($remotehost)).hostname
$reply = add-member -input $reply NoteProperty HostName $hostname -passthru La deuxième ligne appelle quelques commentaires:
Je ne devrais pas avoir besoin d'assigner le résultat d'add-member à la variable $reply. Je le fais uniquement pour éviter un double affichage. En effet, add-member retourne l'objet auquel elle a ajouté un membre, or cela vient en redondance avec le retour que j'effectue en fin de script (return $reply), retour que je ne peux pas supprimer car il est utile pour les cas où add-member n'est pas appelé.
Vous aurez également noté la présence du paramètre -passthru en fin de ligne. Ce paramètre n'est normalement pas nécessaire pour le bon fonctionnement de ma commande, mais un bug dans la RC0 de PowerShell m'oblige à l'inclure pour que l'ajout de la propriété HostName soit pris en compte. Le problème exact est que add-member ne marche que sur les objets de type PSObject. Or, dans les versions précédentes de PowerShell la conversion d'un objet en PSObject était implicite. Avec la RC0 elle ne l'est plus, sauf quand on spécifie l'argument -passthru.
On pourrait aussi faire une conversion explicite, en écrivant par exemple:
$reply = [PSObject]$reply
add-member -input $reply NoteProperty HostName $hostname
Au passage, j'ai remarqué que le problème est aléatoire. J'ai ouvert un bug sur le sujet, suggérant que le comportement pré-RC0 soit réintroduit.
Janel June 18 Ca y est, Paul McCartney a 64 ans"When I'm sixty-four" est une des chansons phares du répertoire des Beatles. Cette chanson a été écrite par Paul McCartney en 1967. Paul avait alors 25 ans, et il écrivait alors:
When I get older losing my hair many years from now
Will you still be sending me a valentine, Birthday greetings, bottle of wine? If I'd been out til quarter to three would you lock the door? Will you still need me, will you still feed me, when I'm sixty-four? ... (paroles complètes sur http://www.stevesbeatles.com/songs/when_im_sixty_four.asp)
Hé bien, on pourrait dire que la vie n'a pas apporté une réponse positive aux questions que Paul posait virtuellement à sa compagne. A l'époque il était encore avec Jane Asher mais il venait de rencontrer Linda Eastman avec qui il se mariera deux ans plus tard. Le couple resta très uni jusqu'à la mort de Linda en 1998. En 2002 Paul s'est remarié avec Heather Mills, mais ils se sont séparés récemment. Paul est donc seul (officiellement en tout cas) pour fêter ses 64 ans, et ses questions d'il y a 39 ans risquent d'avoir un écho bien amer.
Ah, si au moins il était resté avec Jane Asher, elle pourrait toujours le nourrir:
Pour ce qui est d'avoir besoin de lui, ses fans (dont je suis) sont toujours très nombreux et très actifs. En leur nom, je vous souhaite un très bon anniversaire, sir Paul!
Janel
June 12 Théorie de la relativitéCe week-end sur le groupe de discussion microsoft.public.fr.scripting, un fil un peu long a vu des échanges de solutions pour résoudre ce qui était au départ un problème d’élimination de lignes redondantes dans un fichier. Des solutions ont été proposées en VBScript (langage initialement demandé par l’utilisateur à l’origine du fil), en PowerShell et en PeJBshell. Comment, vous ne savez pas ce qu’est le PeJBshell ? Allez vite voir ici et ne revenez que quand vous saurez votre leçon !
VBScript
Const Pour_Lire = 1
Const Pour_Ajouter = 8 Source = "C:\source.txt" Cible = "C:\cible.txt" Set oFSO = CreateObject("Scripting.FileSystemObject") Set F1 = oFSO.OpenTextFile (source, Pour_Lire, True) Set F3 = oFSO.OpenTextFile (cible, Pour_Ajouter, True) Lit1 = F1.ReadLine F3.WriteLine Lit1 Do Until F1.AtEndOfStream Flag = False Lit1 = F1.ReadLine Set F2 = oFSO.OpenTextFile (cible, Pour_Lire, True) Do Until F2.AtEndOfStream Lit2 = F2.ReadLine If Lit2 = Lit1 Then Flag = True F2.Close Exit Do End If Loop If Flag = False Then F3.WriteLine Lit1 Loop © Sympatix, 2006
PowerShell
type source.txt | select -unique > cible.txt
© janel, 2006
PeJBshell
.pyt
lwrite('cible.txt',Set(lload('source.txt'))) quit © MCI, 2006
Evidemment, il serait facile de tirer à boulets rouges sur VBScript. Oui, le langage est plus bavard que les deux autres. Il faut dire que VBScript n’a pas été conçu pour être utilisé en mode interactif, donc certains concepts comme le « pipeline » qui permet de passer les résultats d’une commande à une autre lui échappent. Cela dit, le script en PeJBshell (vous aurez reconnu le langage Python derrière ce nom commercial douteux J) n’utilise pas le « pipeline » et reste très concis. Toute la concision repose ici sur l’utilisation de la classe Set (n’ayant aucune expérience avec Python, je vous renvoie à la documentation abondante disponible en ligne pour plus d’informations sur cette classe).
Est-il possible d’optimiser le script VBScript malgré tout ? Certainement, oui. Le mérite du script ci-dessus est qu’il marche, avec quelques limites en termes de performance à en croire son auteur, mais il s’agit d’un premier jet, et après tout les autres solutions souffrent peut-être des mêmes limitations. En tout cas elles n’ont pas été soumises à des tests très avancés leur permettant de prétendre qu’elles sont plus performantes.
La discussion a ensuite évolué vers un deuxième problème posé sous la forme d’un défi : comment isoler les lignes communes à deux fichiers donnés ?
L’auteur du défi, Michel Claveau (a.k.a. MCI), a immédiatement proposé sa solution en PeJBshell que je vous livre telle quelle :
.pyt
s=Set(lload('t1.txt')) r=Set(lload('t2.txt')) lwrite('intersect.txt',(s & r)) quit Une amicale querelle m’opposant à MCI sur le front des langages de script, je fus donc sommé de trouver une solution aussi concise et élégante en PowerShell. Dans l’exemple ci-dessus, les lignes communes aux deux fichiers sont retournées par l’opérateur & qui semble effectuer une intersection entre deux objets de la classe Set. Ne trouvant pas d’équivalent à la classe Set dans la bibliothèque du .Net Framework (si quelqu’un a une idée, je suis preneur), je dus me rabattre sur l’utilisation de la commandelette compare-object. Cette commandelette est tout à fait adaptée au besoin ci-dessus exprimé, elle a pour seul (léger) inconvénient de nous contraindre à isoler les objets pour lesquels le résultat de la comparaison nous intéresse en testant une propriété particulière, et à accéder à la valeur qui nous intéresse via une autre propriété. Cela donne ceci :
$t1 = type t1.txt
$t2 = type t2.txt $intersect = compare-object $t1 $t2 -inc | where {$_.SideIndicator -eq "=="} $intersect | foreach {$_.InputObject} > intersect.txt Pour chaque ligne parcourue, compare-object retourne une propriété SideIndicator qui est égale à ‘==’ si la ligne est présente dans les deux fichiers, ‘<=’ si la ligne n’est présente que dans $t1 (c’est-à-dire t1.txt) et enfin ‘=>’ si la ligne n’est présente que dans $t2 (t2.txt). On devra donc filtrer uniquement les lignes pour lesquelles SideIndicator est égale à ‘==’ pour avoir les lignes communes. Le contenu de la ligne à proprement parler est stocké dans la propriété InputObject.
On voit qu’en nombre de lignes « effectives » PowerShell est un peu plus bavard que PeJBshell (enfin, Python) mais ça se tient. On aurait pu tout enchaîner en PowerShell sur une seule ligne et utiliser des raccourcis, ce qui rapproche cette solution de l’autre solution en nombre de lignes et de caractères :
compare-object (gc fic1.txt) (gc fic2.txt) -inc|? {$_.SideIndicator -eq "=="}|% {$_.InputObject} > intersect.txt
Mais cette ligne enchaîne pas moins de cinq commandes différentes, suivies d’une redirection dans un fichier. Même si une telle ligne est relativement facile à construire en mode interactif après quelques tâtonnements, ça n’est pas aussi simple et intuitif que la syntaxe lwrite(‘intersect.txt’,(s & r)) (si tant est qu’on considère & comme un opérateur d’intersection intuitif, alors que de prime abord on pourra penser que l’opération réalisée est une concaténation des deux fichiers).
J’en étais resté là, quand en repensant à la logique sous-jacente à l’opération d’intersection l’idée m’est venue d’utiliser l’opérateur –contains (voir mon billet précédent pour une illustration de son petit frère –notcontains). L’idée se révéla payante puisque j’arrivai à la solution suivante :
type t1.txt | ? {(type t2.txt) -contains $_} > intersect.txt
Ce que Jean – JMST ne manqua pas de ramener à :
${t1.txt} | ? {${t2.txt} -contains $_} > intersect.txt
L’économie réalisée par la syntaxe de Jean est de 6 caractères. A ce niveau-là de concision, je dirais que le plus important est l’aisance avec laquelle l’utilisateur va utiliser une syntaxe plutôt qu’une autre. Pour ma part je reste attaché à type, qui n’est pourtant qu’un alias de la commandelette get-content (et il existe d’autres alias plus courts pour cette même commandelette : gc et cat).
Le défi aurait pu en rester là, ces échanges ayant au moins eu le mérite de montrer la puissance et la flexibilité des environnements de commande évolués (PowerShell étant un environnement parmi d’autres) – vous noterez au passage que personne ne prit la peine de fournir un exemple en VBScript. Mais il manquait un candidat à l’appel : quid de l’invite de commande standard de Windows ? Quand on évoque la recherche un peu évoluée de texte dans un ou plusieurs fichiers, on pense tout de suite à la commande findstr. Et en effet, bien que peu familier des subtilités de cette commande j’eus tôt fait d’arriver à la solution suivante :
findstr /g:t1.txt t2.txt > intersect.txt
Alors, l’invite de commande standard plus forte que tous les autres environnements ? PowerShell, PeJBshell et autres IPython ne seraient que des efforts vains devant la grandeur et la simplicité de cmd.exe ? Comme ne manquèrent pas de le faire remarquer Jean et MCI, findstr est un exécutable (findstr.exe) et est donc accessible depuis PowerShell et PeJBshell (et vraisemblablement depuis n’importe quel autre environnement de type ‘shell’). Cela ne retire donc rien aux mérites de ces environnements, au contraire ! Loin de s’exclure mutuellement, ils se complètent admirablement.
Sur ces belles paroles, je vous dis à bientôt !
Janel June 11 Révélations dans l'affaire de la touche TabDans mon billet daté du 5 juin 2006, j’évoquais un problème introduit par l’amélioration apportée par MOW à ma version de la fonction tabexpansion:
Les types disponibles sont chargés en mémoire dans une variable globale ($global:dtAssemblies), ce qui permet d’y accéder beaucoup plus rapidement les fois suivantes. Le problème avec cette mise en mémoire est qu’elle n’est faite qu’une fois. Si par la suite l’utilisateur charge une nouvelle DLL contenant des types supplémentaires, ces types ne seront pas résolus par la fonction car ils n’auront pas été mémorisés.
Je me propose ici de corriger ce problème. La technique utilisée est fort simple. En plus de la liste des types, je garde une liste des « assemblies » elles-mêmes. Ainsi, à chaque nouvelle tentative de résolution d’un nom de type, je peux recharger la liste des types uniquement pour les « assemblies » qui sont actives mais qui ne faisaient pas partie de la liste la fois précédente. L’inventaire des types terminé, je n’ai plus qu’à réactualiser la liste des « assemblies » pour la fois suivante.
Voici le code. Les lignes que j’ai ajoutées ou modifiées sont en caractères gras. J’ai également sorti du test « if (!($global:dtAssemblies)) {...} » toute la partie liée au remplissage (cf. « #fill the DataTable » et lignes suivantes). En effet, le remplissage est maintenant effectué à chaque fois, mais uniquement sur les « assemblies » récemment chargées.
Attention : il ne s’agit que de la partie de la fonction tabexpansion correspondant à la résolution des noms de types ; veillez à l’insérer au bon endroit dans votre propre fonction.
# Cache and Handle namespace and TypeNames..
#
'^\[(.*)' {
# Only the first time create a DataTable with Typenames, namespaces and dotCount (level)
# and initialise a global variable that will keep a list of all assemblies
#
$matched = $matches[1]
if (!($global:dtAssemblies)) {
$global:dtAssemblies = new-object data.datatable
[VOID]($global:dtAssemblies.Columns.add('name',[string]))
[VOID]($global:dtAssemblies.Columns.add('DC',[int]))
[VOID]($global:dtAssemblies.Columns.add('NS',[string]))
$global:glAssemblies = @()
}
# fill the DataTable with newly added assemblies (will populate from all assemblies the first time)
#
[void]([appdomain]::CurrentDomain.GetAssemblies() | where {$global:glAssemblies -notcontains $_} | foreach {
$_.GetTypes() | foreach {
$dc = $_.fullname.length - $_.fullname.replace('.','').length
$ns = $_.namespace
$global:dtAssemblies.rows.add("$_",$dc,$ns)
}
})
# refresh global list
#
$global:glAssemblies = [appdomain]::CurrentDomain.GetAssemblies()
# actual tab completion
#
$dots = $matched.length - $matched.replace('.','').length
switch ($dots) {
0 {"[System","[Microsoft"}
Default {
$res = @();$res += $global:dtAssemblies.select("ns like '$($matched)%' and dc = $($dots + 1)") |
sort ns | select -uni ns | % {"[$($_.ns)"}
$res += $global:dtAssemblies.select("name like '$($matched)%' and dc = $dots") |
sort name | % {"[$($_.name)]"}
@($res)
}
}
break;
}
Le cœur du changement introduit est le filtre suivant :
[appdomain]::CurrentDomain.GetAssemblies() | where {$global:glAssemblies -notcontains $_}
L’opérateur –notcontains retourne $false ou $true selon que le terme de droite est contenu ou non dans l’objet de gauche. L’objet de gauche sera typiquement une collection ou une énumération. Il existe également un opérateur –contains qui effectue le test inverse.
La fonction mise à jour et rechargée, je peux maintenant ajouter des « assemblies » à ma session PowerShell et parcourir leurs types :
PS> [reflection.assembly]::loadwithpartialname(“system.windows.forms”)
GAC Version Location
--- ------- --------
True v2.0.50727 C:\WINDOWS\assembly\GAC_MSIL\System.Windows.Forms\2.0.0.0__b77a5c561934e089\System.Windows.For...
PS> [system.windows.forms. [Tab]
PS> [System.Windows.Forms.ButtonInternal [Tab]
PS> [System.Windows.Forms.Design (etc)
Je profite de ce billet pour relever deux autres problèmes introduits par la version de MOW :
Tout d’abord, si je demande à PowerShell de résoudre [system.win, je m’attendrais à me voir proposer [System.Windows et ainsi de suite pour finalement arriver à [System.Windows.Forms. Malheureusement il n’en est rien, la faute aux deux lignes suivantes :
$res = @();$res += $global:dtAssemblies.select("ns like '$($matched)%' and dc = $($dots + 1)") | sort ns | select -uni ns | % {"[$($_.ns)"}
$res += $global:dtAssemblies.select("name like '$($matched)%' and dc = $dots") | sort name | % {"[$($_.name)]"}
Dans ces deux lignes, la recherche de résultats se base sur le nombre de points contenus dans le texte saisi pour récupérer les types de base correspondants. Si la racine d’un type de base est composée de plusieurs points, comme System.Windows.Forms, une recherche qui ne comprend qu’un seul point ne donnera rien car il n’existe aucun type de base simplement appelé System.Windows.
Deuxième problème, la fonction ne parcourt pas des types racines autres que System ou Microsoft (les deux racines standards). Si je crée un type dont la racine est MyNameSpace, [MyName [Tab] ne résoudra pas le nom. Il faudra saisir jusqu’au premier point ([MyNameSpace.) pour commencer à parcourir les types proposés par MyNameSpace. La faute à la clause switch suivante :
switch ($dots) {
0 {"[System","[Microsoft"}
Avis aux amateurs pour la correction de ces deux problèmes !
Janel June 05 Rebondissements dans l'affaire de la touche TabAh, les joies du blog ! A peine me ravissais-je à l’idée que je pourrais profiter d’un beau week-end ensoleillé que je vis MOW attelé sur son propre blog à corriger et à améliorer le code que j’avais fourni pour la fonction tabexpansion dans mon billet du 2 juin. Mais rassurez-vous, cela ne m’a pas gâché le week-end pour autant.
Les améliorations de MOW portent principalement sur deux points :
1. Le temps d’attente
Je le soulignais dans mon billet, le premier appui sur la touche Tab met plusieurs secondes à donner la première réponse, le temps de parcourir tous les types disponibles dans toutes les « assemblies ». Même si le parcours des réponses suivantes est instantané, ce temps d’attente initial peut être un frein à l’usage. La solution de MOW consiste à charger les réponses dans une table lors du premier appel à cette fonction. La toute première fois prend donc toujours quelques secondes, mais les fois suivantes la fonction fait directement appel à la table, ce qui améliore considérablement les performances. En plus, cet exercice est une bonne illustration de l’utilisation du type System.Data.Datatable.
L’inconvénient de cette méthode est qu’elle ne prend pas en compte les « assemblies » qui ont pu être chargées après le premier appel à la fonction. Je suppose que ça ne sera pas un problème dans 90% des cas. On pourrait sans doute corriger ce problème en comparant la liste des « assemblies » en cours d’utilisation avec la liste précédente. Le temps de comparaison ne devrait pas être pénalisant, et on pourrait même optimiser le chargement des nouveaux types en ne parcourant que les nouvelles « assemblies ». Je n’aurai guère le temps de m’y mettre cette semaine, mais s’il pleut le week-end prochain, promis je m’y colle !
2. Le parcours des sous-types
Ma fonction avait un autre défaut : elle donnait les noms de type en entier sans les découper par sous-niveau, qui plus est triés dans un ordre non prévisible. Pour des types simples à un ou deux niveaux de hiérarchie ce n’est pas un problème, mais si l’on veut parcourir une classe de types un peu touffue cela rend le parcours long voire hasardeux. La méthode de MOW segmente la navigation par niveau :
PS> [sys [Tab]
PS> [System [Tab]
PS> [System.di [Tab]
PS> [System.Diagnostics
Etc. En prime, s’il n’existe plus de sous-niveau pour le type affiché, la fonction termine le nom par un crochet fermant ([System.Diagnostics.Process]).
Si vous voulez tester le code de MOW, c’est ici que ça se passe. Laissez-lui un commentaire, même en français, il se fera un plaisir de le traduire via Google. J
Et puisque j’en suis à parler des blogs qui parlent de ce sujet, jetez donc aussi un œil sur la contribution de DbmwS. Plus on est de fous, … !
Janel June 02 PowerShell et la touche Tab (suite et fin)Dans la première partie sur PowerShell et la touche Tab, j’ai décrit dans ses grandes lignes la façon dont PowerShell utilise la fonction tabexpansion pour terminer automatiquement ce que l’utilisateur saisit en ligne de commande. Nous allons maintenant voir comment personnaliser cette fonction en y ajoutant nos propres types de données à terminer.
A tout seigneur, tout honneur, je commencerai par un exemple tiré du blog de MOW, un des rares (sinon le seul ?) MVP PowerShell en ce bas monde. MOW propose d’ajouter la résolution des propriétés et méthodes statiques pour n’importe quel type. Il suffit de saisir le nom du type entre crochets suivi du double deux-points réglementaire, et éventuellement de donner les premières lettres si on a déjà une idée du nom de membre que l’on recherche. Par exemple :
PS> [datetime]:: [Tab]
PS> [datetime]::Compare(
Vous noterez que les méthodes sont affichées avec une parenthèse ouvrante, ce qui les distingue des propriétés. La tabulation résout également les énumérations :
PS> [consolecolor]::B [Tab]
PS> [consolecolor]::Black [Tab]
PS> [consolecolor]::Blue
Voici le code à ajouter à votre fonction pour que ça marche :
# this Part is added to handle Type Static Members
# /\/\o\/\/ 2006
# Handle Static Members
'(\[.*\])::(\w*)' {
invoke-expression "$($matches[1]) | gm -static" | where {$_.name -like "$($matches[2])*"} |% {
if ($_.MemberType -band $method) {
"$($matches[1])::$($_.name)" + '('
} Else {
"$($matches[1])::$($_.name)"
}
}
break
}
Pour éditer la fonction, exportez-la dans un fichier texte:
PS> get-content function:tabexpansion > c:\scripts\expand-tab.ps1
PS> notepad $$
$$ est une variable système qui pointe sur le dernier objet manipulé, ‘c:\scripts\expand-tab.ps1’ dans notre cas.
Attention, pensez immédiatement à ajouter les première et dernière lignes suivantes au script :
function tabexpansion {
[contenu actuel du fichier]
}
Voilà, vous pouvez maintenant ajouter le code ci-dessus. Où l’ajouter ? De préférence à la suite des sections déjà incluses, par exemple juste après les lignes suivantes :
# expand the parameter sets and emit the matching elements
foreach ($n in $cmdlet.ParameterSets | Select-Object -expand parameters)
{
$n = $n.name
if ($n -like $pat) { '-' + $n }
}
break;
}
[insérer ici le code de MOW]
Veillez à insérer le code *avant* les deux dernier crochets de la fonction : l’un termine la clause switch et l’autre marque la fin de la fonction.
Après avoir modifié le code de la fonction, il vous faudra sauvegarder et la recharger pour que les modifications soient effectives :
PS> . c:\scripts\expand-tab.ps1
Pensez à ajouter cette ligne à votre profil ($profile) pour que la fonction personnalisée soit active à chaque nouvelle session.
Quand j’ai vu le code de MOW, je me suis demandé comment on pourrait automatiquement terminer la saisie d’un type. En effet, avant d’arriver aux propriétés d’un type on est parfois bloqué par le nom du type lui-même. En gros, je voudrais pouvoir taper quelque chose comme [system.diag et parcourir avec la touche Tab tous les types qui commencent par ces lettres. Ca n’est pas très compliqué, pour peu qu’on sache où les types sont déclarés dans PowerShell.
En tant que client du .Net Framework, PowerShell utilise les types publiés dans des DLL spécialisées, nommées « assemblies » en anglais (si quelqu’un connaît le terme français, je suis preneur). Les « assemblies » les plus courantes sont chargées automatiquement au démarrage de toute session PowerShell, et il est possible d’en charger d’autres (fournies avec le Framework ou écrites par des tiers) si l’on veut utiliser des types d’objets supplémentaires. Je reviendrai sur tout cela dans un autre billet, mais l’essentiel pour aujourd’hui est qu’on puisse accéder à la liste des « assemblies » actives à un instant T et qu’on puisse énumérer les types contenus par chacune de ces « assemblies ».
Voici le code :
# Handle Types
# janel 2006
'^\[(.*)' {
[appdomain]::CurrentDomain.GetAssemblies() | % {$_.GetTypes() | ? {$_.FullName -like "$($matches[1])*" -or $_.FullName -like "System.$($matches[1])*"}} | % {
"[$($_.FullName)]"
}
}
Comme pour l’exemple précédent, on l’insèrera en fin de fonction. Dans ce cas précis, il est même important d’insérer ce code *après* le code de MOW. Je vous laisse réfléchir là-dessus, comme dirait l’autre. J
Quelques exemples d’utilisation :
PS> [system.diag [Tab]
PS> [System.Diagnostics.Assert] [Tab]
PS> [System.Diagnostics.AssertFilter]
PS> [dat [Tab]
PS> [System.DateTime]
Comme vous pouvez le voir, j’ai reproduit le comportement de PowerShell qui ajoute automatiquement le type de base System si vous l’omettez. Vous constaterez sans doute un délai de quelques secondes (variable selon la configuration de votre PC) au premier appui de la touche Tab. C’est le temps que met la fonction pour énumérer tous les types existants. Ensuite, les appuis successifs sur Tab et Shift-Tab pour parcourir les types un à un sont instantanés.
Pour finir, un dernier exemple de personnalisation de la fonction tabexpansion qui sort un peu du rôle attendu de la touche Tab. Après tout, me disais-je, ce que fait la touche Tab, ce n’est que de remplacer un texte par un autre. On peut donc envisager n’importe quel scénario, et pas simplement de terminer automatiquement le texte déjà commencé. M’est alors venue l’idée d’implémenter une gestion sommaire de raccourcis. Le principe serait de pouvoir taper un ou deux caractères, Tab, et hop ! Toute une expression apparaît (ou un mot particulièrement long et compliqué). Par exemple :
PS> write-host #h [Tab]
PS> write-host ‘Hello world!’
Ou encore:
PS> dir #d [Tab]
PS> dir |?{$_.PSIsContainer}
Pour cela, il faut deux choses: 1) le code qui va bien dans la fonction tabexpansion, et 2) la table de hachage qui associera le nom de chaque raccourci (clé) avec le contenu de remplacement (valeur). Voici déjà le code :
# Handle text shortcuts
# janel 2006
'^#(.*)' {
${#}[[string]$matches[1]]
break
}
Comme vous pouvez le voir, le code nécessaire est très léger. L’expression régulière '^#(.*)' détecte tout mot commençant par # (j’ai choisi ce caractère comme préfixe à mes raccourcis parce qu’il est utilisé par PowerShell pour marquer le début de commentaires, et il m’a semblé qu’on a rarement besoin d’insérer des commentaires lorsqu’on est en mode interactif). Le résultat de cette expression régulière est utilisé comme clé dans la table de hachage ${#}. Vous noterez au passage le nom ésotérique de cette table. En effet, PowerShell accepte à peu près n’importe quel jeu de caractères pour les noms de variable, à certaines conditions. Je ne pouvais pas appeler ma table $#, mais ${#} était un nom valide. Je pense qu’il est très peu probable que ce nom tombe en conflit avec une variable déclarée par l’utilisateur.
Il reste à remplir cette table de hachage avec les couples clé-valeur de votre choix. On peut le faire en ligne de commande de plusieurs façons :
PS> ${#}.Add(“h”, “Hello World !”)
PS> ${#} += @{“r”=’|{$_.PSIsContainer}’}
Vous remarquerez que dans le deuxième exemple, j’entoure le texte de remplacement par des guillemets simples (apostrophes) au lieu des doubles guillemets usuels. Cela indique à PowerShell qu’il ne doit pas évaluer les variables contenues dans cette chaîne de caractères lorsqu’il l’affichera. On veut simplement qu’il l’affiche telle quelle. L’évaluation sera faite ensuite lorsqu’on appuiera sur la touche Entrée pour exécuter toute la ligne saisie.
De plus, dans ce même exemple j’ai évité de taper le moindre espace. En effet, lorsqu’une chaîne contient des espaces, PowerShell l’affichera entourée de guillemets simples. C’est très utile lorsqu’on veut accéder au nom d’un répertoire qui contient des espaces, mais c’est gênant s’il s’agit d’une suite d’instructions. Il faudra donc faire attention à cela si vous vous créez vos propres raccourcis et que ceux-ci sont des suites d’instructions comme dans cet exemple.
L’étape suivante consiste à créer une base de raccourcis exploitable d’une session à l’autre. Pour cela, on pourra encore compter sur le fichier $profile, ce qui permettra d’avoir les raccourcis chargés à chaque session. Je vous laisse choisir votre méthode (ajout des éléments un à un, lecture d’un fichier, etc). Je publierai la mienne si j’ai quelque chose de valable. J
Voilà, à vous de jouer maintenant. Il ne vous reste plus qu’à implémenter vos propres besoins de résolution de texte. Amusez-vous bien !
Janel June 01 PowerShell et la touche Tab (1ere partie)Comme de nombreux shells, PowerShell permet à l’utilisateur de terminer automatiquement le texte en cours de saisie avec la touche Tab. Dans les premières versions de PowerShell, cette fonctionnalité était limitée aux noms de répertoires et de fichiers (comme dans cmd.exe). Par exemple, si vous tapez :
PS> cd c:\doc [Tab]
PowerShell cherche tous les fichiers et répertoires qui commencent par c:\doc et les affiche un par un (il faut utiliser Tab et Shift-Tab pour parcourir toutes les entrées s’il y en a plusieurs). A priori, le premier choix proposé sera :
PS> cd ‘C:\Documents and Settings’
Dans sa dernière version (actuellement en RC1), PowerShell offre bien plus que cela. On peut parcourir les commandelettes disponibles en tapant le verbe suivi de ‘-‘:
PS> get- [Tab]
PowerShell proposera successivement Get-Acl, Get-Alias, Get-AuthenticodeSignature, …. Si le répertoire en cours contient des fichiers commençant par get-, ceux-ci seront proposés avant les commandelettes.
Mais il y a mieux ! Si vous ne vous rappelez pas des paramètres exacts que get-childitem utilise, vous pouvez utiliser la touche Tab pour vous les énumérer :
PS> gci - [Tab]
PowerShell proposera un à un tous les paramètres acceptés par gci (alias pour get-childitem). Si au lieu de taper ‘-‘ vous tapez ‘-r’, PowerShell ne vous proposera que les paramètres qui commencent par r.
Mais il y a encore mieux ! Vous cherchez la propriété d’un objet mais vous ne vous souvenez plus de son nom exact ? Là aussi, la touche Tab peut vous aider :
PS> $outlook = gps outlook # assigne le process actif Outlook.exe à la variable $outlook
PS> $outlook. [Tab] # notez le ‘.’ qui indique qu’on veut accéder à un membre de l’objet $outlook
PowerShell proposera successivement tous les membres de l’objet $outlook, d’abord les propriétés puis les méthodes : $outlook.Handles, $outlook.Name, $outlook.NPM, $outlook.PM, $outlook.VM, $outlook.WS, $outlook.add_Disposed(, etc. Vous noterez que les méthodes apparaissent avec une parenthèse ouvrante, ce qui les distingue clairement des propriétés.
Mais il y a encore bien mieux ! Hé oui, car en fait on veut toujours faire plus et mieux, et pour cela les concepteurs de PowerShell ont donné le pouvoir aux utilisateurs : le fonctionnement de la touche Tab est entièrement accessible par la fonction tabexpansion. Il est donc possible de modifier cette fonction pour ajouter de nouvelles fonctionnalités et/ou modifier les fonctionnalités existantes. Vous pouvez vérifier tout de suite ce que fait la fonction par défaut en tapant :
PS> get-content function:tabexpansion
Vous verrez que la fonction accepte deux paramètres : $line et $lastword. Le premier fait référence à la ligne toute entière, le deuxième fait référence au dernier mot de la ligne. La plupart du temps on se contentera d’analyser le dernier mot, mais dans certains cas il peut être nécessaire d’analyser le contexte à partir des mots précédents dans la ligne pour déduire les valeurs à proposer. C’est le cas pour les paramètres d’une commandelette.
La fonction est principalement articulée autour d’une instruction switch qui parcourt une à une des expressions régulières (voir MSDN sur le sujet, ou une illustration dans mon billet précédent). Chaque expression régulière correspond à une situation pour laquelle on veut déduire des propositions possibles. Ces propositions seront retournées à PowerShell sous la forme d’un tableau de chaînes de caractères, et PowerShell s’occupera de les afficher une à une via les touches Tab et Shift-Tab. Prenons un exemple pour mieux comprendre :
switch –regex ($lastword) {
# Handle property and method expansion...
'\$(\w+)\.(\w*)' {
$method = [Management.Automation.PSMemberTypes] 'Method,CodeMethod,ScriptMethod,ParameterizedProperty'
$variableName = $matches[1]
$val = Get-Variable -value $variableName
$pat = $matches[2] + '*'
Get-Member -inputobject $val | where {$n = $_.name; $n -like $pat -and $n -notmatch '^[ge]et_'} | foreach {
if ($_.MemberType -band $method)
{
# Return a method...
'$' + $variableName + '.' + $_.name + '('
}
else {
# Return a property...
'$' + $variableName + '.' + $_.name
}
}
break;
}
… }
Cette section de la fonction s’occupe de résoudre les propriétés et méthodes possibles pour une variable. L’expression régulière ‘\$(\w+)\.(\w*)’ doit donc correspondre à la construction d’une variable suivie d’un point indiquant qu’on veut accéder à un membre de cette variable (le membre pouvant être une méthode ou une propriété). Et de fait, ‘\$(\w+)\.(\w*)’ correspond à toute chaîne de caractères commençant par le signe $ et séparée en deux parties par un point. Chaque partie (avant et après le point) est isolée dans l’expression par des parenthèses, ce qui a pour effet de distinguer ces deux parties de la chaîne dans les résultats stockés par le tableau $matches. L’entrée $matches[1] correspondra à la partie située entre $ et ., alors que $matches[2] correspondra à la partie située après le point. Si la touche Tab est tapée alors que le dernier mot de la ligne est $outlook.mo, $matches[1] contiendra outlook et $matches[2] contiendra mo.
Le code qui suit (get-member …) s’occupe d’explorer les membres existants pour la variable $matches[1] en ne s’intéressant qu’à ceux qui commencent par $matches[2] (where {…}). Les résultats sont retournés pour affichage, avec une différence dans l’affichage selon qu’il s’agit de méthodes ou non. S’il s’agit de méthodes, une parenthèse ouvrante sera accolée à leur nom.
Explorez les autres exemples présents dans la fonction fournie par défaut pour vous familiariser avec le principe.
Dans un prochain billet, je donnerai des exemples de personnalisation.
Janel |
|
|