4

我正在编写一个Chunk-Object可以将对象数组分为子数组的函数。例如,如果我将一个数组传递给它@(1, 2, 3, 4, 5)并指定每个块的元素,2那么它将返回 3 个数组@(1, 2)和. 如果用户想要在将每个元素分块为子数组之前对其进行处理,用户也可以提供一个可选参数。现在我的代码是:@(3, 4)@(5)scriptblock

function Chunk-Object()
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true,
                   ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true)] [object[]] $InputObject,
        [Parameter()] [scriptblock] $Process,
        [Parameter()] [int] $ElementsPerChunk
    )

    Begin {
        $cache = @();
        $index = 0;
    }

    Process {
        foreach($o in $InputObject) {
            $current_element = $o;
            if($Process) {
                $current_element = & $Process $current_element;
            }
            if($cache.Length -eq $ElementsPerChunk) {
                ,$cache;
                $cache = @($current_element);
                $index = 1;
            }
            else {
                $cache += $current_element;
                $index++;
            }
        }
    }

    End {
        if($cache) {
            ,$cache;
        }
    }
}


(Chunk-Object -InputObject (echo 1 2 3 4 5 6 7) -Process {$_ + 100} -ElementsPerChunk 3)
Write-Host "------------------------------------------------"
(echo 1 2 3 4 5 6 7 | Chunk-Object -Process {$_ + 100} -ElementsPerChunk 3)

结果是:

PS C:\Users\a> C:\Untitled5.ps1
100
100
100
100
100
100
100
------------------------------------------------
101
102
103
104
105
106
107

PS C:\Users\a> 

如您所见,它适用于管道输入的对象,但不适用于从参数获取的值。如何修改代码以使其在这两种情况下都能正常工作?

4

4 回答 4

4

不同之处在于,当您将数组通过管道传递到Chunk-Object时,该函数对作为管道对象序列传递的数组中的每个元素执行一次进程块,而当您将数组作为参数传递给-InputObject参数时,进程块对整个数组执行一次,该数组作为一个整体分配给$InputObject

因此,让我们看一下命令的管道版本:

echo 1 2 3 4 5 6 7 | Chunk-Object -Process {$_ + 100} -ElementsPerChunk 3

这个工作的原因是对于管道的每次迭代,$_设置为管道中当前数组元素的值,该值也分配给$InputObject变量(作为单元素数组,由于[object[]]类型转换。在这种情况下, foreach循环实际上是无关紧要的,因为$InputObject数组对于进程块的每次调用总是有一个元素。您实际上可以删除循环并更改$current_element = $o$current_element = $InputObject,并且您会得到完全相同的结果。

现在,让我们检查将数组参数传递给-InputObject的版本:

Chunk-Object -InputObject (echo 1 2 3 4 5 6 7) -Process {$_ + 100} -ElementsPerChunk 3

这不起作用的原因是您传递给-Process参数的脚本块包含$_,但是foreach循环将每个元素分配给$o,并且$_没有在任何地方定义。结果中的所有元素都是 100,因为每次迭代都将$current_element设置为 scriptblock 的结果,当$_为 null{$_ + 100}时,它总是计算为 100 。为了证明这一点,尝试将脚本块中的$_更改为$o,您将得到预期的结果:

Chunk-Object -InputObject (echo 1 2 3 4 5 6 7) -Process {$o + 100} -ElementsPerChunk 3

如果您希望能够在脚本块中使用$ _,只需将foreachforeach($o in $InputObject) {循环替换为$InputObject | %{. 这样两个版本都可以工作,因为Chunk-Object函数在内部使用管道,所以$_被顺序设置为数组的每个元素,无论是否为传入的一系列单独的数组元素多次调用进程块作为管道输入,或者对于多元素数组仅一次。


更新:

我又看了一遍,注意到在这条线上

$current_element = & $Process $current_element;

您似乎试图将$current_element作为参数传递给$Process中的脚本块。这不起作用,因为传递给脚本块的参数与函数中的工作方式大致相同。如果您调用MyFunction 'foo',则 'foo' 不会自动分配给函数中的$_;同样,& {$_ + 100} 'foo'不将$_设置为 'foo'。将您的 scriptblock 参数更改为{$args[0] + 100},无论是否传入管道输入,您都将获得预期的结果:

Chunk-Object -InputObject (echo 1 2 3 4 5 6 7) -Process {$args[0] + 100} -ElementsPerChunk 3

请注意,尽管即使您保留foreach循环,此版本的 scriptblock 参数仍然有效,但我仍然建议使用Foreach-Object ( $InputObject | %{),因为它通常更有效,因此该函数对于大量数据的运行速度更快。

于 2014-01-02T18:57:08.533 回答
3

从技术上讲,问题不在于参数属性。这既与您的论点有关,也与您如何处理它们有关。

问题:(echo 1 2 3 4 5 6 7)创建一个值“1 2 3 4 5 6 7”的字符串,您似乎想要处理一个数组

解决方案:使用数组:@(1, 2, 3, 4, 5, 6, 7)

问题:您正在使用 foreach 语句。这是批处理,而不是管道

解决方案:使用 foreach-object

Process {
    $InputObject | Foreach-Object {
        ...
    }
}

foreach($foo in $bar)将收集所有项目,然后迭代。$list | Foreach-Object { ... }分别处理每个项目,允许管道继续

注意:如果输入实际上是一个字符串,您还必须拆分字符串,并将每个元素转换为整数;或者,如果这是您所期望的,请将参数类型更改为整数。

最终答案:

function Chunk-Object()
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true,
                   ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true)] [object[]] $InputObject,
        [Parameter()] [scriptblock] $Process,
        [Parameter()] [int] $ElementsPerChunk
    )

    Begin {
        $cache = @();
        $index = 0;
    }

    Process {
        $InputObject | ForEach-Object {
            $current_element = $_;
            if($Process) {
                $current_element = & $Process $current_element;
            }
            if($cache.Length -eq $ElementsPerChunk) {
                ,$cache;
                $cache = @($current_element);
                $index = 1;
            }
            else {
                $cache += $current_element;
                $index++;
            }
        }
    }

    End {
        if($cache) {
            ,$cache;
        }
    }
}


Set-PSDebug -Off
Write-Host "Input Object is array"
Chunk-Object -InputObject @(1, 2, 3, 4, 5, 6, 7) -Process {$_ + 100} -ElementsPerChunk 3
Write-Host "------------------------------------------------"
Write-Host "Input Object is on pipeline"
@(1, 2, 3, 4, 5, 6, 7) | Chunk-Object -Process {$_ + 100} -ElementsPerChunk 3
Write-Host "------------------------------------------------"
Write-Host "Input object is string"
(echo "1 2 3 4 5 6 7")  | Chunk-Object -Process {$_ + 100} -ElementsPerChunk 3
Write-Host "------------------------------------------------"
Write-Host "Input object is split string"
(echo "1 2 3 4 5 6 7") -split ' ' | Chunk-Object -Process {$_ + 100} -ElementsPerChunk 3
Write-Host "------------------------------------------------"
Write-Host "Input object is int[] converted from split string"
([int[]]("1 2 3 4 5 6 7" -split ' '))  | Chunk-Object -Process {$_ + 100} -ElementsPerChunk 3
Write-Host "------------------------------------------------"
Write-Host "Input object is split and converted"
(echo "1 2 3 4 5 6 7") -split ' ' | Chunk-Object -Process {[int]$_ + 100} -ElementsPerChunk 3
于 2014-01-02T18:03:25.990 回答
1

PowerShell 会自动解开通过管道输入的对象,因此会产生不同的行为。

考虑以下代码:

function Test {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [Object[]] $InputObject
    )

    process {
        $InputObject.Count;
    }
}

# This example shows how the single array is passed
# in, containing 4 items.
Test -InputObject (1,2,3,4);

# Result: 4

# This example shows how PowerShell unwraps the
# array and treats each object individually.
1,2,3,4 | Test;

# Result: 1,1,1,1

考虑到这一点,我们必须以不同的方式对待输入,这取决于它是如何被传入的。

function Test {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [Object[]] $InputObject
        , [ScriptBlock] $Process
    )

    process {
        if ($InputObject.Count -gt 1) {
            foreach ($Object in $InputObject) {
                Invoke-Command -ScriptBlock $Process -ArgumentList $Object;
            }
        }
        else {
            Invoke-Command -ScriptBlock $Process -ArgumentList $InputObject;
        }
    }
}

Test -InputObject (1,2,3,4) -Process { $args[0] + 100 };

Write-Host -Object '-----------------';

1,2,3,4 | Test -Process { $args[0] + 100; };

如果您希望用户能够使用$_而不是$args[0],那么您必须确保该函数的用户process { ... }在其 ScriptBlock 中包含一个块。请参阅以下示例。

function Test {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [Object[]] $InputObject
        , [ScriptBlock] $Process
    )

    process {
        if ($InputObject.Count -gt 1) {
            foreach ($Object in $InputObject) {
                $Object | & $Process;
            }
        }
        else {
            $_ | & $Process;
        }
    }
}

Test -InputObject (1,2,3,4) -Process { process { $_ + 100; }; };

Write-Host -Object '-----------------';

1,2,3,4 | Test -Process { process { $_ + 100; }; };
于 2014-01-02T17:53:07.177 回答
0

不要使用 $Inputobject,而是尝试给它一个像 $Input 这样的参数名称。这是我用于教学的示例函数,它解释了如何:

Function Get-DriveC {
[cmdletbinding()]

Param(
[Parameter(ValueFromPipeline)]
[ValidateNotNullorEmpty()]
[string[]]$Computername = $env:computername)

Begin {
    Write-Verbose "Starting Get-DriveC"
    #define a hashtable of parameters to splat
    $param=@{Computername=$null;class="win32_logicaldisk";errorAction="Stop";
    filter="deviceid='c:'"}
}
Process {
foreach ($computer in $computername) {
  Try {
   Write-Verbose "Querying $computer"
   $param.Computername=$computer
   Get-CimInstance @param
  }
  Catch {
    Write-Warning "Oops. $($_.exception.message)"
  }
} #foreach
} #process

End {
    Write-Verbose "Ending Get-DriveC"
 }

} #end function

我可以通过管道将计算机名称传递给它,或者将数组作为参数值传递。

InputObject 我相信是一个保留字。您可以使用它,但我认为您可能必须在不同的参数集中进行设置。

于 2014-01-02T17:44:33.817 回答