Le problème - Description générique
Comme on peut le voir dans l'aide pour about_pipelines (help pipeline
) powershell envoie les objets un par un dans le pipeline¹. Donc Get-Process -Name notepad | Stop-Process
envoie un processus à la fois dans le tuyau.
Disons que nous avons un CmdLet (Do-SomeStuff) tiers qui ne peut être modifié ou changé d'aucune façon. Do-SomeStuff fonctionne différemment s'il reçoit un tableau de chaînes ou s'il reçoit un objet chaîne unique.
Do-SomeStuff n'est qu'un exemple, il pourrait remplacer ForEach-Object
, Select-Object
, Write-Host
(ou tout autre CmdLet acceptant une entrée de pipeline)
Dans cet exemple, Do-SomeStuff traitera les éléments individuels du tableau un par un.
$theArray = @("A", "B", "C")
$theArray | Do-SomeStuff
Si nous voulons envoyer le tableau complet en un seul objet à Do-SomeStuff, on peut essayer quelque chose comme ça
@($theArray) | Do-SomeStuff
Mais cela ne produit pas le résultat attendu car PowerShell "ignore" le nouveau tableau à un seul élément.
Alors, comment voulez-vous "forcer" $theArray
à transmettre dans le canal en tant qu'objet tableau unique au lieu des éléments de contenu un à la fois?
Le problème - exemple pratique
Comme illustré ci-dessous, la sortie de Write-Host
est différent si un tableau est passé ou s'il passe les éléments individuels du tableau un par un.
PS C:\> $theArray = @("A", "B", "C")
PS C:\> Write-Host $theArray
A B C
PS C:\> $theArray | foreach{Write-Host $_}
A
B
C
PS C:\> @($theArray) | foreach{Write-Host $_}
A
B
C
Comment faites-vous pour obtenir $theArray | foreach{Write-Host $_}
pour produire la même sortie que Write-Host $theArray
?
NOTES DE BAS DE PAGE
Un tableau normal de chaînes
PS C:\> @("A", "B", "C").GetType().FullName
System.Object[]
Un tableau normal de chaînes dirigées vers Foreach-Object
PS C:\> @("A", "B", "C") | foreach{$_.GetType().FullName}
System.String
System.String
System.String
Chaque chaîne du tableau est traitée une à la fois par le ForEach-Object CmdLet.
Un tableau de tableaux, où les tableaux "intérieurs" sont des tableaux de chaînes.
PS C:\> @(@("A", "B", "C"), @("D", "E", "F"), @("G", "H", "I")) | foreach{$_.GetType().FullName}
System.Object[]
System.Object[]
System.Object[]
Chaque tableau du tableau est traité un à la fois par le ForEach-Object CmdLet, et le contenu de chaque sous-tableau de l'entrée est traité comme un objet même s'il s'agit d'un tableau.
Réponse courte: utilisez l'opérateur de tableau unaire ,
:
,$theArray | foreach{Write-Host $_}
Réponse longue: il y a une chose que vous devez comprendre à propos de l'opérateur @()
: il interprète toujours son contenu comme une instruction, même si le contenu n'est qu'une expression. Considérez ce code:
$a='A','B','C'
$b=@($a;)
$c=@($b;)
J'ajoute ici la marque explicite de fin de déclaration ;
, Bien que PowerShell permette de l'omettre. $a
Est un tableau de trois éléments. Quel résultat de l'instruction $a;
? $a
Est une collection, donc la collection doit être énumérée et chaque élément individuel doit être transmis par pipeline. Le résultat de l'instruction $a;
Est donc trois éléments écrits dans le pipeline. @($a;)
voit que trois éléments, mais pas le tableau d'origine, et crée un tableau à partir d'eux, donc $b
est un tableau de trois éléments. De la même manière $c
Est un tableau de trois mêmes éléments. Ainsi, lorsque vous écrivez @($collection)
vous créez un tableau, qui copie les éléments de $collection
, Au lieu du tableau d'un seul élément.
Le caractère virgule fait des données un tableau. Pour que le pipeline traite votre tableau en tant que tableau, au lieu d'opérer sur chaque élément du tableau individuellement, vous devrez peut-être également encapsuler les données entre parenthèses.
Ceci est utile si vous devez évaluer le statut de plusieurs éléments dans le tableau.
en utilisant la fonction suivante
function funTest {
param (
[parameter(Position=1, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[alias ("Target")]
[array]$Targets
) # end param
begin {}
process {
$RandomSeed = $( 1..1000 | Get-Random )
foreach ($Target in $Targets) {
Write-Host "$RandomSeed - $Target"
} # next target
} # end process
end {}
} # end function
Considérez les exemples suivants:
Le simple fait de mettre votre tableau entre parenthèses ne garantit pas que la fonction traitera le tableau de valeurs en un seul appel de processus. Dans cet exemple, nous voyons les changements de nombres aléatoires pour chaque élément du tableau.
PS C:\> @(1,2,3,4,5) | funTest
153 - 1
87 - 2
96 - 3
96 - 4
986 - 5
Le simple fait d'ajouter la virgule principale ne garantit pas non plus que la fonction traitera le tableau de valeurs en un seul appel de processus. Dans cet exemple, nous voyons les changements de nombres aléatoires pour chaque élément du tableau.
PS C:\> , 1,2,3,4,5 | funTest
1000 - 1
84 - 2
813 - 3
156 - 4
928 - 5
Avec la virgule de tête et le tableau de valeurs entre parenthèses, nous pouvons voir que le nombre aléatoire reste le même car la commande foreach de la fonction est exploitée.
PS C:\> , @( 1,2,3,4,5) | funTest
883 - 1
883 - 2
883 - 3
883 - 4
883 - 5
Il existe une solution à l'ancienne, si cela ne vous dérange pas que votre processus soit une fonction.
Exemple: vous voulez qu'un tableau soit copié dans le presse-papiers d'une manière qui vous permette de le reconstruire sur un autre système sans aucune connectivité PSRemoting. Vous voulez donc qu'un tableau contenant "A", "B" et "C" se transmute en une chaîne: @ ("A", "B", "C") ... au lieu d'un tableau littéral.
Donc, vous construisez ceci (ce qui n'est pas optimal pour d'autres raisons, mais restez sur le sujet):
# Serialize-List
param
(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$list
)
$output = "@(";
foreach ($element in $list)
{
$output += "`"$element`","
}
$output = $output.Substring(0, $output.Length - 1)
$output += ")"
$output
et cela fonctionne lorsque vous spécifiez directement le tableau en tant que paramètre:
Serialize-List $ list
@("ABC")
... mais pas tellement quand vous le faites passer par le pipeline:
$ list | Serialize-List
@ ("C")
Mais refactorisez votre fonction avec des blocs de début, de processus et de fin:
# Serialize-List
param
(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$list
)
begin
{
$output = "@(";
}
process
{
foreach ($element in $list)
{
$output += "`"$element`","
}
}
end
{
$output = $output.Substring(0, $output.Length - 1)
$output += ")"
$output
}
... et vous obtenez la sortie souhaitée dans les deux sens.
Serialize-List $ list
@("ABC")
$ list | Serialize-List
@("ABC")