| Jacques's profilejanelPhotosBlogLists | Help |
|
|
January 21 Mes scripts PowerShell sont polyglottes, même en v1 !Suite à la publication de mon billet sur la « localisation des scripts » dans PowerShell, une discussion intéressante a démarré sur un forum de PowerShell-Scripting.com où j’avais également publié l’information. Sur ce forum, Laurent Dardenne a évoqué les techniques possibles pour faire de la localisation avec la v1 de PowerShell. Je vous rappelle que la technique exposée dans mon billet repose sur des fonctionnalités apparues avec la v2.
Malheureusement, j’ai trouvé les techniques décrites par Laurent plutôt complexes et pas forcément tout à fait adaptées à ce que j’ai besoin de faire quand je veux proposer les différents messages d’un script en plusieurs langues. J’ai donc pris le temps de créer une fonction, très simple donc à priori très facilement perfectible, pour couvrir les besoins les plus courants sans avoir à installer la v2 de PowerShell.
Tout d’abord, voici le corps de la fonction (je l’ai appelée import-culturaldata pour la distinguer de la commandelette disponible dans la v2, mais je ne suis pas sûr que ce nom soit très approprié – je suis preneur de toute suggestion) :
function import-culturaldata
{
param (
[string]$filename = [IO.Path]::GetFileNameWithoutExtension((split-path -leaf $myinvocation.MyCommand)),
[System.Globalization.CultureInfo]$UIculture = (get-UIculture)
)
if ($myInvocation.ScriptName) { $scriptname = $myInvocation.ScriptName }
else { $scriptname = $myInvocation.MyCommand.Definition }
$fullpath = $(split-path $ScriptName) + "\$UIculture\$filename" + ".psl1"
$ht = @{}
get-content $fullpath | where {($_.TrimStart()) -ne "" -and ($_.TrimStart())[0] -ne "#"} | foreach {
$key, $value = $_.Split("=", 2)
$ht.Add($key.Trim(), $value.TrimStart())
}
$ht
}
La fonction accepte deux paramètres :
- $filename – par défaut, le nom du script qui appelle la fonction (sans son extension)
- $UIculture – par défaut, la culture de la session en cours
La fichier lu doit avoir une extension .psl1 (pour PowerShell Localization, v1-compliant). J’ai tenu à me démarquer de l’extension choisie pour le même usage dans la v2 (.psd1) car le format d’un fichier .psd1 est potentiellement bien plus complexe que celui utilisé par ma fonction. Un fichier .psl1 doit contenir des entrées au format suivant :
<nom> = <valeur>
<nom> peut être n’importe quel texte pouvant être utilisé par PowerShell pour créer un nom de variable. Le caractère "=" est interdit.
<valeur> peut être n’importe quel texte, y compris contenant des "=". Je n’ai pas testé l’insertion de here-strings mais je doute que ça marche de manière totalement transparente. En revanche, on peut insérer des variables ({0}, {1}, …) qui pourront être substituées en cours de script avec l’opérateur -f. Je fournis une illustration de cet usage dans l’exemple ci-dessous.
Pour faciliter la lecture d’un fichier .psl1 on pourra insérer des lignes vierges, des espaces avant ou après <nom>, des espaces avant <valeur> et des lignes de commentaire commençant par "#" (précédé ou non par des espaces).
La fonction retourne un tableau associatif (hashtable en anglais) dont les paires clé/valeur sont créées à partir des entrées du fichier (clé = <nom>, valeur = <valeur>).
Prenons un exemple d’utilisation archi-simple. Pour cet exemple, imaginons que j’ai sauvegardé la fonction ci-dessus dans un fichier c:\outils\import-culturaldata.ps1. Pour tester la fonction, j’ai un répertoire c:\tests qui contient un fichier test-local.ps1 et deux sous-répertoires, c:\tests\fr-FR et c:\tests\en-US contenant chacun un fichier test-local.psl1.
Voyons d’abord le contenu des deux fichiers test-local.psl1 :
PS> get-content fr-FR\test-local.psl1
# culture "fr-FR"
Welcome = Bienvenue {0} au jeu "Devinez un jour" !
Prompt = Tapez un nom de jour
Success = Vous avez deviné ! Toutes nos félicitations !
PS>
PS> get-content en-US\test-local.psl1
# culture "en-US"
Welcome = Welcome {0} to the "Guess a day" game!
Prompt = Enter the name of any day
Success = You had it right! Congratulations!
Vous aurez reconnu les messages déjà utilisés dans mon billet précédent (à peu de chose près).
Voyons maintenant le script de test, test-local.ps1 :
PS> get-content test-local.ps1
param ([System.Globalization.CultureInfo]$myculture = (get-UIculture))
. c:\outils\import-culturaldata.ps1
$messages = import-culturaldata -uiculture $myculture
$messages.Welcome –f $env:username
$messages.Prompt
$messages.Success
Vous le voyez, j’ai été au plus simple.
Le script accepte comme paramètre optionnel une culture, qui par défaut sera celle déjà en place. Ca permettra de tester rapidement les différentes localisations disponibles.
Ensuite, le script récupère les données localisées dans une variable $messages – vous noterez la syntaxe de ma fonction qui retourne son résultat dans une variable, au lieu de prendre le nom de la variable en argument comme c’est le cas avec import-localizeddata.
Pour finir, le script affiche les trois messages l’un à la suite de l’autre. Le premier message, $messages.Welcome, contient une variable {0} à laquelle je substitue le nom de l’utilisateur pour personnaliser le message.
Voici le script en action :
PS> .\test-local en-US
Welcome janel to the "Guess a day" game!
Enter the name of any day
You had it right! Congratulations!
PS>
PS> .\test-local
Bienvenue janel au jeu "Devinez un jour" !
Tapez un nom de jour
Vous avez deviné ! Toutes nos félicitations !
Voilà. Au passage, vous aurez peut-être trouvé un peu curieux le test suivant dans le code de la fonction :
if ($myInvocation.ScriptName) { $scriptname = $myInvocation.ScriptName }
else { $scriptname = $myInvocation.MyCommand.Definition }
J’ai dû ajouter ce test pour permettre à la fonction de tourner à la fois sur PowerShell v1 et la v2 CTP3. En effet, il semble qu’il y ait un bug avec la CTP3 dont $myinvocation.ScriptName ne retourne rien dans ce contexte, alors qu’elle devrait retourner le nom complet du script qui a appelé la fonction. Le bug est maintenant documenté, on peut donc espérer qu’il sera corrigé avec la prochaine fournée. En attendant, cette "rustine" permettra de maintenir le code tel quel sans se préoccuper de la version installée sur les postes qui l’utiliseront.
Bien à vous.
Janel January 19 PowerShell, svchost.exe et les fonctions avancées (1ere partie)En cette fin de matinée mon PC semblait très occupé, au moins autant que moi. En jetant un œil à la liste des tâches en cours j’ai pu constater qu’il restait très peu de mémoire disponible. Après avoir fermé quelques applications inutilement restées ouvertes depuis vendredi dernier, j’ai vu qu’il restait encore plusieurs processus gourmands, et notamment deux svchost.exe consommant chacun plus de 50 Mo.
On sait que Windows utilise les processus svchost.exe pour héberger des services n’ayant pas leur propre exécutable. Chaque instance de svchost.exe peut héberger plusieurs services, parfois plusieurs dizaines. On sait moins comment on peut obtenir la liste des services hébergés par un processus svchost.exe donné. Il existe plusieurs méthodes, je m’attarderai ici sur l’une d’entre elles qui fait appel à Windows PowerShell.
Lorsqu’il s’agit de manipuler des processus et des services dans PowerShell, on pense immédiatement à deux commandelettes fournies en standard :
PS> get-process
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ------- ------ ----- ----- ----- ------ -- ----------- 27 1 380 376 11 1952 AEADISRV 38 2 736 432 21 1968 agrsmsvc 183 3 2608 580 41 1024 Ati2evxx [...]
PS> get-service
Status Name DisplayName ------ ---- ----------- Stopped AddFiltr AddFiltr Stopped Adobe LM Service Adobe LM Service Running AeLookupSvc Application Experience [...]
Comme on pourrait s’y attendre, on peut facilement obtenir la liste des processus nommés svchost.exe :
PS> get-process svchost
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ------- ------ ----- ----- ----- ------ -- ----------- 331 5 4020 3856 45 816 svchost 732 11 6440 4872 56 884 svchost 469 20 106252 13912 206 932 svchost [...]
On peut même très facilement isoler les processus dont la mémoire de travail est supérieure à 50 Mo :
PS> get-process svchost | where {$_.workingset -gt 50MB}
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ------- ------ ----- ----- ----- ------ -- ----------- 1026 22 94800 79816 198 1068 svchost 2242 290 134076 98224 323 1080 svchost
Mais get-process ne fournit pas de méthode pour aller voir quels services se cachent dans ces processus. On pourrait à la rigueur s’en faire une idée en parcourant les modules chargés par chacun de ces processus, mais la liste serait assez rébarbative à parcourir et à recouper avec de véritables services Windows.
On aurait alors envie de se tourner vers get-service. Pendant un instant j’ai imaginé que les objets retournés par get-service avaient un numéro de tâche qui permettrait de les rattacher à un processus. Mais non. Ou alors, je n’ai pas trouvé.
J’ai alors pensé à un troisième larron : la classe WMI Win32_Service. PowerShell permet d’accéder très facilement aux classes WMI :
PS> get-wmiobject win32_service | get-member -membertype properties
TypeName: System.Management.ManagementObject#root\cimv2\Win32_Service
Name MemberType Definition ---- ---------- ---------- AcceptPause Property System.Boolean AcceptPause {get;set;} AcceptStop Property System.Boolean AcceptStop {get;set;} Caption Property System.String Caption {get;set;} CheckPoint Property System.UInt32 CheckPoint {get;set;} CreationClassName Property System.String CreationClassName {get;set;} Description Property System.String Description {get;set;} DesktopInteract Property System.Boolean DesktopInteract {get;set;} DisplayName Property System.String DisplayName {get;set;} ErrorControl Property System.String ErrorControl {get;set;} ExitCode Property System.UInt32 ExitCode {get;set;} InstallDate Property System.String InstallDate {get;set;} Name Property System.String Name {get;set;} PathName Property System.String PathName {get;set;} ProcessId Property System.UInt32 ProcessId {get;set;} ServiceSpecificExitCode Property System.UInt32 ServiceSpecificExitCode {get;set;} ServiceType Property System.String ServiceType {get;set;} Started Property System.Boolean Started {get;set;} StartMode Property System.String StartMode {get;set;} StartName Property System.String StartName {get;set;} State Property System.String State {get;set;} Status Property System.String Status {get;set;} SystemCreationClassName Property System.String SystemCreationClassName {get;set;} SystemName Property System.String SystemName {get;set;} TagId Property System.UInt32 TagId {get;set;} WaitHint Property System.UInt32 WaitHint {get;set;} [...]
Bingo ! La propriété ProcessId (surlignée ci-dessus) semble tout à fait correspondre à ce que je cherche, à savoir faire une jointure entre la liste des processus svchost.exe et la liste des services. Essayons :
PS> get-process svchost | where {$_.workingset -gt 50MB} | foreach { >> $id = $_.id; gwmi win32_service | where {$_.processi d -eq $id}} | >> ft proc*,name,displayname,stat* -auto >>
ProcessId name displayname state status --------- ---- ----------- ----- ------ 1068 AudioEndpointBuilder Windows Audio Endpoint Builder Running OK 1068 CscService Offline Files Running OK 1068 EMDMgmt ReadyBoost Running OK [...]
Et voilà.
NB. Je suis obligé de tronquer les commandes sur plusieurs lignes pour que l'affichage soit lisible dans mon blog. En réalité, je tape les commandes les unes à la suite des autres sur un même ligne.
Pour celles et ceux qui s’interrogent: oui, on pouvait s’épargner la création d’une variable intermédiaire ($id) mais ça n’apportait aucune amélioration dans les performances de la requête, ça rendait la ligne un peu plus complexe à déchiffrer, et au final ça m’aurait éloigné de l’objet de cette discussion. J
Quant à mon problème de mémoire vive utilisée par svchost.exe, cet exercice m’a surtout permis de constater qu’il y avait beaucoup de monde pour utiliser cette mémoire, ce qui n’est pas forcément un problème en soi. Il y aurait eu deux ou trois services hébergés par chacun de ces processus, le problème aurait été autrement plus criant. Là, je me suis contenté de parcourir la liste et d’arrêter les services qui ne me paraissaient pas indispensables.
Il ne me restait plus qu’à pousser cette ligne de commandes dans un script, de manière à m’éviter de devoir rechercher l’information à chaque fois qu’il m’arrive de vouloir obtenir l’information. Dans sa version la plus simple, le fichier ressemblait alors à ceci :
PS> get-content get-hostedservice.ps1 # get-hostedservice # # Returns a list of services hosted by a process. # # Syntax: get-hostedservice -id ProcessId
param ([int]$Id)
get-wmiobject Win32_Service | where {$_.ProcessId -eq $Id}
Et voici un exemple de mise en oeuvre :
PS> gps svchost | where {$_.workingset -gt 50MB} | foreach {get-hostedservice $_.id} | >> ft proc*,name,displayname,stat* -auto >>
ProcessId name displayname State Status --------- ---- ----------- ----- ------ 1068 AudioEndpointBuilder Windows Audio Endpoint Builder Running OK 1068 CscService Offline Files Running OK 1068 EMDMgmt ReadyBoost Running OK [...]
La ligne à taper est plus simple que la précédente, mais pas tant que ça. Encore faut-il se rappeler que le script accepte l’Id d’un processus comme paramètre. Et j’aurais aimé pouvoir simplement récupérer les objets du pipeline dans mon script, sans avoir à les traiter un par un dans ma ligne de commandes avec une boucle foreach.
C’est là que la v2 de PowerShell apporte quelques grosses améliorations qui vont nous simplifier encore plus la tâche. Mais nous verrons tout cela dans la 2e partie, à venir dans quelques jours. D’ici là, soyez sages ! J
Janel January 16 Mes scripts PowerShell sont polyglottesAvec la v2 CTP3 de PowerShell arrivent une foultitude de nouvelles fonctionnalités, toutes plus sympathiques les unes que les autres. On parle beaucoup de la gestion de sessions à distance et de l’exécution asynchrone de tâches (la seconde dépendant de la première). Personnellement, une autre fonctionnalité a attiré mon attention ces jours-ci et m’a paru digne d’intérêt pour toute personne qui aura besoin de diffuser son script à des populations ne comprenant pas forcément toutes la même langue.
Il s’agit de la possibilité de personnaliser un script de manière à ce que les messages qu’il affiche soient automatiquement choisis en fonction de la langue de l’utilisateur. En fait, on peut même adapter d’autres variables que les messages, mais dans la pratique ce sont essentiellement les messages destinés à l’utilisateur qu’on voudra « localiser » (anglicisme fréquemment utilisé pour décrire les différents travaux d’adaptation d’un logiciel aux particularités linguistiques et autres d’un pays). Cette possibilité, dans PowerShell, s’appelle « script internationalization ». Vous pourrez en trouver une description relativement sommaire dans les « release notes » de la v2 CTP3.
La fonctionnalité de « script internationalization » repose sur une autre fonctionnalité, la gestion de « DATA sections ». Il s’agit de blocs de données isolés du code pour faciliter la lecture du script et sa maintenance éventuelle. Un bloc de donnée est déclaré avec le mot-clé spécial DATA, suivi dans sa forme la plus simple d’un nom de variable (sans le signe $) et du contenu de cette variable. La syntaxe complète et plus d’informations sont accessibles depuis PowerShell en tapant :
help about_data_sections
Mais passons directement à l’utilisation concrète de blocs de données dans un script que nous voudrons offrir en plusieurs langues.
Soit le script suivant, qu’on appellera GuessADay.ps1 :
# culture="fr-FR" par défaut
Data day {
ConvertFrom-StringData @'
d1 = lundi
d2 = mardi
d3 = mercredi
d4 = jeudi
d5 = vendredi
d6 = samedi
d7 = dimanche
'@
}
Data messages {
ConvertFrom-StringData @'
Welcome = Bienvenue au jeu "Devinez un jour" !
Prompt = Tapez un nom de jour
Success = Vous avez deviné ! Toutes nos félicitations !
'@
}
Data errors {
ConvertFrom-StringData @"
NotExist = {0} n'existe pas. La bonne réponse était {1}.
WrongDay = Erreur! La bonne réponse était {0}.
"@
}
# importer les données traduites :
Import-LocalizedData -bindingvariable day -filename day -ea silentlycontinue
Import-LocalizedData -bindingvariable messages -filename messages -ea silentlycontinue
Import-LocalizedData -bindingvariable errors -filename errors -ea silentlycontinue
# Construire un tableau avec les noms de jours :
$days = $day.d1, $day.d2, $day.d3, $day.d4, $day.d5, $day.d6, $day.d7
# Choisir un jour au hasard :
$computerday = get-random $days
# accueillir le joueur et lui proposer de saisir un nom de jour :
$messages.Welcome
$userday = read-host $messages.Prompt
# vérifier le nom saisi et afficher un message en conséquence :
switch ($userday)
{
$computerday { $messages.Success; break }
{$days -notcontains $_} { $errors.NotExist -f $userday,$computerday; break }
default { $errors.WrongDay -f $computerday }
}
Il s’agit d’un petit jeu extrêmement simple et totalement stupide. L’ordinateur choisit un jour de la semaine au hasard et demande à l’utilisateur de le deviner.
L’intérêt de ce script est de montrer comment on peut charger des messages adaptés à la langue de l’utilisateur, telle qu’elle est définie dans la session PowerShell en cours.
En l’occurrence, les messages du script GuessADay.ps1 ont été regroupés dans trois variables :
Data day {
ConvertFrom-StringData @'
d1 = lundi
d2 = mardi
d3 = mercredi
d4 = jeudi
d5 = vendredi
d6 = samedi
d7 = dimanche
'@
}
La première variable, $day, est un tableau contenant les noms des jours de la semaine. $day.d1 = "lundi", $day.d2 = "mardi", etc. Vous noterez la syntaxe du bloc DATA, un peu particulière. Je vous rappelle que vous trouverez plus d’explications dans l’aide en ligne (cf. plus haut dans ce billet).
Data messages {
ConvertFrom-StringData @'
Welcome = Bienvenue au jeu "Devinez un jour" !
Prompt = Tapez un nom de jour
Success = Vous avez deviné ! Toutes nos félicitations !
'@
}
La deuxième variable, $messages, contient les messages utilisés au cours du jeu. $messages.Welcome est un message de bienvenue, $messages.Prompt est le message utilisé pour inviter le joueur à saisir un nom de jour, et $messages.Success est le message affiché en cas de succès.
Data errors {
ConvertFrom-StringData @'
NotExist = {0} n'existe pas. La bonne réponse était {1}.
WrongDay = Erreur! La bonne réponse était {0}.
'@
}
Enfin, la troisième variable, $errors, contient les messages d’erreur. Je les ai isolés des messages standards, notamment pour mettre en évidence une technique particulière qui est souvent utile pour les messages d’erreurs, à savoir l’inclusion de paramètres dans le message. Ici, $errors.NotExist est utilisé si le jour saisi ne correspond à aucun nom de jour connu, et $errors.WrongDay est utilisé si le jour saisi n’est pas celui que l’ordinateur avait choisi.
L’inclusion de paramètres utilise la technique de formatage de chaînes de caractères avec l’opérateur -f. Chaque paramètre est identifié dans le texte par son numéro d’ordre inséré entre crochets : {0} pour le premier paramètre, {1} pour le deuxième, et ainsi de suite. Au moment de l’affichage du message, il suffit de passer les paramètres à la suite du message et de l’opérateur -f :
$errors.NotExist -f $userday,$computerday
La ligne ci-dessus va afficher le message $errors.NotExist, en remplaçant {0} par $userday et {1} par $computerday. Et voilà. J
Tout cela est bien joli, mais tous les message de ce script sont en français. Alors, où est la traduction automatique tant vantée ?
Patientez, j’y viens ! En fait, il va nous falloir créer quelques fichiers supplémentaires, et même un (ou plusieurs) répertoire(s) supplémentaire(s). Prenons un exemple simple où nous voulons diffuser notre script à des américains. S’ils l’exécutent tel quel, ils risquent fort de ne pas comprendre grand-chose. On va donc créer des fichiers qui contiendront des traductions en anglais des différents messages du jeu.
Pour cela, on va d’abord créer un répertoire \en-US dans le répertoire où le script est stocké. Pourquoi « en-US » ? Parce que c’est le nom de la culture (l’ensemble des paramètres régionaux) retournée par un poste configuré pour un américain. On peut voir le nom de la culture utilisée pour l’interface de l’utilisateur en cours en tapant la commandelette suivante :
PS> get-UICulture
LCID Name DisplayName
---- ---- -----------
1036 fr-FR Français (France)
L’exemple ci-dessus correspond à la configuration classique d’un utilisateur français. Chez un américain, la même commandelette devrait retourner un résultat différent :
PS> get-UICulture
LCID Name DisplayName
---- ---- -----------
1033 en-US English (United States)
PowerShell va se servir du nom de la culture (“fr-FR” pour un français, "en-US") pour déterminer dans quel répertoire aller chercher les données spécifiques à cette culture. Cette opération est réalisée par la commandelette import-localizeddata :
Import-LocalizedData -bindingvariable day -filename day -ea silentlycontinue
Import-LocalizedData -bindingvariable messages -filename messages -ea silentlycontinue
Import-LocalizedData -bindingvariable errors -filename errors -ea silentlycontinue
Ici, le script réalise trois opérations d’importation distinctes, une par variable ($day, $messages et $errors). Chaque variable est stockée dans un fichier différent dont le nom est précisé après le paramètre –filename. L’extension du fichier n’est pas précisée, mais il s’agira forcément d’un fichier utilisant l’extension .psd1. On aura donc trois fichiers dans le répertoire \en-US :
PS> dir en-US
Directory: C:\Users\janel\documents\tests\GuessADay\en-US
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 16/01/2009 16:44 184 day.psd1
-a--- 16/01/2009 17:13 176 errors.psd1
-a--- 16/01/2009 16:49 193 messages.psd1
Si le script ne contenait qu’un seul bloc de données, on aurait eu un seul fichier de traduction à gérer. Dans ce cas, on pouvait ne pas préciser le nom de fichier et il suffisait de stocker les messages traduits dans un fichier .psd1 reprenant le nom du script (guessaday.psd1).
Mais voyons maintenant à quoi ressemblent les fichiers contenant les versions américaines de nos messages :
PS> dir en-US | foreach {"`n$_ :`n"; get-content $_.pspath}
day.psd1 :
# culture="en-US"
ConvertFrom-StringData @'
d1 = Monday
d2 = Tuesday
d3 = Wednesday
d4 = Thursday
d5 = Friday
d6 = Saturday
d7 = Sunday
'@
errors.psd1 :
# Culture="en-US"
ConvertFrom-StringData @'
NotExist = There is no such day as {0}. The correct answer was {1}.
WrongDay = Wrong! The correct answer was {0}.
'@
messages.psd1 :
# Culture "en-US"
ConvertFrom-StringData @'
Welcome = Welcome to the "Guess a day" game!
Prompt = Enter the name of any day
Success = You had it right! Congratulations!
'@
Dans chaque fichier on retrouve la même structure que dans le bloc de données correspondant, à ceci près qu’on ne reprend pas la déclaration "Data …". En fait, le fichier toute entier est lui-même considéré comme un bloc de données, et c’est l’opération d’importation qui s’occupe de faire le lien avec la variable correspondante.
Notez que la ligne de commentaire précisant la culture du fichier est tout à fait facultative. Je l’ai juste mise en tête de chaque fichier à titre d’information.
Une fois que tout ça est en place, il ne reste plus qu’à diffuser le script et ses fichiers de traduction (en faisant attention de toujours respecter l’arborescence avec les noms de culture).
Sur un poste français, voici ce que pourra donner une phase de jeu :
PS> .\guessaday
Bienvenue au jeu "Devinez un jour" !
Tapez un nom de jour: lundi
Erreur! La bonne réponse était dimanche.
Et le même script, sur un poste américain :
PS> .\guessaday
Welcome to the "Guess a day" game!
Enter the name of any day: Tuesday
You had it right! Congratulations!
Voilà. A vous de trouver un usage un tantinet plus utile. Evidemment, vous pouvez ajouter autant de répertoires que vous aurez de traductions à offrir, en faisant toujours attention de bien nommer les répertoires du nom de la culture à laquelle ils correspondent. Et n’oubliez pas que les messages utilisés dans le script seront ceux utilisés par défaut si aucun répertoire n’existe pour la langue de l’utilisateur. Dans mon exemple j’ai utilisé le français par défaut, mais il est possible que vous ayez à utiliser l’anglais par défaut et à fournir le français en traduction dans un répertoire fr-FR.
A vous de jouer !
Janel January 01 Dix lignes de Bling?En ce tout premier jour de l’année 2009, un nouveau script PowerShell fort intéressant a été posté par Clint Huffman sur Codeplex :
Bling.ps1 permet de créer un graphique à partir d’un log créé par Perfmon, l’outil de suivi de performances de Windows. Les logs sont acceptés au format BLG ou CSV. Chaque compteur de performance présent dans le log est représenté par une ligne sur le graphique.
La page ci-dessus fournit toutes les indications pour installer et utiliser le script. Je n’ai pas encore eu le temps de le tester, mais je ne manquerai pas de vous faire part de mes remarques le cas échéant. N’hésitez pas à laisser les vôtres en commentaire !
Même si dans sa v1 le script a quelques limitations, il s’agit à l’évidence d’une belle réalisation qui pourra inspirer de nombreux scripteurs.
Profitez bien, et comme dirait l’autre : allez 2009 !
Janel December 06 La fin de l'impérialisme a sonné pour les CSVQuelle bonne nouvelle ! Hier j’ai reçu une série de mails de Microsoft Connect m’indiquant que mon Feedback #152145 avait été rouvert et résolu.
Si cela ne vous parle pas, ne raccrochez pas tout de suite. Laissez-moi vous expliquer les choses dans l’ordre :
Microsoft Connect est le site web que Microsoft utilise pour échanger avec les utilisateurs de ses produits en cours de développement. On y accède à l’adresse https://connect.microsoft.com. A ma connaissance, il faut avoir été invité à participer à un programme de test pour pouvoir se connecter, mais il est possible que certains programmes soient ouverts à tout le monde.
Une des méthodes d’échange proposées sur ce site est la soumission de Feedback par les utilisateurs d’un produit. Ce Feedback peut être la soumission d’un bug, ou une suggestion pour l’ajout d’une fonctionnalité.
C’est ce second type de Feedback que j’avais utilisé en juin 2006 pour suggérer à l’équipe Windows PowerShell d’ajouter aux commandelettes import-csv et export-csv la possibilité d’accepter un autre séparateur que la virgule. En effet, ce séparateur n’est pas utilisé par tous les pays dans le monde. Par exemple, celui que nous utilisons en France est le point-virgule. Du coup, pour tout utilisateur ne disposant pas de réglages compatibles avec ceux des USA, les fichiers CSV créés par PowerShell ne pouvaient pas être lus directement dans Excel, et inversement les fichiers CSV générés par Excel ne pouvaient pas être importés directement dans PowerShell.
Le 30 juin 2006 j’ai donc soumis un Feedback, portant le numéro 152145, qui suggérait l’addition de deux paramètres :
-delimiter : ce paramètre permettrait à l’utilisateur de spécifier un caractère particulier à utiliser comme délimiteur,
-useRegionalSettings : ce paramètre, de type Switch, permettrait à l’utilisateur de forcer PowerShell à utiliser le séparateur de liste défini dans les paramètres régionaux de la session en cours.
A l’époque, ce Feedback a été fermé après une rapide évaluation et une réponse polie. Par la suite, le Feedback a été évalué comme « Very Important » par 17 utilisateurs (ce système de vote est une des forces de Connect, qui permet à Microsoft d’apprécier l’importance des Feedbacks remontés et de définir ses priorités en conséquence). Entre temps, le développement de la version 2 de PowerShell a démarré, et après moultes péripéties, j’ai donc reçu hier des mails m’indiquant successivement que mon Feedback était rouvert, qu’il était résolu et de nouveau fermé.
Ayant l’honneur et la chance d’être en possession d’une version récente de PowerShell v2 (à usage interne uniquement) je me suis précipité dessus pour vérifier, et ô joie ! Les commandelettes *-csv ont bien deux formes syntaxiques : une avec un paramètre optionnel –delimiter, l’autre avec un paramètre optionnel –useCulture (de type Switch). Vous noterez au passage la transformation de –useRegionalSettings en –useCulture, totalement appropriée puisque dans l’environnement .NET on parle de Culture pour désigner ce qui sous Windows est appelé Regional Settings.
D’ailleurs, le fichier d’aide des commandelettes donne un moyen simple pour savoir quel est le séparateur de liste configuré pour votre session :
PS> (get-culture).TextInfo.ListSeparator
Je me permets également de signaler la présence de deux autres commandelettes, qui me semble-t-il n’étaient pas présentes dans les versions précédentes de PowerShell : convertFrom-csv et convertTo-csv. Leur usage est similaire à import-csv et export-csv à ceci près qu’elles ne passent pas par un fichier.
J’ignore si la dernière CTP disponible incluait déjà ces modifications. N’ayant pas eu beaucoup l’occasion de me servir de PowerShell jusqu’à ces deux dernières semaines, j’avais un peu perdu le fil des nouveautés. N’hésitez pas à regarder et à laisser l’information en commentaire sur ce billet. Si la CTP n’a pas encore les paramètres décrits ci-dessus, patience… Vous devriez les voir apparaître dans la prochaine version.
A bientôt ?
Janel December 04 Nouveau blog de Microsoft sur PowerShell, en français!Hé oui, c’est bien moi ! Je suis toujours là, même si je n’ai rien publié sur ce blog depuis janvier. En fait, je ne compte pas spécialement reprendre une activité régulière, au contraire. Le but de ce billet est, en quelque sorte, de passer le relais en vous faisant part de la naissance du blog officiel de Microsoft sur Windows PowerShell en français :
Bon, l’URL ci-dessus ne semble pas spécialement francophone. Pourtant, depuis mon navigateur elle m’amène bien au blog francophone de Microsoft. Il est possible que cette URL évolue à l’avenir. Surveillez régulièrement si vous êtes intéressé(e).
Quant à moi, je me retire sur la pointe des pieds. Entre www.powershell-scripting.com, site indépendant et toujours aussi actif qui va bientôt atteindre les 150 000 visites, et le blog de Microsoft ci-dessus, vous avez quelques sources d’information régulières et sérieuses. Il m’arrivera peut-être de continuer à poster ici ou là, mais certainement pas aussi régulièrement que j’ai pu le faire en 2006/2007.
Bien à vous,
Janel December 07 Faire d'une liste plusieurs variables en un seul coupDans mon billet sur l’import et l’export des credentials, j’ai utilisé une syntaxe qui a appelé un commentaire de la part d’Arnaud :
$username,$password = type $filename
Cette ligne suppose que le contenu de $filename va être retourné sous forme d’une liste (System.Array). Le premier élément de la liste sera affecté à la variable $username et l’élément suivant sera affecté à la variable $password.
Le fichier $filename a été créé par le script export-credential.ps1. Regardons de plus près un fichier $filename créé sur mon poste :
PS> type cred.txt
\janel
01000000d06c9ddf0115d1118c7a00c04fc297eb010000000ea455d65181ad438e3639faf77e7ebc0000000002000000000003660000a8000000100
0000009d9b7e4e3f7bcde09d555a4e21d0b670000000004800000a00000001000af004e1681e8ff0a56ccb867796cd63d6db810000000c5a30b7222
450fde060021fa5b4abfa0240000003fa185001ed0132aaf91ecd50e0e49ae4f566b82
Le fichier contient deux lignes : la première représente bien le nom d’utilisateur (\janel) et la seconde est une longue suite de caractères alphanumériques. Il s’agit de la version cryptée du mot de passe. Le fait que la deuxième ligne s’étale sur plusieurs lignes à l’affichage ne veut pas dire qu’il s’agit de plusieurs lignes au sens où PowerShell l’entend dans le contexte d’un fichier texte : une ligne est simplement une suite de caractères terminée par un retour chariot ou par une fin de fichier, quelle que soit sa longueur.
Quand on lit un fichier avec la commande type (alias de la commandelette get-content), celle-ci retourne une liste dont les éléments sont les lignes du fichier. On a donc bien ici deux éléments correspondant aux deux lignes du fichier.
Mais, vous demandez-vous, que se passerait-il si le fichier contenait plus de lignes ? Ah, la bonne question que voilà ! Regardons avec un autre exemple :
PS> $pouce,$index = 1..5
PS> $pouce
1
PS> $index
2
3
4
5
Aïe ! La variable $pouce a bien reçu la valeur 1, premier élément de la liste créée par 1..5. Mais $index a récupéré tous les éléments suivants, de 2 à 5. Si j’avais voulu ne lui affecter que la valeur 2, il aurait fallu soit préciser que je ne voulais que les deux premiers éléments de la liste, soit ajouter une troisième variable pour récupérer les autres valeurs :
PS> $pouce,$index = (1..5)[0..1]
PS> $pouce
1
PS> $index
2
PS> $pouce,$index,$autres = 1..5
PS> $pouce
1
PS> $index
2
PS> $autres
3
4
5
Voilà. A vous de continuer d’explorer ce que vous pouvez faire avec cette syntaxe.
Janel Un avant-goût du prochain livre sur PowerShell en françaisOui, ça y est, les livres sur Windows PowerShell arrivent en français comme s’il en pleuvait ! Enfin, presque. Pour l’instant il n’y en a qu’un de publié, voir pour cela un de mes billets récents sur ce même blog.
Cela dit, voici enfin un avant-goût sur le prochain livre à paraître sur le sujet, et le premier à paraître entièrement rédigé en français :
Si vous avez déjà passé un peu de temps à chercher de l’aide sur PowerShell, vous connaissez sans doute déjà les auteurs du livre : Arnaud Petitjean et Robin Lemesle sont les fondateurs et les principaux animateurs de l’excellent site powershell-scripting.com.
L’article ci-dessus fournit un résumé du livre et un aperçu de la table des matières. Un lien est également fourni vers Amazon pour précommander le livre (37 €). Pour avoir participé à la relecture de l’ouvrage, je peux témoigner de la pertinence des nombreux exemples fournis et de la densité des explications qui accompagnent le lecteur dans une meilleure compréhension des technologies accessibles grâce à PowerShell (.NET Framework, WMI, ADSI, etc).
Bonnes fêtes de fin d’année !
Janel November 29 Formation PowerShell sur l'installation et le paramétrage des produits MicrosoftJ’ai tardé à relayer l’information, mais comme on dit, mieux vaut tard que jamais :
La société ISIMEDIA Systèmes va organiser en interne une formation PowerShell débutant sur 3 jours.
Le but de cette formation est d'amener les ingénieurs systèmes à avoir des bases solides et pouvoir développer leurs propres scripts d'installation et de paramétrage des produits Microsoft. Elle devrait se dérouler aux alentours du 26 décembre 2007, à Paris dans le quartier d’Opéra. Le but n'est pas commercial, mais il s’agit de rentabiliser cette formation qui sera dispensée sur mesure à deux ingénieurs d’ISIMEDIA Systèmes. ISIMEDIA Systèmes cherche deux autres personnes pour compléter les effectifs tout en gardant un minimum d'interaction avec le formateur. A quatre personnes, les trois jours reviendront à 1000€ HT par personne (hors frais de transport, de nourriture et d’hébergement).
Si vous êtes intéressé(e) n'hésitez à contacter : Alban CAOUREN ISIMEDIA Systèmes 01 45 62 04 66 Voilà, j’espère que l’annonce sera utile à au moins deux d’entre vous.
Janel November 27 Je fais dans l'import/export des credentialsNe dit-on pas que les informaticiens – et en particulier les administrateurs système – sont des fainéants, et que c’est pour ça qu’ils écrivent des scripts, pour éviter de se fatiguer à ressaisir une même commande ?
Hé bien, si cette tendance est avérée, voici qui devrait caresser tous les administrateurs système dans le sens du poil. Les deux scripts que je vous propose aujourd’hui s'appellent export-credential et import-credential. Ils permettent de sauvegarder dans un fichier les « credentials » d'un utilisateur de manière à pouvoir les recharger à volonté (par exemple dans un script).
Imaginons qu’on veuille interroger une série de serveurs distants avec get-wmiobject mais que son propre compte n'ait pas accès à ces serveurs: la technique classique consiste alors à utiliser get-credential pour saisir un nouveau couple username/password, et à passer le résultat à get-wmiobject via son paramètre –credential :
PS> $cred = get-credential
PS> get-wmiobject win32_operatingsystem -computer MONSERVEUR -credential $cred
Problème, si on doit répéter la manip régulièrement au cours de sessions PowerShell différentes, voire si on doit inclure get-wmiobject dans un script, l'interactivité de get-credential devient une contrainte. C'est là que mes petits scripts interviennent:
Etape 1: sauvegarde des credentials (à faire une fois pour toutes)
# export-credential.ps1
# v1
# janel, 27/11/07
param ($credential, $filename)
$credential.username > $filename
convertfrom-securestring $credential.password >> $filename
Etape 2: chargement des Credentials (à utiliser à volonté)
# import-credential.ps1
# v1
# janel, 27/11/07
param ($filename)
$username,$password = type $filename
$pwd = convertto-securestring $password
new-object system.management.automation.PSCredential $username,$pwd
Exemple d'usage:
# Une fois pour toutes:
PS> $cred = get-credential
PS> export-credential $cred c:\scripts\admin.txt
# Dans un script qui a besoin d'utiliser les credentials:
$cred = import-credential c:\scripts\admin.txt
get-wmiobject win32_operatingsystem -computer MONSERVEUR -credential $cred
Voilà. L'avantage de cette technique (outre le fait qu'elle autorise la récupération des credentials sans avoir besoin de repasser par le popup Windows) est qu'elle préserve la confidentialité du mot de passe en le stockant sous sa forme cryptée.
La contrainte (liée à l'avantage ci-dessus énoncé) est qu'on ne peut importer les credentials qu'à partir du même compte qui les a saisis avec get-credential. En effet, la chaîne sécurisée contenant le mot de passe est générée à partir d'une clé détenue par le compte actif. Attention donc si vous utilisez import-credential dans un script: vous ne pourrez pas le planifier avec un autre compte. Ou alors, il faudra avoir exécuté la première étape à partir de cet autre compte également.
Profitez bien !
Janel November 20 Le premier livre en français sur PowerShellEt voilà, il fallait que ça arrive un jour : le premier livre sur Windows PowerShell en français est sorti cette semaine.
Il s’agit de la traduction d’un ouvrage d’Ed Wilson, formateur spécialiste des scripts pour l’administration et les réseaux. Le livre s’appelle en français Windows PowerShell, Kit d’administration (éditions Dunod / Microsoft Press).
Apparemment le livre est disponible à la FNAC, et donc j’imagine dans la plupart des bonnes librairies techniques en France. Vous pouvez également l’acheter en ligne, ou simplement en savoir plus sur son contenu, sur le site de l’éditeur :
Prix public : 35 €
En attendant le premier livre écrit en français sur Windows PowerShell, dont la sortie est prévue début février 2008. Plus que deux mois à tenir !
Janel November 16 Enregistrez vos commandes PowerShell dans le journal des évènementsTenez, pour conclure la semaine en beauté, voici une petite info non documentée :
Vous savez sans doute que Windows PowerShell crée un journal spécifique pour l’enregistrement des évènements qui le concernent. Ce journal est consultable notamment via la console de gestion de votre système, sous le nom simple de « Windows PowerShell » (dans la rubrique « Application and Service Logs » sous Vista – à traduire en français si vous n’avez pas un Windows en anglais). Par défaut, ce journal enregistre notamment les activités de démarrage et d’arrêt de la console, ainsi que l’initialisation des différents fournisseurs installés.
Ce que vous ne savez peut-être pas, c’est que vous pouvez également demander à PowerShell d’enregistrer dans ce même journal l’ensemble des commandes qui lui sont transmises. Pour cela, il suffit de taper la commande suivante :
PS> $LogCommandLifecycleEvent = $true
A partir de là, toute commande saisie sera enregistrée. Essayez, tapez une commande, rafraîchissez votre journal d’évènements Windows PowerShell, et découvrez les évènements qui ont été ajoutés.
Attention, absolument toutes les commandes transmises au moteur PowerShell sont enregistrées. Cela inclut les commandelettes ajoutées automatiquement à la fin des pipelines de manière habituellement transparente : format-default et out-default. Et cela inclut également les commandes de la fonction prompt qui est appelée à chaque retour à l’invite de commande. Si votre fonction prompt contient une vingtaine de commandes, vous les verrez toutes générer des évènements à chaque appel à la fonction.
Vous constaterez également que deux évènements sont enregistrés pour chaque commande :
Evènement ID 500 : début de la commande (« started »)
Evènement ID 501 : fin de la commande (« stopped »)
Les détails de l’évènement contiennent le nom de la commande et toutes les autres informations qui vous seront utiles pour pouvoir analyser ce qui a été exécuté.
Voilà, maintenant vous savez presque tout. Ah oui, bien sûr, si vous voulez désactiver cette fonction d’enregistrement, c’est très simple :
PS> $LogCommandLifecycleEvent = $false
Et maintenant, à vous de jouer !
Janel November 15 Mais, ce filtre est une vraie passoire!En testant un bout de script pour répondre à un utilisateur sur un forum, j’ai réalisé que le fonctionnement de where-object est souvent méconnu.
La commandelette where-object (alias : where, ?) est bien connue et fréquemment utilisée pour permettre de filtrer une collection d’objets :
# affiche les processus démarrés depuis moins d’une heure :
get-process | where {$_.StartTime -gt [DateTime]::Now.AddHours(-1)}
Ce qu’on oublie souvent, c’est que son fonctionnement ne s’arrête pas à faire ce filtrage. En réalité, where-object va exécuter le code qui lui est fourni pour tous les objets qui lui sont transmis. A chaque fois que le code exécuté retournera la valeur $true, where-object passera l’objet en cours à la commande suivante dans le pipeline :
# affiche les noms de tous les processus,
# en formatant l’affichage de ceux démarrés depuis moins d’une heure :
get-process | where {
write-host $_.name
$_.StartTime -gt [DateTime]::Now.AddHours(-1)
} | format-table name, starttime
On peut très certainement trouver des applications plus utiles que mon exemple, mais je voulais simplement attirer votre attention sur ce point. A vous de trouver les applications qui vous seront utiles. L’important, déjà, est de savoir comment ça marche ! J
Bon vent (frais),
Janel November 06 Ca y est, la CTP est disponible!Vous pouvez la trouver ici :
Si vous ne l’avez pas encore fait, lisez le billet de Jeffrey Snover sur le blog de l’équipe PowerShell. Il contient des mises en garde très précises sur l’utilisation de la CTP.
A bon entendeur !
Janel November 05 Windows PowerShell v2 arrive en CTPL’équipe PowerShell a annoncé vendredi la disponibilité prochaine de la version CTP de Windows PowerShell v2. CTP veut dire « Customer Technology Preview ». Comme le blog ci-dessous l’explique, il s’agit d’une version de travail loin d’être achevée, dont l’objectif principal est de recueillir les avis d’utilisateurs experts sur les choix technologiques en cours d’implémentation dans ce qui deviendra la v2 de PowerShell à une échéance encore indéterminée.
Jeffrey Snover insiste sur le fait que, bien qu’accessible à tous, la CTP de PowerShell n’est pas forcément à mettre entre toutes les mains. Il est important de lire le message ci-dessus pour comprendre les impacts et les risques potentiels à installer à utiliser cette version. Si vous avez le moindre doute, ne l’installez pas. Si au contraire vous piaffez d’impatience, hé bien… patience. J
Janel October 19 Comparer deux fichiers texte avec PowerShellSur le forum de PowerShell-Scripting, quelqu’un demandait récemment comment faire pour comparer les lignes de deux fichiers texte et obtenir le nombre de caractères différents. Bien sûr, l’utilisation du terme « comparer » m’a sauté aux yeux comme une évidence : compare-object !
Pour des raisons trop longues à expliquer ici (je viens de découvrir que Live.com impose une limite à la taille des billets, limite que j'ai apparemment franchie avec la première version de ce billet), la solution reposant sur compare-object ne marchait pas. Vous trouverez les explications sur le forum en question. Je suis donc arrivé au script suivant :
# -- compare-textfile.ps1 --
param ( $reference, $difference )
$file1 = [System.IO.File]::OpenText((dir $reference)) $file2 = [System.IO.File]::OpenText((dir $difference))
$line = 1
while (!$file1.EndOfStream -or !$file2.EndOfStream) { $line1 = $line2 = "" ($file1.EndOfStream) -or ($line1 = $file1.ReadLine()) > $nul ($file2.EndOfStream) -or ($line2 = $file2.ReadLine()) > $nul
$length = $line1.length if ($line2.length -gt $length) { $length = $line2.length }
$matchline = New-Object PSObject $chararray = @() $diff = 0
for ($i=0;$i -lt $length;$i++) { if ([char]$line1[$i] -ne [char]$line2[$i]) { $diffchars = New-Object PSObject $diffchars | Add-Member -MemberType NoteProperty Position $i $diffchars | add-member -MemberType NoteProperty Reference $line1[$i] $diffchars | add-member -MemberType NoteProperty Difference $line2[$i] $chararray += $diffchars $diff++ } }
$matchline | Add-Member -MemberType NoteProperty Line $line $matchline | Add-Member -MemberType NoteProperty Different $diff $matchline | Add-Member -MemberType NoteProperty Equal ($length - $diff) $matchline | Add-Member -MemberType NoteProperty Characters $chararray $matchline
$line++ }
$file1.Close() $file2.Close()
# -- fin de script --
Imaginons deux fichiers texte simples :
PS> type ref.txt HELLO WORLD HOULA HOUP
PS> type dif.txt HELLO WORLD! HOULA, HOUPS...
PS> compare-textfile ref.txt dif.txt
Line Different Equal Characters ---- --------- ----- ---------- 1 1 11 {@{Position=11; Reference=... 2 10 5 {@{Position=5; Reference= ...
Le résultat s’affiche ligne par ligne. On peut voir que sur la première ligne on a un caractère de différent et onze caractères égaux (Equal). Sur la deuxième ligne, j’ai dix caractères différents et cinq égaux. Mais quelles sont ces différences ? Et qu’est-ce que c’est que ce charabia à la fin de chaque ligne ?
PS> $comp = compare-textfile ref.txt dif.txt PS> $comp[0].characters
Position Reference Difference -------- --------- ---------- 11 !
La propriété Characters d’une ligne (ici, la première) est en fait un tableau qui contient la position de chaque différence, et pour chaque position le caractère de référence (celui présent ou pas à la position donnée dans le premier fichier passé en paramètre) ainsi que le caractère de différence (celui présent ou pas à la même position donnée dans le deuxième fichier passé en paramètre).
Attention : les numéros de ligne commencent à 1, mais les positions dans chaque ligne comment à 0. J’ai fait ce choix pour suivre les conventions les plus fréquemment rencontrées dans les numérotations de ligne : ouvrez un fichier dans un éditeur de texte évolué, vous verrez que la numérotation des lignes commence toujours à 1. A l’inverse, la numérotation des caractères dans une chaîne PowerShell commence toujours à 0.
Mais revenons à notre exemple. Regardons maintenant la deuxième ligne :
PS> $comp[1].characters
Position Reference Difference -------- --------- ---------- 5 , 6 H 7 O H 8 U O 9 P U 10 P 11 S 12 . 13 . 14 .
L’insertion de la virgule en cinquième position a déplacé la fin du texte ce qui provoque une cascade de différences jusqu’aux points de suspension.
Très bien, mais le problème initial consistait à obtenir le nombre total de différences entre les deux fichiers. Or, ici je n’affiche les différences que ligne par ligne. Bigre, comment faire ? Voyons voir :
PS> compare-textfile ref.txt dif.txt | measure-object different –sum
Janel October 09 Un éditeur de scripts PowerShell gratuit et super efficaceVoilà une bonne nouvelle qui fait plaisir ! La dernière version de PowerGUI, console d’administration en mode graphique basée sur PowerShell, inclut maintenant un éditeur de scripts à la fois simple et efficace, le PowerGUI Script Editor :
Parmi les fonctionnalités les plus pratiques que j’ai pu relever après quelques minutes d’utilisation :
Pour compléter ce tableau, trois excellentes nouvelles :
Alors, qu’attendez-vous ? J
Janel October 03 Je veux ajouter un bouton Imprimer à mon Windows Form créé à partir de PowerShellIl y a quelque temps sur le forum de PowerShell-Scripting un utilisateur a demandé comment il pouvait imprimer un Windows Form qu’il avait créé à partir de PowerShell. Le premier lot de réponses qu’il a reçues tournaient autour de la possibilité de faire une capture de la fenêtre dans le presse-papiers de Windows, et d’envoyer le contenu du presse-papiers vers l’imprimante. La solution est ingénieuse, mais elle repose sur plusieurs points difficiles à résoudre. En premier lieu, il faut pouvoir gérer le presse-papiers, ensuite il faut pouvoir interagir avec l’imprimante et notamment savoir lui transmettre l’image capturée dans un format qu’elle comprenne.
Dans une telle situation, on peut aussi ajouter au formulaire un bouton "Imprimer" qui appellera les méthodes du Framework .NET qui vont bien. Il existe un exemple sur MSDN pour imprimer un Windows Form, mais le code fourni n’est évidemment pas en PowerShell mais en VB.Net et en C#. J'ai donc dû gratter un peu pour pouvoir le traduire en PowerShell. Voici ce que ça donne - le code qui suit suppose que le Windows Form s'appelle $form :
# [void] [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")[void] [Reflection.Assembly]::LoadWithPartialName("System.Drawing") $printClick = { $myGraphics = $form.CreateGraphics() $size = $form.Size $script:memimage = new-object System.Drawing.Bitmap $form.Width, $form.Height, $myGraphics $memgraphics = [System.Drawing.Graphics]::FromImage($script:memimage) $memgraphics.CopyFromScreen($form.Location.X, $form.Location.Y, 0, 0, $size) $printdoc.Print()} $PrintPage = { $_.Graphics.DrawImage($script:memimage, 0, 0)} $printDoc = New-Object System.Drawing.Printing.PrintDocument$printDoc.Add_PrintPage($PrintPage) $printButton = New-Object System.Windows.Forms.Button$printButton.Text = "Imprimer"$printButton.Add_Click($printClick) # $form = New-Object System.Windows.Forms.Form$form.Controls.Add($printButton)# $form.ShowDialog() Les lignes commentées sont simplement là pour rappeler ce qui doit être fait par ailleurs pour créer et afficher le Windows Form. Le script existant doit déjà contenir des lignes équivalentes.
Au-delà du besoin spécifique de lancer une impression, cet exemple vous montrera également comment associer une action à un évènement qui survient sur un contrôle. Ici, nous avons l’action d’impression initiée par un clic sur le bouton $printButton et dans cette action d’impression la « sous-action » d’impression d’une page initiée par l’appel à la méthode Print() de l’objet $printDoc, respectivement associées aux blocs de commandes $printClick et $printPage.
Il ne vous reste plus qu’à l’adapter à vos propres besoins et à laisser libre cours à votre fantaisie !
Janel October 01 Infos photos (et plus) avec PowerShellEn parcourant la collection de scripts PowerShell qui commence à envahir mon disque dur, j’ai retrouvé ce script, get-filemetadata.ps1, qui récupère les méta-données d’un fichier. Ces méta-données sont des informations sur le contenu d’un fichier, généralement renseignées par l’application qui l’a créé.
Ainsi, les méta-données d’un document Word incluent le titre (information à distinguer du nom du fichier), le nombre de mots, le nombre de pages ou encore le temps total passé à éditer le document (pas sûr que cette information soit totalement pertinente). Les méta-données d’une photo numérique incluent la résolution de l’image, le temps de pose, l’ouverture du diaphragme ou encore le matériel utilisé.
Je ne suis pas l’auteur de ce script. Je me rappelle avoir vu ce sujet traité sur plusieurs blogs (celui de MOW notamment, ainsi que le blog d’un membre de l’équipe PowerShell) mais le script que j’ai n’étant pas signé il m’est impossible d’identifier à coup sûr son origine. Qui plus est, il est probable que je l’aie personnalisé après l’avoir téléchargé. Bref, la version que voici est – sauf preuve du contraire – le premier « script traditionnel » publié ici, comme on a des « chants traditionnels » dans notre patrimoine :
# get-filemetadata.ps1
# # Auteur: traditionnel, adapté par janel
#
begin {
function emitMetaInfoObject($path) {
[string]$path = (resolve-path $path).path
[string]$dir = split-path $path
[string]$file = split-path $path -leaf
$shellApp = new-object -com shell.application
$myFolder = $shellApp.Namespace($dir)
$fileobj = $myFolder.Items().Item($file)
$metaInfoObj = new-object psobject
$metaInfoObj.psobject.typenames[0] = "Custom.IO.File.Metadata"
for ( $i=0 ; $i -lt 300; $i++) {
$n = $myFolder.GetDetailsOf($null, $i)
if ($n) {
$v = $myFolder.GetDetailsOf($fileobj,$i)
if ($v) {
$metaInfoObj | add-member noteproperty $n $v
}
}
}
$metaInfoObj
}
}
process {
if ($_) {
emitMetaInfoObject $_.Fullname
}
}
end {
if ($args) {
$paths = @()
foreach ($path in $args) {
if (!(test-path $path)) {
write-error "$path is not a valid path"
}
$paths += resolve-path $path
}
foreach ($path in $paths) {
emitMetaInfoObject $path
}
}
}
# end of script
Voyons un peu ce que ça donne sur un album de Lloyd Cole :
PS> dir | get-filemetadata | format-table artists,year,album,title,length -auto
Artists Year Album Title Length
------- ---- ----- ----- ------
Lloyd Cole and the Commotions 1985 Easy Pieces Rich 00:04:22
Lloyd Cole and the Commotions 1985 Easy Pieces Why I Love Country Music 00:03:00
Lloyd Cole and the Commotions 1985 Easy Pieces Pretty Gone 00:03:32
Lloyd Cole and the Commotions 1985 Easy Pieces Grace 00:04:05
Lloyd Cole and the Commotions 1985 Easy Pieces Cut Me Down 00:04:29
...
Sur des photos, ça donne les résultats suivants :
PS> dir *.jpg | get-filemetadata | ft name, "date created", "camera model", "focal length", "f-stop", exposure* -a
Name Date created Camera model Focal length F-stop Exposure bias Exposure time
---- ------------ ------------ ------------ ------ ------------- -------------
001.JPG 13/01/2007 17:21 NIKON D50 55 mm f/5,6 0 step 1/60 sec.
Aéroport 2.jpg 10/03/2007 09:37 NIKON D50 40 mm f/4,5 0 step 1/125 sec.
Aéroport.jpg 10/03/2007 09:34 NIKON D50 40 mm f/4,5 0 step 1/125 sec.
BW 1.jpg 28/02/2007 10:53 NIKON D50 35 mm f/2 -0,3 step 1/250 sec.
BW 2.jpg 28/02/2007 11:00 NIKON D50 35 mm f/2 -1 step 1/250 sec.
...
Maintenant, si vous voulez vraiment exploiter les données ci-dessus à des fins statistiques par exemple, comme calculer la durée totale de tous les morceaux de musique de votre compositeur préféré, ou encore estimer la longueur de focale moyenne de vos photos, vous devrez appliquer un petit traitement aux données retournées par le script. Rien de bien méchant, mais en gros il faudra enlever le texte superflu (comme les ‘mm’ de la longueur focale) ou encore convertir le format utilisé pour l’affichage en une valeur compréhensible par PowerShell (comme ’00:04:22’ pour la durée d’un morceau de musique).
De plus, ce script est conçu pour traiter les méta-données de n’importe quel type de fichier. L’avantage est évident, mais si vous manipulez des photos couramment, vous devez savoir que les méta-données qu’une photo numérique peut stocker sont considérablement plus nombreuses. Ces données, qu’on appelle également données EXIF, peuvent être visualisées dans la plupart des logiciels de traitement d’images, ou encore en ligne de commande avec un outil gratuit fabuleux : ExifTool.
ExifTool permet de très nombreuses choses qu’il serait difficile d’implémenter dans un script PowerShell sans avoir recours à une bibliothèque de fonctions externe. Je n’ai pas encore trouvé la bibliothèque en question, ni trouvé le courage ou le temps de m’y coller, alors voici une méthode à deux balles pour – au moins – exploiter les informations retournées par ExifTool depuis un script PowerShell :
filter get-exifdata {
$img = new-object PSObject
exiftool $_.FullName | foreach {
$n,$v = $_.split(":",2) | foreach {$_.trim()}
$img | add-member NoteProperty $n $v -ea SilentlyContinue
}
$img
}
Pour que la fonction ci-dessus marche, il faut que vous ayez téléchargé et copié exiftool.exe dans un répertoire déclaré par votre PATH.
Vous pouvez ensuite l’utiliser ainsi :
PS> dir *.jpg | get-exifdata
Tel quel, l’affichage risque d’être assez rébarbatif et finalement peu différent de ce que vous donnerait ExifTool directement. Mais comme pour get-filemetadata, on pourra utiliser les commandelettes standard pour filtrer, trier, mesurer, grouper et formater les données émises. Déjà, essayons cela :
PS> dir *.nef | get-exifdata | ft "focal length",aperture,"focus distance",depth* -a
Focal Length Aperture Focus Distance Depth of Field
------------ -------- -------------- --------------
200.0mm 5.6 33.50 m 6.31 m (30.64 - 36.95)
70.0mm 11.0 21.13 m 389.65 m (10.85 - 400.50)
18.0mm 11.0 1.68 m inf (0.79 m - inf)
18.0mm 11.0 1.68 m inf (0.79 m - inf)
18.0mm 11.0 1.68 m inf (0.79 m - inf)
...
On constate qu’ExifTool obtient des informations sur des fichiers RAW (*.NEF est l’extension de fichier pour le format RAW de Nikon) que get-filemetadata ne sait pas obtenir – ici par exemple, « Focus Distance » qui indique la distance entre l’appareil photo et le point sur lequel la mise au point a été faite, et « Depth of Field » qui indique la profondeur de champ avec entre parenthèses le point le plus proche et celui le plus distant entre lesquels la netteté est considérée comme acceptable.
En faisant une petite manipulation sur les valeurs de la propriété « Focus Distance », on pourra récupérer un nombre réutilisable pour nos propres calculs. Voici comme dernier exemple une ligne de commande qui va mesurer la distance de mise au point moyenne sur toutes les photos (au format RAW) dans le répertoire en cours :
PS> dir *.nef | get-exifdata | select @{n="Distance";e={$_."Focus Distance".Split()[0]}} | measure-object distance –average –min -max
Count : 186
Average : 10,8857526881721
Sum :
Maximum : 33,5
Minimum : 0,5
Property : Distance
Voilà, maintenant je sais que sur les 186 photos de cette série, en moyenne j’ai mis au point à 10,88m de distance. Essentiel, non ?
A vous de jouer!
Janel September 28 Je redirige les objets PowerShell dans un fichier ExcelAu cours de l’été j’avais commencé une série de billets sur quelques techniques de base pour interagir avec Word à partir de PowerShell. Voici un script montrant une technique simple d’interaction avec Excel. Ce script, baptisé out-excel, récupère les objets émis par le pipeline et les sauvegarde dans un fichier Excel.
La manipulation repose sur un format intermédiaire : le format CSV. PowerShell permet d’exporter une collection d’objets au format CSV avec la commandelette standard export-csv, et il se trouve que ce format est reconnu par Excel. Je peux donc :
Il est possible que vous ayez des problèmes à l’exécution de ce script. Notamment :
Quoi qu’il en soit, voici le script :
# out-excel.ps1
#
# Auteur: janel
# Date: 28/09/2007
#
# Redirige les objets émis par le pipeline dans un fichier Excel
#
# Usage:
#
# <commande> | out-excel [-filename] <fichier> [-force] [-autofit] [-header]
#
# -filename indique le nom du fichier Excel à utiliser
# -force spécifie qu'un fichier existant du même nom sera écrasé
# -autofit ajuste automatiquement la taille des colonnes à leur contenu
# -header met la première ligne en gras et bloque son affichage
#
# Exemple:
#
# get-process | select name,company,path,{$_.mainmodule.fileversion} | out-excel process.xls -force -h -a
#
# ... crée un fichier process.xls dans le répertoire courant avec les informations
# souhaitées sur les processus en cours d’exécution.
#
param (
[String]$filename,
[switch]$force,
[switch]$autofit,
[switch]$header
)
# ajouter le répertoire en cours au nom de fichier si non précisé:
if (!(split-path $filename)) {$filename = join-path $pwd.path $filename}
# créer un fichier temporaire pour stocker les données au format CSV:
$tempfile = [System.IO.Path]::GetTempFileName()
$data = @($input)
$data | export-csv $tempfile -notypeinformation
# utiliser l'objet COM Excel:
$excel = new-object -com Excel.Application
# ouvrir le fichier CSV dans Excel:
$xlDelimited = [Microsoft.Office.Interop.Excel.xlTextParsingType]::xlDelimited
$xlTextQualifierNone = [Microsoft.Office.Interop.Excel.XlTextQualifier]::xlTextQualifierNone
$m = [System.Type]::Missing
$csv = $excel.Workbooks.OpenText($tempfile, $m, 1, $xlDelimited, $xlTextQualifierNone, $m, $m, $m, $true, $m, $m, $m, $m, $m, $m, $m, $m, $m)
# appliquer les modifications optionnelles:
$workbook = $excel.ActiveWorkbook
if ($autofit) {[void]$workbook.ActiveSheet.Columns.AutoFit()}
if ($header)
{
$workbook.ActiveSheet.Rows.Item(1).Font.Bold = $true
$workbook.Activate()
$excel.ActiveWindow.SplitRow = 1
$excel.ActiveWindow.FreezePanes = $true
}
# supprimer un fichier du même nom s'il existe:
if ($force) {del $filename -ea SilentlyContinue > $null}
# sauvegarder le fichier au format Excel:
$xlWorkbookNormal = [Microsoft.Office.Interop.Excel.xlFileFormat]::xlWorkbookNormal
$workbook.SaveAs($filename, $xlWorkbookNormal)
remove-item $tempfile
# faire le ménage:
$excel.WindowState = "xlMinimized"
$excel.visible = $true
stop-process @(get-process | where {$_.MainWindowHandle -eq $excel.Hwnd})[0].Id
# fin du script
Commentaires bienvenus !
Janel |
|
|