Jacques's profilejanelPhotosBlogLists Tools Help
    October 24

    Retrouver ses petits avec select-string

    Comme moi, vous avez des fichiers texte dont vous devez analyser le contenu : retrouver un message spécifique dans une série de fichiers log, vérifier la présence éventuelle d’un texte répondant à un schéma particulier, savoir quels fichier(s) contiennent l’information et où. Dans une invite de commande standard, vous penseriez tout de suite à la commande findstr, et vous auriez raison. Cette commande est toujours accessible sous Windows PowerShell, mais il est également possible d’utiliser les commandes natives de PowerShell, notamment la commandelette select-string.
     
    Imaginons que je travaille dans le répertoire Windows et que je fasse des recherches sur le contenu des fichiers INI. Je veux par exemple retrouver le fichier qui contient la section [drivers]. Avec findstr j’écrirai :
     
    Windows[33/58]> findstr /l "[drivers]" *.ini
    system.ini:[drivers]
     
    [Aparté] pour ceux qui se demandent ce que veut dire Windows[33/58]>, c’est juste mon prompt. Il m’indique que je travaille dans le répertoire Windows (dont le chemin d’accès complet est rappelé dans la barre de titre de la console), et que ce répertoire contient 33 fichiers et 58 sous-répertoires.
    [/Aparté]
     
    [Aparté2] Le paramètre /l (pour littéral) ordonne à findstr de rechercher le texte littéral. Sinon, findstr traitera le texte comme une expression régulière. Vous ne verrez sans doute pas de différence dans la plupart des situations, mais l’exemple ci-dessus ne marchera pas si j’enlève le paramètre /l. Essayez, vous verrez la différence. Cela tient au fait que les crochets sont des éléments de syntaxe d’une expression régulière. En l’occurrence, l’expression régulière “[drivers]” retourne toutes les lignes qui contiennent au moins un des caractères entre crochets, soit d, r, i, v, e ou s. N’hésitez pas à consulter la documentation en ligne de Microsoft sur le sujet.
    [/Aparté2]
     
    Avec select-string on pourra écrire de la même façon :
     
    Windows[33/58]> select-string "[drivers]" *.ini -simple
     
    system.ini:9:[drivers]
     
    Le résultat est sensiblement identique à celui produit par findstr, à ceci près que select-string renvoie automatiquement le numéro de ligne correspondant. On pourrait également l’obtenir avec findstr en ajoutant le paramètre /n.
     
    Notez pour votre information que la valeur “[drivers]” est en fait passée au paramètre -pattern et que la valeur *.ini est passée au paramètre –path. Ces deux paramètres ont les positions 1 et 2 par défaut, d’où la possibilité de les nommer ou pas. Le paramètre -simple a pour nom complet -simpleMatch, et vous l’aurez déjà compris il correspond au /l de findstr.
     
    Plutôt que de préciser la liste des fichiers dans la syntaxe de select-string, on pourra être amené à récupérer cette liste avec une autre commande (ou une série d’autres commandes) et à rediriger cette liste vers select-string :
     
    Windows[33/58]> dir *.ini | select-string "[drivers]" -simple
     
    system.ini:9:[drivers]
     
    Bon, mais essayons d’aller un peu plus loin en retrouvant la liste de toutes les sections contenues dans ces fichiers. Le nom d’une section se caractérise par le fait de commencer par un crochet ouvrant et de terminer par un crochet fermant, tout ce qui se trouve entre les deux étant le nom de la section. On pourrait taper simplement :
     
    Windows[33/58]> dir *.ini | select-string "[" -simple
     
    Malheureusement, cela va retourner toutes les lignes qui contiennent un crochet ouvrant, qu’il soit placé en début de ligne ou pas. Essayez, et vous verrez que de nombreuses lignes parasitent le résultat. Il faut donc pouvoir forcer la détection du crochet ouvrant en tant que premier caractère de la ligne. Une expression régulière saura très bien faire cela (voir la documentation en ligne déjà citée) :
     
    Windows[33/58]> dir *.ini | select-string "^\["
     
    msdfmap.ini:18:[connect default]
    msdfmap.ini:22:[sql default]
    msdfmap.ini:26:[connect CustomerDatabase]
    msdfmap.ini:30:[sql CustomerById]
    msdfmap.ini:33:[connect AuthorDatabase]
    msdfmap.ini:37:[userlist AuthorDatabase]
    msdfmap.ini:40:[sql AuthorById]
    ODBC.INI:1:[ODBC 32 bit Data Sources]
    ODBC.INI:3:[Visio Database Samples]
    SMSCFG.ini:1:[SMS MultiBoot Configuration]
    SMSCFG.ini:3:[Configuration - Client Properties]
    system.ini:2:[386Enh]
    system.ini:9:[drivers]
    system.ini:13:[mci]
    vbaddin.ini:1:[Add-Ins32]
    win.ini:2:[fonts]
    win.ini:3:[extensions]
    win.ini:4:[mci extensions]
    win.ini:5:[files]
    win.ini:6:[Mail]
    win.ini:13:[MCI Extensions.BAK]
     
    Ok, super. Maintenant que j’ai le résultat qui m’intéresse, je voudrais pouvoir l’afficher différemment, voire le garder en mémoire sans forcément l’afficher tout de suite. Je vais donc inspecter l’objet qui résulte de la commandelette select-string :
     
    Windows[33/58]> dir *.ini | select-string "^\[" | get-member
     
       TypeName: Microsoft.PowerShell.Commands.MatchInfo
     
    Name           MemberType   Definition
    ----           ----------   ----------
    Equals         Method       System.Boolean Equals(Object obj)
    GetHashCode    Method       System.Int32 GetHashCode()
    GetType        Method       System.Type GetType()
    get_Filename   Method       System.String get_Filename()
    get_IgnoreCase Method       System.Boolean get_IgnoreCase()
    get_Line       Method       System.String get_Line()
    get_LineNumber Method       System.Int32 get_LineNumber()
    get_Path       Method       System.String get_Path()
    get_Pattern    Method       System.String get_Pattern()
    RelativePath   Method       System.String RelativePath(String directory)
    set_IgnoreCase Method       System.Void set_IgnoreCase(Boolean value)
    set_Line       Method       System.Void set_Line(String value)
    set_LineNumber Method       System.Void set_LineNumber(Int32 value)
    set_Path       Method       System.Void set_Path(String value)
    set_Pattern    Method       System.Void set_Pattern(String value)
    ToString       Method       System.String ToString(), System.String ToString(String directory)
    Filename       Property     System.String Filename {get;}
    IgnoreCase     Property     System.Boolean IgnoreCase {get;set;}
    Line           Property     System.String Line {get;set;}
    LineNumber     Property     System.Int32 LineNumber {get;set;}
    Path           Property     System.String Path {get;set;}
    Pattern        Property     System.String Pattern {get;set;}
    MSDN           ScriptMethod System.Object MSDN();
     
    Je note par exemple les propriétés Filename, Line et Linenumber qui correspondent certainement aux valeurs utilisées pour l’affichage par défaut. Essayons :
     
    Windows[33/58]> select-string "^\[" *.ini | format-table filename,linenumber,line -auto
     
    Filename    LineNumber Line
    --------    ---------- ----
    msdfmap.ini         18 [connect default]
    msdfmap.ini         22 [sql default]
    msdfmap.ini         26 [connect CustomerDatabase]
    msdfmap.ini         30 [sql CustomerById]
    msdfmap.ini         33 [connect AuthorDatabase]
    msdfmap.ini         37 [userlist AuthorDatabase]
    msdfmap.ini         40 [sql AuthorById]
    ODBC.INI             1 [ODBC 32 bit Data Sources]
    ODBC.INI             3 [Visio Database Samples]
    SMSCFG.ini           1 [SMS MultiBoot Configuration]
    SMSCFG.ini           3 [Configuration - Client Properties]
    system.ini           2 [386Enh]
    system.ini           9 [drivers]
    system.ini          13 [mci]
    vbaddin.ini          1 [Add-Ins32]
    win.ini              2 [fonts]
    win.ini              3 [extensions]
    win.ini              4 [mci extensions]
    win.ini              5 [files]
    win.ini              6 [Mail]
    win.ini             13 [MCI Extensions.BAK]
     
    Je peux utiliser ces propriétés pour trier, filtrer, mesurer les résultats retournés par select-string. Quelques opérations à titre d’exemple, en commençant par l’affectation d’un select-string à une variable :
     
    Windows[33/58]> $sections = select-string "^\[" *.ini
    Windows[33/58]> $sections.length
    21
    Windows[33/58]> $sections | where {$_.line -match "ext"}
     
    win.ini:3:[extensions]
    win.ini:4:[mci extensions]
    win.ini:13:[MCI Extensions.BAK]
     
    Windows[33/58]> $sections | group filename | sort count -desc
     
    Count Name                      Group
    ----- ----                      -----
        7 msdfmap.ini               {msdfmap.ini, msdfmap.ini, msdfmap.ini, msdfmap.ini...}
        6 win.ini                   {win.ini, win.ini, win.ini, win.ini...}
        3 system.ini                {system.ini, system.ini, system.ini}
        2 ODBC.INI                  {ODBC.INI, ODBC.INI}
        2 SMSCFG.ini                {SMSCFG.ini, SMSCFG.ini}
        1 vbaddin.ini               {vbaddin.ini}
     
    Windows[33/58]> $sections | where {$_.filename -eq "win.ini"} | foreach {$_.line}
    [fonts]
    [extensions]
    [mci extensions]
    [files]
    [Mail]
    [MCI Extensions.BAK]
     
    Tout cela n’est qu’une brève introduction aux possibilités de select-string. Je vous laisse le plaisir d’explorer cette commandelette au gré de vos besoins.
     
    Janel
     
    October 23

    PowerShell, $null et les crashes

    Aujourd’hui, petit exercice de récupération de certains évènements du journal système. En l’occurrence le script ci-dessous, que j’appellerai get-crash.ps1, récupère les évènements 6008 correspondant à une interruption inopinée du système. Le script ajoute une propriété CrashDateTime aux objets évènements retournés, dont la valeur est déterminée par la date et l’heure données dans le corps du message de l’évènement.
     
    $crashes = get-eventlog -logname system | where {$_.eventid -eq 6008}
    foreach ($crash in $crashes) {
        $start = $crash.message.indexof("shutdown at ") + "shutdown at ".length
        $end = $crash.message.indexof("was unexpected") - 1
        $crashdatetime = $crash.message.substring($start, $end - $start)
        add-member -input $crash -type NoteProperty -name CrashDateTime -value $crashdatetime -passthru
    }
     
    On pourra l’utiliser comme ceci:
     
    PS> get-crash | ft timewritten, crashdatetime
     
    TimeWritten                                                 CrashDateTime
    -----------                                                 -------------
    18/10/2006 12:15:07                                         12:13:57 on 18/10/2006
    10/10/2006 12:11:56                                         12:10:43 on 10/10/2006
    09/10/2006 08:31:32                                         08:29:33 on 09/10/2006
     
    Bon, le but de cet exercice n’est pas vraiment de montrer comment traiter les évènements système de Windows dans PowerShell (encore que ça puisse constituer un bon point de départ). A l’évidence mon script n’est pas exploitable tel quel ; il repose sur un contenu de message en anglais, il faudrait donc l’adapter au langage de chaque système à interroger. De plus, l’usage d’une variable CrashDateTime sous forme d’une chaîne de caractères ne constitue pas une bonne pratique, il aurait fallu que mon script convertisse le contenu en une valeur exploitable en tant que System.DateTime pour, par exemple, calculer le temps écoulé entre l’heure du crash et le redémarrage du système. Je vous laisse le soin de compléter si besoin est.
     
    Non, le véritable but de l’exercice était de voir ce qui se passe si le journal des évènements système ne contient aucun crash (pas de sarcasme s’il vous plaît au fond de la salle). Si votre système n’a jamais eu la joie de s’interrompre de manière inopinée, vous pouvez voir ce que donne le script ci-dessus. Si votre journal système contient au moins une entrée 6008 vous pouvez faire le test avec cet autre script qui se contente d’afficher les évènements dont l’ID est passé en argument :
     
    $eventid = $args[0]
    $events = get-eventlog -logname system | where {$_.eventid -eq $eventid}
    foreach ($event in $events) {
        "Evènement n°$($event.index), daté du $($event.timegenerated):"
        $event.message
        "(source: $($event.source))`n"
    }
     
    PS> get-event 6008
    Evènement n°2727, daté du 10/18/2006 12:15:07:
    The previous system shutdown at 12:13:57 on 18/10/2006 was unexpected.
    (source: EventLog)
     
     
    Evènement n°1042, daté du 10/10/2006 12:11:56:
    The previous system shutdown at 12:10:43 on 10/10/2006 was unexpected.
    (source: EventLog)
     
     
    Evènement n°800, daté du 10/09/2006 08:31:32:
    The previous system shutdown at 08:29:33 on 09/10/2006 was unexpected.
    (source: EventLog)
     
    Que se passe-t-il si je passe un ID non présent dans le journal système ?
     
    PS> get-event 9999
    Evènement n°, daté du :
    (source: )
     
    Berk. La boucle foreach a été éxécutée alors qu’il n’y avait aucun évènement à traiter. On aurait préféré que foreach prenne en compte le fait que $events était vide et ne fasse rien. Alors, est-ce un bug de PowerShell ? Et d’abord, $events était-il vraiment vide ? Vérification :
     
    PS> . .\get-event 9999
    Evènement n°, daté du :
    (source: )
     
    PS> $events
    PS> $events -eq $null
    True
     
    Ah. Donc, $events a reçu la valeur $null, ce qui pour PowerShell n’est pas pareil que de n’avoir aucune valeur. Il ne s’agit pas d’un bug mais d’un choix tout à fait défendable ; je vous renvoie à la littérature existante ou à venir sur le sujet (voir notamment le livre à paraître de Bruce Payette chez Manning). Pour résumer, PowerShell considère $null comme une valeur scalaire, et si je passe $null à une boucle de traitement elle s’exécutera pour traiter $null.
     
    Alors, comment puis-je indiquer à PowerShell que je n’ai *aucun* résultat, et qu’il ne faut donc pas exécuter la boucle de traitement ? La solution consiste à utiliser une liste vide :
     
    PS> $liste = @()
    PS> $liste | foreach {"élément = "+$_}
    PS>
     
    Vous pouvez le constater si vous faites le test, la boucle foreach ne s’exécute pas du tout. En effet, PowerShell n’a rien à transmettre car la liste est vraiment vide. Comment adapter ce principe à mes scripts ? Il suffit de forcer PowerShell à considérer le résultat de la recherche comme une liste. Si la recherche ne retourne aucun évènement, la liste sera vide.
     
    $eventid = $args[0]
    $events = @(get-eventlog -logname system | where {$_.eventid -eq $eventid})
    foreach ($event in $events) {
        "Evènement n°$($event.index), daté du $($event.timegenerated):"
        $event.message
        "(source: $($event.source))`n"
    }
     
    PS> get-event 9999
    PS>
     
    Cette conversion forcée (soulignée en rouge dans l'exemple ci-dessus) peut s’appliquer au script get-crash.ps1, ainsi qu’à tout script ou toute fonction qui veut s’assurer de ne traiter des objets que s’il en existe au moins un.
     
    Janel
    October 20

    Jeu, set et -match

    Dans mon billet publié hier soir, j’ai utilisé à deux reprises la commandelette where-object (ou plutôt son alias where) pour filtrer une liste d’objets selon leur concordance avec un terme donné. Dans les deux cas j’aurais pu simplifier la syntaxe en utilisant l’opérateur –match à la place du filtre:
     
    Cas 1
    PS> $citations | where {$_ -match “bonheur”}
    On n'échappe pas au spectacle du bonheur.
    Fin de citation
    PS>
    PS> $citations –match “bonheur”
    On n'échappe pas au spectacle du bonheur.
    Fin de citation
     
    Cas 2
    PS> (get-help filesystem).dynamicparameters.dynamicparameter | where {$_.name -eq "Delimiter"}
     
    Type            : @{Name=System.String}
    CmdletSupported : Get-Content
    PossibleValues  :
    Name            : Delimiter
    Description     : Specifies the delimiter to use when reading the file. The default is "\n" (end of line).
    PS>
    PS> (get-help filesystem).dynamicparameters.dynamicparameter -match "Delimiter"
     
    Type            : @{Name=System.String}
    CmdletSupported : Get-Content
    PossibleValues  :
    Name            : Delimiter
    Description     : Specifies the delimiter to use when reading the file. The default is "\n" (end of line).
     
    Attention, dans le deuxième cas je remplace avec succès –eq par –match, mais il y a bien une différence entre –eq et –match. L’opérateur –eq vérifie la concordance exacte entre deux termes (à l’exception de la casse majuscules/minuscules qui n’est pas prise en compte par défaut : utiliser –ceq pour une prise en compte de la casse), alors que l’opérateur –match vérifie que le terme de droite est une expression régulière qui s’applique au terme de gauche.
     
    PS> "hello" -eq "Hello"
    True
    PS> "hello world" -eq "Hello"
    False
    PS> "hello world" -match "Hello"
    True
     
    Egalement, vous remarquerez que dans la première syntaxe de mon Cas 2 je limitais le filtrage au contenu de la propriété Name du DynamicParameter, alors que la seconde syntaxe « simplifiée » cherche une correspondance avec toutes les propriétés qui peuvent être traitées en tant que System.String. On peut ainsi étendre le champ de ses recherches et découvrir des choses intéressantes, ce qui est mon cas à l’instant:
     
    PS> (get-help filesystem).dynamicparameters.dynamicparameter -match "get-content"
     
    Type            : @{Name=Microsoft.PowerShell.Commands.FileSystemCmdletProviderEncoding}
    CmdletSupported : Add-Content, Get-Content, Set-Content
    PossibleValues  : @{PossibleValue=System.Management.Automation.PSObject[]}
    Name            : Encoding
    Description     :
     
    Type            : @{Name=System.String}
    CmdletSupported : Get-Content
    PossibleValues  :
    Name            : Delimiter
    Description     : Specifies the delimiter to use when reading the file. The default is "\n" (end of line).
     
    Type            : @{Name=System.Management.Automation.SwitchParameter}
    CmdletSupported : Get-Content
    PossibleValues  : @{PossibleValue=@{Value=; Description=System.Management.Automation.PSObject[]}}
    Name            : Wait
    Description     : Waits for content to be appended to the file. If content is appended, it returns the appended content
                      . If the content has changed, it returns the entire file.
     
                      When waiting, Get-Content checks the file once in each second until you interrupt it, such as by pres
                      sing Ctrl + C.
     
    A vous de jouer !
     
    Janel
    October 19

    get-content, les fins de ligne et les paramètres dynamiques

    Lorsqu’on lit un fichier texte avec la commandelette get-content, on obtient une table avec un enregistrement par ligne. Par exemple :
     
    PS> set-content liste.txt @"
    >> Beurre
    >> Café
    >> Epinards
    >> Poisson
    >> Allumettes
    >> "@
    >>
    PS> $liste = get-content liste.txt
    PS> $liste.length
    5
    PS> $liste[0]
    Beurre
    PS> $liste[-1]
    Allumettes
     
    Maintenant, quid de la segmentation d’un fichier selon un autre critère ? Autrement dit, peut-on personnaliser le délimiteur de fin de ligne ? A priori, une recherche dans l’aide en ligne de get-content ne donne rien qui puisse répondre à ce besoin. Et pourtant :
     
    PS> get-content fonctions.ps1
    function addition {
     return $args[0] + $args[1]
    }
    function soustraction {
     return $args[0] - $args[1]
    }
    function multiplication {
     return $args[0] * $args[1]
    }
    function division {
     return $args[0] / $args[1]
    }
    PS> $fn = get-content -delimiter "}" fonctions.ps1
    PS> $fn[0]
    function addition {
     return $args[0] + $args[1]
    }
     
    Encore plus fort J :
     
    PS> get-content citations.txt
    On n'échappe pas au spectacle du bonheur.
    Fin de citation
    Et à quoi bon exécuter des projets, puisque le projet est en lui-même une jouissance suffisante?
    Fin de citation
    "Vous êtes libre ce soir - Oui, mais permettez-moi de le rester."
    Fin de citation
    PS> $citations = get-content -delimiter "Fin de citation" citations.txt
    PS> $citations | where {$_ -match “bonheur”}
    On n'échappe pas au spectacle du bonheur.
    Fin de citation
     
    Vous l’aurez peut-être compris, le délimiteur est un System.String, on peut donc passer n’importe quelle chaîne de caractères, et pas simplement un seul caractère. Attention, la valeur passée est sensible à la casse. Dans l’exemple ci-dessus, « Fin de citation » est différent de « fin de citation ».
     
    Alors, d’où vient ce paramètre –delimiter qui ne figure pas dans l’aide de get-content ? La réponse se trouve dans l’aide du provider FileSystem. Si vous tapez « get-help FileSystem », vous verrez la partie concernant ce paramètre tout à la fin du très long texte. Ce paramètre étant un « paramètre dynamique » fourni par le provider, on peut également accéder à sa description ainsi :
     
    PS> (get-help filesystem).dynamicparameters.dynamicparameter | where {$_.name -eq "Delimiter"}
     
    Type            : @{Name=System.String}
    CmdletSupported : Get-Content
    PossibleValues  :
    Name            : Delimiter
    Description     : Specifies the delimiter to use when reading the file. The default is "\n" (end of line).
     
    Au passage, le guide utilisateur de la RC2 ne précise pas que les providers fournis en standard viennent avec une aide en ligne, accessible par « get-content providername ». Essayez avec tous les providers disponibles (« get-psprovider » pour avoir la liste complète), vous découvrirez sans doute plein d'autres choses !
     
    Janel
    October 10

    J'ai de la chance!

    Vous connaissez sans doute le moteur de recherche Google. Ce moteur implémente une fonction intitulée « J’ai de la chance » (« I’m feeling lucky » dans sa version originale). Le site de Google décrit cette fonction ainsi :
     
    Le bouton « J'ai de la chance » affiche directement (et uniquement) la page Web considérée par Google comme la plus pertinente pour la requête exprimée. Dans ce cas, aucune autre page n'est citée. En utilisant le bouton « J'ai de la chance », vous passez moins de temps à rechercher les pages Web qui vous intéressent, ce qui vous laisse plus de temps pour les exploiter.
     
    Très intéressant, mais l’exercice peut se révéler amusant. Je me souviens d’un vieux gag avec cette fonction où on tapait un mot du genre « stupide » et on atterrissait sur la biographie de George W. Bush. Mais voici un nouveau gag presqu’aussi savoureux (en tout cas pour ceux qui s’intéressent à la guerre commerciale que se livrent les géants de l’Internet) :
     
    D’abord, assurez-vous d’être sur la page US du site :
     
    • Tapez http://www.google.com
    • Si vous votre navigateur vous redirige sur Google France, cliquez sur « Google.com in English »
    Ensuite, tapez « search » comme critère de recherche et cliquez sur «  I’m Feeling Lucky ». Comme disent les américains : Enjoy !
     
    Janel