| Jacques's profilejanelPhotosBlogLists | Help |
|
March 28 PowerShell sera inclus dans Windows Server (Longhorn)Ah là là ! Quelle bonne nouvelle ce matin, y’a des matins comme ça ! J
Au Microsoft Management Summit qui se tient en ce moment à San Diego, Microsoft a annoncé que Windows PowerShell sera inclus en standard dans la prochaine version de Windows Server, nom de code Longhorn. PowerShell sera disponible dès la Beta 3 à venir. C’est pas beau, ça ?
Cette décision importante est en grande partie liée au succès rencontré par PowerShell depuis sa sortie en novembre dernier : plus de 400 000 téléchargements, une douzaine de livres (aucun en français malheureusement), un nombre sans cesse croissant de partenaires qui fournissent des extensions, etc.
Vous pouvez retrouver l’annonce formelle sur le blog de l’équipe PowerShell :
Bonne journée !
Janel March 27 Comment éviter de traiter le cas de $null dans une boucle PowerShellJ’avais déjà écrit sur ce sujet il y a quelques temps, mais une question récente de Keith Hill sur le forum US dédié à PowerShell a remis le sujet sur la table, alors un petit rappel ne fera sans doute pas de mal…
Imaginons que j’écrive une fonction foo qui retourne un certain nombre d’éléments, parfois un, parfois plusieurs, et parfois aucun. Pour illustrer notre propos je me contenterai de faire une fonction foo qui retourne les fichiers du répertoire en cours selon le filtre passé en argument.
Imaginons encore que je récupère le résultat dans une variable et que j’utilise cette variable dans une boucle pour appliquer un traitement à chaque élément de cette variable. Jusque là, tout va bien. Sauf si ma fonction foo n’a retourné aucun résultat. Dans ce cas, la boucle sera quand même exécutée une fois, avec un résultat imprévisible puisque son contenu est nul ($null pour être plus exact). Mettons-nous en situation :
PS> function foo {dir $args[0]}
PS> $docs = foo *.txt
PS> foreach ($doc in $docs) {$doc.get_name()}
Culture.txt
ProgramFiles.txt
PS> $docs = foo *.ppt
PS> foreach ($doc in $docs) {$doc.get_name()}
You cannot call a method on a null-valued expression.
At line:1 char:39
+ foreach ($doc in $docs) {$doc.get_name( <<<< )}
Alors, que se passe-t-il? Et comment éviter cela ?
D’abord, ce qui se passe… PowerShell traite $null comme une valeur bel et bien existante, de valeur nulle. Cela permet par exemple de tester la valeur (if ($docs -eq $null {…}).
Par contre, il faut bien comprendre que cette valeur est scalaire. Or, dans le cas qui nous occupe ici comme dans la plupart des cas où cette histoire de $null pose problème, on s’attend en fait à récupérer une liste d’éléments. Si, au moment où on récupère le résultat de la fonction, on précise explicitement qu’on souhaite récupérer une liste, PowerShell saura alors créer une liste vide dans le cas où aucun élément n’est retourné. La grande différence entre une liste vide et la valeur $null, c’est que si ensuite on passe la variable comme compteur pour une boucle, en tant que liste vide elle est considérée comme ayant 0 élément, alors qu’en tant que valeur $null elle est considérée comme ayant 1 élément : $null.
Pour déclarer explicitement qu’on récupère une liste, on pourra encadrer l’appel à la fonction de l’opérateur désignant une liste : @(). Illustration :
PS> $docs=@(foo *.ppt)
PS> foreach ($doc in $docs) {$doc.get_name()}
PS>
Et voilà. J
Janel March 25 De l'art de traiter les champs d'un fichier CSVSur le forum français de Microsoft dédié au Scripting, ---DGI972--- soumettait un nouveau problème sous cette forme :
> Bonjour,
> > Je reçois un fichier de type csv ou il y a des blancs (code 20 hexa) > soit à droite soit à gauche selon les champs. Ce remplissage inutile me > bloque dans l'interprétation de ce fichier. il faudrait que je supprime > tous les blancs du fichier a l'extrême droite ou gauche du champ. > Je n'ai vraiment aucune idée comment aborder le sujet. > > Merci de m'aiguiller un peu S’ensuit une discussion très longue sur la base d’une solution en VBScript qui s’appuie sur les expressions régulières, puis sur des variations permettant d’améliorer les performances et de pallier à certains cas spécifiques. J’ai pris le train avec tellement de retard que je n'ai pas essayé de monter dedans. A la place, j’ai profité de cet « exercice imposé » pour présenter quelques points de technique en PowerShell. Le script d'abord (très succinct), les explications ensuite:
type fic.csv | %{$line=$_.split(";”) | %{$_.trim()}; [string]::join(“;”,$line)}
L'idée de base est très simple: on parcourt chaque ligne du fichier fic.csv; pour chaque ligne on splitte les champs selon le séparateur point-virgule; pour chaque champ récupéré on applique la méthode Trim() qui consiste à supprimer les blancs en début et en fin de texte; et pour finir on reconstitue la ligne en joignant les champs par un point-virgule avec la méthode statique [System.String]::Join(). Et on recommence pour la ligne suivante... J'aurais pu l'écrire sur plusieurs lignes, avec une indentation qui permettait de comprendre plus facilement l'imbrication des boucles, et en remettant foreach à la place de son alias %: type fic.csv | foreach {
$line = $_.split(";") | foreach { $_.trim() } [string]::join(";",$line) } Avantage de présenter le code indenté, on voit bien qu’à la place de la ligne $_.trim() on pourrait appliquer n’importe quel traitement devant s’appliquer à chaque champ (par exemple une substitution de termes, une traduction, un calcul, etc). Mais la possibilité de tester PowerShell en mode interactif fait qu'on finit souvent par un enchaînement de commandes sur une seule ligne, et la première version ci-dessus est exactement ce que j'ai tapé après quelques tâtonnements.
J'aurais également pu me passer d'une variable intermédiaire $line en appliquant la méthode statique [System.String]::Join() à l'ensemble des instructions de traitement de chaque ligne, mais le résultat perd en lisibilité ce qu'il gagne en longueur (enfin, c'est du moins mon impression ;-)): type fic.csv | %{[string]::join(";",($_.split(";") | %{$_.trim()}))}
En fait, au départ j'avais un problème avec la syntaxe de la méthode Join() qui semblait ne pas marcher - en fait, j'avais simplement inversé l'ordre des paramètres - alors ma toute première version correcte a été ceci: $oldofs=$ofs; $ofs=";"
type fic.csv | %{$line=$_.split(";") | %{$_.trim()}; "$line"} $ofs=$oldofs L'unique différence tient à la technique d'affichage de la ligne débarrassée de ses blancs inutiles. Au lieu de me servir de [string]::join(), je me contente d'afficher la variable $line entre guillemets, ce qui force PowerShell à rassembler les éléments de la liste. Par défaut, le délimiteur utilisé pour afficher une liste d'éléments sous cette forme est l'espace. On peut le voir en tapant le code suivant: PS> $liste="hello","world"
PS> $liste hello world PS> "$liste" hello world Mais ce délimiteur est modifiable grâce à la variable standard $OFS. Si l'on affecte le point-virgule à $OFS, une liste d'éléments affichée sous forme de chaîne de caractères devient alors une ligne au format CSV, tout bêtement : PS> $ofs=";"
PS> "$liste"
hello;world
Les assignations en début et en fin de script permettent simplement de mémoriser l'état initial de la variable $OFS et de le restaurer après traitement. Voilà. J'espère que ça vous aura donné quelques éléments supplémentaires pour appréhender les nombreuses possibilités de travail avec PowerShell. A vous de jouer maintenant. Janel March 16 Courir devant l'objectifAu gré de mes lectures je viens de découvrir le travail du photographe muggezifter. Vous ne le connaissez peut-être pas, il n’a certes pas le palmarès d’un David Hamilton, Helmut Newton ou encore la reconnaissance éternelle d’un Henri Cartier-Bresson, mais il a mis en ligne une série de photos sur le site Flickr qui, l’air de rien, fera sans doute date dans l’histoire de la photographie J. L’idée est archi-simple : le photographe pose son appareil quelque part, règle le retardateur sur 2 secondes et court devant l’objectif pour être le plus loin possible au moment du déclenchement. Voilà ce que ça donne, avec à l’heure actuelle 48 photos en ligne :
Au fil des photos on peut voir des commentaires élogieux et même quelques liens montrant que le photographe a fait des émules :
L’idée a fait son chemin, à tel point qu’un regroupement des séries autour du même thème existe :
L’épopée de notre héros a même fait l’objet d’un article dans la presse, en l’occurrence l’édition suisse alémanique de 20 minutes :
En voyant les premières photos je me suis demandé par quel miracle le photographe ne s’était pas fait dérober son équipement. En réalisant qu’il est Suisse, alémanique qui plus est, je comprends mieux… J
Janel Vous me les copierez cent fois...Des lignes de fichier, encore et toujours… Hier je donnais une astuce à deux balles pour rapidement éliminer certaines lignes d’un fichier. Aujourd’hui je répondais sur le forum français dédié au scripting à quelqu’un qui cherchait à copier les n dernières lignes d’un fichier. Contrainte, le nombre total de lignes n’est pas connu à l’avance. Deuxième contrainte, le fichier peut contenir beaucoup, beaucoup de lignes (autour de 800 000). Le parcours de l’intégralité du fichier peut donc s’avérer fastidieux et très long.
Essayons. En fait, ma première idée a été de charger le fichier dans un tableau et de simplement demander l’affichage des n dernières lignes du tableau. Ca donne quelque chose comme ça, pour extraire les 100 dernières lignes du fichier input.txt et les recopier dans output.txt :
$lignes = type input.txt
$lignes[-100..-1] | out-file output.txt
Au passage, j’ai simplifié ici ma première syntaxe qui au lieu d’utiliser directement [-100..-1] passait par une soustraction [($lignes.length-99)..-1]. Des fois, on se complique la vie inutilement. Et d’ailleurs, mon idée de charger le fichier dans un tableau était justement liée au fait que je pensais devoir utiliser la propriété length dudit tableau. Donc, cette contrainte supprimée, je pouvais regrouper tout ça en une simple instruction :
(type input.txt)[-100..-1] > output.txt
Au passage à nouveau J j’ai simplifié ici la syntaxe de redirection vers un fichier. Le symbole > est bien connu de tous les administrateurs depuis des lustres, alors pourquoi se compliquer la vie avec la commandelette out-file ? Simplement, out-file est très utile si vous voulez préciser l’encodage du fichier. Mais bon, c’est une autre discussion.
Restait à tester la commande sur un cas réel, c’est-à-dire un fichier de 800 000 lignes. J’ai donc « rapidement » (hmmm) généré un fichier lignes.txt de 800 000 lignes et j’ai mesuré le temps d’exécution de ma commande appliquée à ce fichier :
PS> measure-command {(type lignes.txt)[-$100..-1]}
…
TotalSeconds : 74,4792187
…
Environ 1 minute et 15 secondes, pas trop mauvais, mais pas formidable non plus. Pouvait-on faire mieux ? Oui, sans doute, en passant par les classes .NET spécialisées. Un petit essai confirma vite mes soupçons :
PS> measure-command {[io.file]::ReadAllLines((convert-path lignes.txt))[-100..-1]}
…
TotalSeconds : 10,6395449
…
A peine plus de 10 secondes pour traiter le même fichier, là oui c’est très correct ! Bon, c’est sûr, la commande à taper est moins intuitive que la première version. Il faut connaître la classe System.IO.File (qu’on peut abréger en IO.file puisque PowerShell ajoute automatiquement l’espace de noms System pour nous). Ensuite, on peut parcourir les méthodes avec la touche Tab et trouver ReadAllLines() qui correspond bien à ce qu’on veut faire.
La petite difficulté supplémentaire vient du fait qu’on passe un nom de fichier en paramètre. Là, il faut savoir que les classes .NET se moquent totalement de savoir dans quel répertoire la session PowerShell se trouve. A la place, .NET utilise le répertoire par défaut spécifié au chargement de l’instance PowerShell en cours (répertoire qui peut être modifié par la suite, mais qui est totalement indépendant des changements qu’on a pu faire en navigant de manière interactive). D’ailleurs, on peut voir ce que donne la commande suivante :
PS> [io.file]::ReadAllLines("lignes.txt")[-100..-1]
Exception calling "ReadAllLines" with "1" argument(s): "Could not find file 'C:\Users\janel\lignes.txt'."
At line:1 char:24
+ [io.file]::ReadAllLines( <<<< "lignes.txt")[-100..-1]
Vous noterez que la méthode ReadAllLines() a reçu comme argument ‘c:\users\janel\lignes.txt’, alors que ma session (et mon fichier) se trouve à ce moment-là dans un sous-répertoire de mon profil. Heureusement, il existe une commandelette pour nous sortir de ce mauvais pas : convert-path. La commandelette convert-path convertit un chemin exprimé à la sauce PowerShell en chemin exprimé selon les standards de Windows. Ca peut vous sembler un peu iconoclaste ici, mais faites-moi confiance, dans notre cas ça permet de transformer le simple nom de fichier en chemin complet vers ce fichier. On aurait pu également utiliser resolve-path, mais il aurait fallu accéder à la propriété path de l’objet retourné, ce qui alourdit la ligne de commande.
Voilà donc au final un petit exercice qui illustre la possibilité avec PowerShell de traiter un même problème de plusieurs manières, plus ou moins simples, et plus ou moins performantes. A vous de voir quelle solution s’adapte le mieux à votre besoin sur le moment, l’important étant de savoir qu’on a accès à plusieurs méthodes.
Pour finir sur la question des performances, même si les chiffres cités dans mon billet ne constituent absolument pas une démonstration, l’équipe Windows PowerShell a déjà reconnu à plusieurs reprises que la problématique des performances n’avait pas pu être traitée correctement pour la v1 de PowerShell. Une mise à jour sortira (ne me demandez pas quand, je n’en sais rien) qui apportera notamment des améliorations notables en la matière.
D’ici là, à bon entendeur salut !
Janel March 15 Supprimer des lignes d'un fichier avec PowerShellCoucou, me revoilou. Après plusieurs semaines (plusieurs mois ?) d’absence, me voici de retour avec quelques petites astuces rapides pour vos scripts PowerShell.
Pour commencer, voici en réponse à une question posée sur le forum microsoft.public.windows.powershell, une méthode simple, rapide et performante pour supprimer certaines lignes d’un fichier texte.
Supposons que j’aie récupéré un fichier sessions.log et que je veuille supprimer toutes les lignes qui comportent mon nom d’utilisateur (janel en l’occurrence). N’y voyez aucun mauvais esprit, c’est juste un exemple. Je pourrai taper la commande suivante :
(type sessions.log) -notmatch "janel" | out-file sessions2.log
Le principe est on ne peut plus simple. J’énumère toutes les lignes du fichier en les soumettant à l’opérateur de comparaison -notmatch. Cet opérateur compare l’expression de gauche (les lignes du fichier) avec l’expression régulière fournie à droite (ici, un simple texte littéral). Si le résultat de la comparaison est négatif, l’opérateur retourne le résultat. En l’occurrence, je récupère ce résultat pour l’envoyer dans un nouveau fichier avec la commandelette out-file.
Le fait que l’opérateur -notmatch accepte des expressions régulières nous ouvre des possibilités immenses d’analyse du contenu. On pourra détecter si le texte est présent en début ou en fin de ligne, tester la répétition d’une séquence particulière de types de caractères, etc. Pour plus d’informations sur les expressions régulières, jetez un œil sur la section correspondante du MSDN (en anglais).
A bon entendeur, salut.
Janel |
|
|