Search code examples
powershellpipeline

trying to write powershell cmdlet accepting pipelined input


I'm trying to get my head around powershell and write a function as cmdlet, found the following code sample in one of the articles, but it doesnt seem to want to work as cmdlet even though it has [cmdletbinding()] declaration on the top of the file.

When I try to do something like

1,2,3,4,5 | .\measure-data 

it returns empty response (the function itself works just fine if I invoke it at the bottom of the file and run the file itself).

Here's the code that I am working with, any help will be appreciated :)

Function Measure-Data {
    <#
    .Synopsis
    Calculate the median and range from a collection of numbers
    .Description
    This command takes a collection of numeric values and calculates the
    median and range. The result is written as an object to the pipeline.
    .Example
    PS C:\> 1,4,7,2 | measure-data

    Median                                    Range
    ------                                    -----
    3                                        6

    .Example
    PS C:\> dir c:\scripts\*.ps1 | select -expand Length | measure-data

    Median                                    Range
    ------                                    -----
    1843                                   178435
    #>

    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$True,ValueFromPipeline=$True)]
        [ValidateRange([int64]::MinValue,[int64]::MaxValue)]
        [psobject]$InputObject 
    )
    
    Begin {
        #define an array to hold incoming data
        Write-Verbose "Defining data array"
        $Data=@()
    } #close Begin
    
    Process {
        #add each incoming value to the $data array
        Write-Verbose "Adding $inputobject"
        $Data+=$InputObject
    } #close process
    
    End {
        #take incoming data and sort it
        Write-Verbose "Sorting data"
        $sorted = $data | Sort-Object
    
        #count how many elements in the array
        $count = $data.Count
        Write-Verbose "Counted $count elements"
        
        #region calculate median
    
        if ($sorted.count%2) {
            <#
            if the number of elements is odd, add one to the count
            and divide by to get middle number. But arrays start
            counting at 0 so subtract one
            #>
            Write-Verbose "processing odd number"            
            [int]$i = (($sorted.count+1)/2-1)            
            #get the corresponding element from the sorted array
            $median = $sorted[$i]    
        }
        else {
            <#
            if number of elements is even, find the average
            of the two middle numbers
            #>
            Write-Verbose "processing even number"
            $i = $sorted.count/2
            #get the lower number
            $x = $sorted[$i-1]
            #get the upper number
            $y = $sorted[-$i]
            #average the two numbers to calculate the median
            $median = ($x+$y)/2
        } #else even
    
        #endregion
    
        #region calculate range

        Write-Verbose "Calculating the range"
        $range = $sorted[-1] - $sorted[0]
    
        #endregion
    
        #region write result

        Write-Verbose "Median = $median"
        Write-Verbose "Range = $range"
        #define a hash table for the custom object
        $hash = @{Median=$median;Range=$Range}
    
        #write result object to pipeline
        Write-Verbose "Writing result to the pipeline"
        New-Object -TypeName PSobject -Property $hash
    
        #endregion
    } #close end
} #close measure-data

this the article where I took the code from: https://mcpmag.com/articles/2013/10/15/blacksmith-part-4.aspx

edit: maybe I should add that versions of this code from previous parts of the article worked just fine, but after adding all the things that make it a proper cmdlet like the help section and verbose lines, this thing just doesnt want to work, and I believe there is something missing, I have a feeling that this could be because it was written for powershell 3 and I am testing it on win 10 ps 5-point-something, but honestly I dont even know in which direction I should look for, that's why I ask you for help


Solution

  • There is nothing wrong with the code (apart from possible optimizations), but the way how you call it can't work:

    1,2,3,4,5 | .\measure-data
    

    When you call a script file that contains a named function, it is expected that "nothing happens". Actually, the scripts runs, but PowerShell does not know which function it should call (there could be multiple). So it just runs any code outside of functions.

    You have two options to fix the problem:

    Option 1

    Remove the function keyword and the curly braces that belong to it. Keep the [cmdletbinding()] and Param sections.

    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$True,ValueFromPipeline=$True)]
        [ValidateRange([int64]::MinValue,[int64]::MaxValue)]
        [psobject]$InputObject 
    )
    
    Begin {
        # ... your code ...
    } #close Begin
    
    Process {
        # ... your code ...
    } #close process
    
    End {
        # ... your code ...
    }
    

    Now the script itself is the "function" and can be called as such:

    1,2,3,4,5 | .\measure-data
    

    Option 2

    Turn the script into a module. Basically you just need to save it with .psm1 extension (there is more to it, but for getting started it will suffice).

    In the script where you want to use the function you have to import the module before you can use its functions. If the module is not installed, you can import it by specifying its full path.

    # Import module from directory where current script is located
    Import-Module $PSScriptRoot\measure-data.psm1
    
    # Call a function of the module
    1,2,3,4,5 | Measure-Data
    

    A module is the way when there are multiple functions in a single script file. It is also more efficient when a function will be called muliple times, because PowerShell needs to parse it only once (it remembers Import-Module calls).