当参数来自管道时,为什么这个 Powershell Cmdlet return 是一个多维数组?
Why does this Powershell Cmdlet return a multidimensional array when parameter comes from pipeline?
我用 C# 编写了一个 Powershell cmdlet,其中 return 使用自己开发的 API 详细介绍了一名或多名员工的直接经理。该 cmdlet 应该 return 一个或多个类型为 Associate 的对象的集合。我遇到的问题是 Cmdlet 的输出类型不一致。
我设计了 Cmdlet,如果您已经有一个 Associate 对象的集合,您可以通过管道将其传入。否则需要在-Identity参数下传入一个或多个userId。
这是我所看到的行为,但就 Cmdlet 输出而言:
- 如果我使用 -Identity 参数传入一个或多个 userId,我会得到预期的集合 Associate:
> $test1 = Get-Manager -Identity 'user1','user2'
> $test1.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
PS H:\> $test1 | select displayName
displayName
-----------
John Doe
Jane Lee
- 如果我通过显式使用 -Assoc 参数传入一个或多个 Associate 对象,我也会得到预期的集合
> $folks = Get-Associate 'brunomik','abcdef2'
> $test2 = Get-Manager -Assoc $folks
> $test2.getType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
PS H:\> $test2 | Select displayName
displayName
-----------
John Doe
Jane Lee
- 但是,如果我使用 管道 传递 Associate 对象的集合,我似乎得到了一个多维数组!:
> $test3 = $folks | Get-Manager
> $test3.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
> $test3 | select displayName
displayName
-----------
># Select-Object can't find a property called displayName
># But if I run GetType() on the first element of the collection:
> $test3[0].GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
># It appears to be yet another collection!
># Now, if I run Select-Object on that first element of $test3, I do see the data:
> $test3[0] | Select displayName
displayName
-----------
John Doe
Jane Lee
这是 Cmdlet 的源代码:
[Cmdlet(VerbsCommon.Get, "Manager", DefaultParameterSetName = @"DefaultParamSet")]
[OutputType(typeof(Associate))]
public class GetManager : Cmdlet
{
private Associate[] assoc = null;
private string[] identity = null;
private bool assocSet = false;
private bool identitySet = false;
//The Assoc parameter supports the pipeline and accepts one or more objects of type Associate
[Parameter(ParameterSetName = @"DefaultParamSet",
ValueFromPipeline = true,
HelpMessage = "An Associate object as returned by the \"Get-Associate\" cmdlet. Cannot be used with the \"Identity\" parameter")]
public Associate[] Assoc
{
get
{
return assoc;
}
set
{
assoc = value;
assocSet = true;
}
}
//The Identity parameter accepts one or more string expressions (user IDs)
[Parameter(HelpMessage = "An Associate user Id. Not to be used with the \"Assoc\" parameter")]
public string[] Identity
{
get
{
return identity;
}
set
{
identitySet = true;
identity = value;
}
}
//This will contain the output of the Cmdlet
private List<Associate> Result = new List<Associate>();
protected override void BeginProcessing()
{
base.BeginProcessing();
}
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
WriteObject(Result);
}
//Builds the Cmdlet Output object
private void BuildOutputObject()
{
List<Associate> Subordinates = new List<Associate>();
//Only the Assoc or Identity parameter may be set; not both.
if (!(assocSet ^ identitySet))
{
throw new ApplicationException($"Either the {nameof(Assoc).InQuotes()} or the {nameof(Identity).InQuotes()} parameter must be set, but not both.");
}
//If Assoc is set, we already have an array of Associate objects, so we'll simply define Subordinates by calling Assoc.ToList()
if (assocSet)
{
Subordinates = Assoc.ToList();
}
//Otherwise, we'll need to create an associate object from each userID passed in with the "Identity" parameter. The MyApi.GetAssociates() method returns a list of Associate objects.
else
{
Subordinates = MyApi.GetAssociates(Identity);
if (!MyApi.ValidResponse)
{
throw new ApplicationException($"No associate under the identifiers {string.Join(",",Identity).InQuotes()} could be found.");
}
}
//Now, to build the output object:
Subordinates.ForEach(p => Result.Add(p.GetManager()));
}
}
ProcessRecord
每个输入参数执行一次。
因此,当您调用 Get-Manager -Identity A,B
时,PowerShell:
- 解析适当的参数集(如果需要)
- 调用
BeginProcessing()
- 将值
A,B
绑定到身份
- 调用
ProcessRecord()
- 调用
EndProcessing()
当您将等效数组传递给它时(例如 "A","B" |Get-Manager
),PowerShell 会枚举输入并将项目绑定到适当的参数 一个接一个相反 - 即 PowerShell:
- 解析适当的参数集(如果需要)
- 调用
BeginProcessing()
- 将值
A
绑定到 Identity
- 调用
ProcessRecord()
- 将值
B
绑定到 Identity
- 调用
ProcessRecord()
- 调用
EndProcessing()
...结果是 2 个 List<Associate>
,而不是一个。
“解决方案”是:
- 不是 return 作为输出对象的具体集合类型,或
- 在
ProcessRecord
中“收集”部分输出,然后在EndProcessing
中输出一次。
1。没有换行 IEnumerable
类型
这种方法非常类似于 C# 中的 迭代器方法 - 将 WriteObject(obj);
视为 PowerShell 版本的 yield return obj;
:
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
foreach(var obj in Result)
WriteObject(obj);
}
WriteObject()
也有一个为您枚举对象的重载,所以最简单的修复实际上只是:
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
WriteObject(Result, true);
}
第一个选项到目前为止是最可取的,因为它允许我们充分利用 PowerShell 管道处理器的性能特征。
2。累计输出,WriteObject()
in EndProcessing()
:
private List<Associate> finalResult = new List<Associate>();
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
# Accumulate output
finalResult.AddRange(Result)
}
protected override void EndProcessing()
{
WriteObject(finalResult);
}
省略 WriteObject
的第二个参数,只调用它 一次 ,将保留 finalResult
的类型,但您将阻止任何下游 cmdlet从执行到完成处理所有输入
我用 C# 编写了一个 Powershell cmdlet,其中 return 使用自己开发的 API 详细介绍了一名或多名员工的直接经理。该 cmdlet 应该 return 一个或多个类型为 Associate 的对象的集合。我遇到的问题是 Cmdlet 的输出类型不一致。
我设计了 Cmdlet,如果您已经有一个 Associate 对象的集合,您可以通过管道将其传入。否则需要在-Identity参数下传入一个或多个userId。
这是我所看到的行为,但就 Cmdlet 输出而言:
- 如果我使用 -Identity 参数传入一个或多个 userId,我会得到预期的集合 Associate:
> $test1 = Get-Manager -Identity 'user1','user2'
> $test1.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
PS H:\> $test1 | select displayName
displayName
-----------
John Doe
Jane Lee
- 如果我通过显式使用 -Assoc 参数传入一个或多个 Associate 对象,我也会得到预期的集合
> $folks = Get-Associate 'brunomik','abcdef2'
> $test2 = Get-Manager -Assoc $folks
> $test2.getType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
PS H:\> $test2 | Select displayName
displayName
-----------
John Doe
Jane Lee
- 但是,如果我使用 管道 传递 Associate 对象的集合,我似乎得到了一个多维数组!:
> $test3 = $folks | Get-Manager
> $test3.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
> $test3 | select displayName
displayName
-----------
># Select-Object can't find a property called displayName
># But if I run GetType() on the first element of the collection:
> $test3[0].GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
># It appears to be yet another collection!
># Now, if I run Select-Object on that first element of $test3, I do see the data:
> $test3[0] | Select displayName
displayName
-----------
John Doe
Jane Lee
这是 Cmdlet 的源代码:
[Cmdlet(VerbsCommon.Get, "Manager", DefaultParameterSetName = @"DefaultParamSet")]
[OutputType(typeof(Associate))]
public class GetManager : Cmdlet
{
private Associate[] assoc = null;
private string[] identity = null;
private bool assocSet = false;
private bool identitySet = false;
//The Assoc parameter supports the pipeline and accepts one or more objects of type Associate
[Parameter(ParameterSetName = @"DefaultParamSet",
ValueFromPipeline = true,
HelpMessage = "An Associate object as returned by the \"Get-Associate\" cmdlet. Cannot be used with the \"Identity\" parameter")]
public Associate[] Assoc
{
get
{
return assoc;
}
set
{
assoc = value;
assocSet = true;
}
}
//The Identity parameter accepts one or more string expressions (user IDs)
[Parameter(HelpMessage = "An Associate user Id. Not to be used with the \"Assoc\" parameter")]
public string[] Identity
{
get
{
return identity;
}
set
{
identitySet = true;
identity = value;
}
}
//This will contain the output of the Cmdlet
private List<Associate> Result = new List<Associate>();
protected override void BeginProcessing()
{
base.BeginProcessing();
}
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
WriteObject(Result);
}
//Builds the Cmdlet Output object
private void BuildOutputObject()
{
List<Associate> Subordinates = new List<Associate>();
//Only the Assoc or Identity parameter may be set; not both.
if (!(assocSet ^ identitySet))
{
throw new ApplicationException($"Either the {nameof(Assoc).InQuotes()} or the {nameof(Identity).InQuotes()} parameter must be set, but not both.");
}
//If Assoc is set, we already have an array of Associate objects, so we'll simply define Subordinates by calling Assoc.ToList()
if (assocSet)
{
Subordinates = Assoc.ToList();
}
//Otherwise, we'll need to create an associate object from each userID passed in with the "Identity" parameter. The MyApi.GetAssociates() method returns a list of Associate objects.
else
{
Subordinates = MyApi.GetAssociates(Identity);
if (!MyApi.ValidResponse)
{
throw new ApplicationException($"No associate under the identifiers {string.Join(",",Identity).InQuotes()} could be found.");
}
}
//Now, to build the output object:
Subordinates.ForEach(p => Result.Add(p.GetManager()));
}
}
ProcessRecord
每个输入参数执行一次。
因此,当您调用 Get-Manager -Identity A,B
时,PowerShell:
- 解析适当的参数集(如果需要)
- 调用
BeginProcessing()
- 将值
A,B
绑定到身份 - 调用
ProcessRecord()
- 调用
EndProcessing()
当您将等效数组传递给它时(例如 "A","B" |Get-Manager
),PowerShell 会枚举输入并将项目绑定到适当的参数 一个接一个相反 - 即 PowerShell:
- 解析适当的参数集(如果需要)
- 调用
BeginProcessing()
- 将值
A
绑定到Identity
- 调用
ProcessRecord()
- 将值
B
绑定到Identity
- 调用
ProcessRecord()
- 调用
EndProcessing()
...结果是 2 个 List<Associate>
,而不是一个。
“解决方案”是:
- 不是 return 作为输出对象的具体集合类型,或
- 在
ProcessRecord
中“收集”部分输出,然后在EndProcessing
中输出一次。
1。没有换行 IEnumerable
类型
这种方法非常类似于 C# 中的 迭代器方法 - 将 WriteObject(obj);
视为 PowerShell 版本的 yield return obj;
:
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
foreach(var obj in Result)
WriteObject(obj);
}
WriteObject()
也有一个为您枚举对象的重载,所以最简单的修复实际上只是:
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
WriteObject(Result, true);
}
第一个选项到目前为止是最可取的,因为它允许我们充分利用 PowerShell 管道处理器的性能特征。
2。累计输出,WriteObject()
in EndProcessing()
:
private List<Associate> finalResult = new List<Associate>();
protected override void ProcessRecord()
{
base.ProcessRecord();
BuildOutputObject();
# Accumulate output
finalResult.AddRange(Result)
}
protected override void EndProcessing()
{
WriteObject(finalResult);
}
省略 WriteObject
的第二个参数,只调用它 一次 ,将保留 finalResult
的类型,但您将阻止任何下游 cmdlet从执行到完成处理所有输入