Question [Function PS v2] Replace-String

Plus d'informations
il y a 15 ans 6 mois #7651 par Laurent Dardenne
Ces derniers jours je suis revenu sur un script de parsing de chaînes issues d'un utilitaire, ici handle.exe .

L'objectif de ce script est de construire, à partir des informations renvoyées par l'utilitaire, des objets personnalisés :
[code:1]
Handle : 1356
Program : powershell.exe
Path : C:\Program Files\PowerShell Community Extensions\Pscx.Core.dll
Pid : 5452
attr : R-D
User : PCTEST\User
[/code:1]
Du coup je me suis dit,\" est-ce que ce type de traitement peut être réécrit à l'aide de la fonction Replace-String\" ?
La réponse est oui :-)

Comme Handle.exe renvoit un nom de programme suivi du détail des handles des fichiers qu'il utilise, le paramètre -Unique va nous aider à traiter ce type de construction :
[code:1]
get-help replace-string -Parameter Unique
[/code:1]
Reste l'autre aspect, que je n'ai pas pris en compte lors de la conception, comment construire un objet à partir d'une chaîne de caractères ?

Dans un premier temps je me suis dit que ce n'était pas possible, car comment associer un texte à un membre nommé sans passer en paramètres les informations NomMembre1=Texte1,NomMembre2=Texte2,... ?
Mais essayons avant de s'avouer vaincu ;-)

Premier problème, la portée des variables déclarées dans le scriptblock (délégué de type [MatchEvaluator]), ça pour le savoir faut juste l'essayer :
[code:1]
$h=new-object System.Collections.Specialized.OrderedDictionary
# Captures nommées
$h.'^(?<program>\S*)\s*pid: (?<pid>\d*)\s*(?<user>.*)$'={
#Récupère une référence directe
$Grps=$args[0].Groups

$id = $Grps
$program = $Grps
$user = $Grps
}
[/code:1]
Bonne nouvelle, les variables déclarées dans le scriptblock le sont dans la portée de Replace-String. Cela semble évident, mais je suis curieux de savoir comment l'équipe de PS à rendu ceci possible!

Normalement le scriptblock doit renvoyer une valeur de type [string], ici on ne renvoit rien, c'est à dire $Null, PS transforme cette valeur en une chaîne vide et ce d'après la signature du délégué (cf. système de reflexion dotnet).

Notez que dans le contexte dynamique de Powershell on détourne l'usage du délégué. Une fois de plus un concept de dotnet est adapté à l'environnement de PowerShell.

Les captures nommées, (?<program>\S*) , facilitent la relecture et l'utilisation du résultat de la regex.
Cette entrée de la hashtable ordonnée permet de récupèrer des informations à partir d'une ligne contenant le nom d'un programme.

La suivante permet de récupèrer des informations à partir d'une ligne contenant le nom d'un fichier utilisé par le dernier programme trouvé :
[code:1]
$h.'^\s*(?<handle>[\da-z]*): File \((?<attr>...)\)\s*(?<file>(\\\\)|([a-z]:«»).*)'={
$Grps=$args[0].Groups
#Le handle est une string représentant
#un nombre en hexadécimal, ex : 35C
$Handle=[int]::«»Parse($Grps,[System.Globalization.NumberStyles]::HexNumber)
$attr=$Grps
$Path=$Grps

\"@{Pid=$id;Program=`\"$program`\";User=`\"$user`\";Handle=`\"$Handle`\";attr=`\"$Attr`\";Path=`\"$Path`\"}\"
}
[/code:1]
Dans un premier temps j'ai utilisé le cmdlet New-Object suivie de Write-Output, mais il n'existe qu'un pipeline et ici il reçoit le résultat de l'exécution du délégué.
Ce résultat, il n'a pas changé, doit être de type [string], si on utilise Write-Output, PowerShell transforme l'objet en une string :
[code:1]
\" @{ Pid=4;Program=System;User=AUTORITE NT\SYSTEM;Handle=400;attr=R--;Path=C:\System Volume Information\_restore{ GUID }\RP732\change.log' } \"
[/code:1]
On ne peut pas utiliser cette chaîne en l'état et il n'existe pas à ma connaissance d'API permettant de reconstruire un PSObject à partir d'une telle chaîne, ce qui est regrettable d'ailleurs.

Tout compte fait c'est cette erreur qui m'a mise sur la voie, puisque le cmdlet New-Object autorise la création d'un objet à partir d'une hashtable, le délégué devait en émettre une !

La dernière ligne de code renvoit une string contenant la déclaration d'une hashtable, celle-ci définissant les membres de l'objet NomMembre1=Texte1,NomMembre2=Texte2,...

Reste à filtrer les lignes que l'on utilise pas :
[code:1]
$h.'^.*$'={}
[/code:1]
Dans le cas où le traitement atteint cette regex, une chaine vide est émise.
Selon les besoins, on sera amener à la filtrer dans le segment de pipeline suivant.
Dans le code de Replace-String on ne peut pas savoir si une chaîne vide doit être émise ou non dans le pipeline. Il manque peut être un option de paramètrage...

Reste à appeler la fonction :
[code:1]
$o=&\"C:\Tools\sysinternal\Handle\handle.exe\"|
Select-string '^\S*\s*pid: \d*\s*.*$|^\s*[\da-z]*: File \((?<attr>...)\)\s*(?<file>(\\\\)|([a-z]:«»).*)'|
Replace-String -input {$_.Line} $h -unique |
Where {$_ -ne [string]::Empty}|
#Reconstruit les objets à partir des hashtables construites
Foreach {Invoke-expression \"`$ht=$_;New-Object PSObject -Property `$ht\"}
[/code:1]
Le dernier segment du pipeline reconstruit le code de création de l'objet à partir de la hashtable puis l'exécute, l'objet est émis dans le pipeline.
La présence de Select-String optimise le traitement, sinon Replace-String parse toutes les lignes. Comme il contient de nombreux tests sur les type et de conversions, il est assez lent :/
Pour construire 959 objets à partir de 1556 lignes, le script d'origine s'exécute en 873,1752 ms, celui basé sur Replace-String en +-5300 ms.

Pour finir je vous rassure, le premier script pour ce type de traitement est la bonne approche, simple et performante !
Bien que de pouvoir généraliser ce type de traitement à l'aide d'un cmdlet ConvertTo-PSObject aurait été appréciable :-)

[edit]
Utilisation de la liste d'objet :
[code:1]
#récupère le programme ayant
#le plus de fichiers ouverts
$dernier=$o|group-object program|sort count|select -last 1
$dernier.Group|select path -unique|sort path

#récupère les fichiers ouverts par PS
($o|group-object program|Where {$_.name -eq \&quot;Powershell.exe\&quot;}).Group|select path -unique|sort path[/code:1]<br><br>Message édité par: Laurent Dardenne, à: 24/08/10 16:39

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 15 ans 6 mois #7663 par Richard Lazaro
haaaa, tu es passé à l'utilisation de la variable $PSCmdlet ? :p

Think-MS : (Get-Life).Days | %{ Learn-More }

\\&quot;Problems cannot be solved by the same level of thinking that created them.\\&quot; - Albert Einstein

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 15 ans 6 mois #7664 par Laurent Dardenne
Richard Lazaro écrit:

haaaa, tu es passé à l'utilisation de la variable $PSCmdlet ?

Oui j'ai tenu compte de ta remarque.
Je pensais que l'usage de ses méthodes WriteXxx avaient un comportement identique aux cmdlet Write-Xxxx, je me suis trompé.

L'usage de WriteError, demande tout de même une étude des catégories avant de les utiliser, c'est un peu \&quot;lourd\&quot; à coder, mais structurant.
Du coup l'usage de la variable PS $ErrorView prend tout son sens.

WriteError associé à Try/Catch permet, si besoin, de trapper une exception et la relancer en erreur non-bloquante :
[code:1]
} catch {
#La propriété est en R/O,
#La propriété n'est pas du type String, etc.

#Par défaut recrée l'exception trappée avec un message personnalisé
$PSCmdlet.WriteError(
(New-Object System.Management.Automation.ErrorRecord (
#Recrée l'exception trappée avec un message personnalisé
$_.Exception,
\&quot;ReplaceStringObjectPropertyError\&quot;,
\&quot;InvalidOperation\&quot;,
$InputObject
)
)
)#WriteError
}#catch
[/code:1]
C'est vraiment du domaine du scripting avancé, mais le manque d'outil se faire sentir pour faciliter ce type de codage.

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 15 ans 6 mois #7691 par Richard Lazaro
Et il est aussi possible de faire l'inverse ... attrapé une erreur non bloquante et la faire bloquante avec la méthode ThrowTerminatingError qui utilise les mêmes paramètre que la méthode WriteError mais qui entraine l'arrêt du script.

Think-MS : (Get-Life).Days | %{ Learn-More }

\\&quot;Problems cannot be solved by the same level of thinking that created them.\\&quot; - Albert Einstein

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 15 ans 6 mois #7694 par Laurent Dardenne
Richard Lazaro écrit:

Et il est aussi possible de faire l'inverse ... attrapé une erreur non bloquante et la faire bloquante avec la méthode ThrowTerminatingError

Oui c'est vrai, bien que dans un flux de pipeline je m'efforce de déclencher le moins possible d'exception.

Dans tout les cas c'est le contexte qui déterminera le choix de conception, ça semble évident mais je tiens à le rappeler :

WriteError :
Reports a nonterminating error to the error pipeline when the cmdlet (ou la fonction) cannot process a record but can continue to process other records (objet).

ThrowTerminatingError :
Reports a terminating error when the cmdlet (ou la fonction) cannot continue, or when you do not want the cmdlet (ou la fonction) to continue to process records (objet).


L'autre problème est la gestion des erreurs non-bloquantes dans le bloc Try/Catch qui semble dédié aux système d'exception de l'OS et pas à celui de PowerShell, voir ces posts :
stackoverflow.com/questions/1142211/try-...em-to-have-an-effect
outputredirection.blogspot.com/2010/04/p...atchfinally-and.html

Mais je n'ai pas pris le temps d'approfondir ce sujet.

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 13 ans 10 mois #11842 par Laurent Dardenne
Deux fonctions de manipulation de fichier XML basées sur Replace-String
[code:1]
Function Update-XMLBaliseName {
#Modifie un nom de balise dans un fichier XML
#Dépend de Replace-String
#Exemple :
# \&quot;C:\Temp\Config.xml\&quot;|Update-XMLBaliseName -Old OldBalise -new NewBalise
Param (
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
[ValidateNotNullOrEmpty()]
[string]$XmlFile,
[Parameter(Mandatory=$true,position=0)]
[ValidateNotNullOrEmpty()]
$OldName,
[Parameter(Mandatory=$true,position=1)]
[ValidateNotNullOrEmpty()]
$NewName,
[Switch] $NoBackup)

begin {
If (-not (gcm Replace-String))
{Throw \&quot;Nécessite la fonction Replace-String. Opération abandonnée\&quot;}
$Modification= @{
\&quot;(?&lt;Start&gt;&lt;|&lt;/)($OldName)&gt;\&quot;=\&quot;`${Start}$NewName&gt;\&quot;;
}
}
process {
if ( $DebugPreference -ne \&quot;SilentlyContinue\&quot;«»)
{ Write-Debug (\&quot;Call : {0}\&quot; -F $MyInvocation.InvocationName) }

$BackupFile=\&quot;$($XmlFile).bak\&quot;
Copy-Item $XmlFile $BackupFile
Get-Content $BackupFile|
Replace-String $Modification|
Set-Content -path $XmlFile
if ($NoBackup)
{Remove-Item $BackupFile}

if ( $DebugPreference -ne \&quot;SilentlyContinue\&quot;«»)
{ Write-Debug (\&quot;End : {0}\&quot; -F $MyInvocation.InvocationName) }
}
} #Update-XMLBaliseName

Function Update-XMLBaliseValue {
#Modifie la valeur d'une balise dans un fichier XML
#Dépend de Replace-String
#Exemple :
# \&quot;C:\Temp\Test.xml\&quot;|Update-XMLBaliseValue -Name \&quot;NewBalise\&quot; -Value \&quot;1.1\&quot;
Param (
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
[ValidateNotNullOrEmpty()]
[string]$XmlFile,
[Parameter(Mandatory=$true,position=0)]
[ValidateNotNullOrEmpty()]
$Name,
[Parameter(Mandatory=$true,position=1)]
$Value,
[Switch] $NoBackup)

begin {
If (-not (gcm Replace-String))
{Throw \&quot;Nécessite la fonction Replace-String. Opération abandonnée\&quot;}
$Modification= @{
\&quot;&lt;$Name&gt;(.*)&lt;/$Name&gt;\&quot;=\&quot;&lt;$Name&gt;$Value&lt;/$Name&gt;\&quot;;
}
}
process {
if ( $DebugPreference -ne \&quot;SilentlyContinue\&quot;«»)
{ Write-Debug (\&quot;Call : {0}\&quot; -F $MyInvocation.InvocationName) }

$BackupFile=\&quot;$($XmlFile).bak\&quot;
Copy-Item $XmlFile $BackupFile
Get-Content $BackupFile|
Replace-String $Modification|
Set-Content -path $XmlFile
if ($NoBackup)
{Remove-Item $BackupFile}

if ( $DebugPreference -ne \&quot;SilentlyContinue\&quot;«»)
{ Write-Debug (\&quot;End : {0}\&quot; -F $MyInvocation.InvocationName) }
}
} #Update-XMLBaliseValue
[/code:1]
Reste la factorisation du bloc process...

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Temps de génération de la page : 0.101 secondes
Propulsé par Kunena