handle input from pipeline

This topic contains 8 replies, has 2 voices, and was last updated by Profile photo of Paul Kelly Paul Kelly 3 years ago.

  • Author
    Posts
  • #14569
    Profile photo of Paul Kelly
    Paul Kelly
    Participant

    I've been going through Don Jones videos from CBTNuggets (they are great).

    I'm starting to separate my scripts into "tools" and "coordination/process" scripts. My scripts are as follows:

    Script1: Import-CSV and output the object.
    Script2: Receive and use the output.
    Script3: The coordination script that pipes them together.

    I don't know how to retrieve the object properly in Script2. Any hlep is appreciated.

    #Script1.ps1
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True)]
        [string]$csv
    )
    $obj = Import-CSV $csv
    Write-Output $obj
    
    #Script2.ps1
    
    
    #Script3.ps1
    Script1.ps1 -csv Servers.csv | Script2.ps1
    

    Error:
    The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of
    the parameters that take pipeline input.

  • #14571
    Profile photo of Don Jones
    Don Jones
    Keymaster


    [Parameter(Mandatory=$True,ValueFromPipeline=$True)]
    [object[]]$MyParam

    Encodes -MyParam to accept pipeline input of objects of the type "object." See "about_functions_advanced_parameters" in the shell.

    The object type is important. If Script1 is outputting strings, then -MyParam should be of type [string[]]. Note that [string] is fine if you'll *only ever provide pipeline input or one item at a time*.

    So, this isn't a question of "retrieving the object in Script2." Scripts only accept input via parameters; you have to tell PowerShell which parameter(s) should accept the incoming pipeline stuff.

  • #14586
    Profile photo of Paul Kelly
    Paul Kelly
    Participant

    Thanks so much! I'm a step further... maybe I'm complicating it. What I'm trying to do is script1 import list of servers. script2 execute a command for each server.


    Write-Output $obj | Out-String


    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$True,ValueFromPipeline=$True)]
    [string[]]$MyParam
    )
    Write-Verbose ($MyParam | Out-String)

    Here's the results from each script. I've tried to ForEach which didn't work out. Do I need to split $MyParam? Here's the Verbose output:


    ComputerName
    ------------
    EVLAWSUS01
    EVLAUTL01

  • #14587
    Profile photo of Don Jones
    Don Jones
    Keymaster

    Well, a couple of things. You need to keep watching the videos, maybe. I feel like you've maybe skipped ahead a bit and missed some of the basics about how PowerShell manages pipeline input. When you write a pipeline function or script, it has to have a particular form.


    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$True,ValueFromPipeline=$True)]
    [string[]]$MyParam
    )
    PROCESS {
    foreach ($Thing in $MyParam) {
    # $thing will be one string object.
    }
    }

    The PROCESS block is needed to tell the shell what bit of your script is processing pipeline objects. But because your parameter is an array[], you still need to enumerate it. Now, this assumes you're piping strings into it. A CSV isn't a string.

    But it's really tough to help further when I'm not seeing everything. For example, I've no idea why Out-String is being brought into this, and depending on what you're doing, it could be resulting in you not getting what you want. You need to pay attention to the difference between dealing with ONE of something, and dealing with a COLLECTION of somethings. You also need to understand that PowerShell likes dealing with objects, not text. Out-String produces text. When you convert an object to text, you remove much of the shell's ability to comprehend and manipulate that object.

    You should try and get out of the habit of using Out-String unless you're in a specific situation where you NEED it, and you KNOW you need it. For example, I would construct a verbose like this:


    Write-Verbose "Now dealing with $thing"

    If $thing contains a string, that's all you need to do.

    But this all comes down to input. For example, suppose I have a CSV file that has a ComputerName column. I could NOT Import-CSV that and pipe it to the script, using the parameters defined here. Import-CSV would produce objects having ComputerName properties – not strings. If that Import-CSV was the goal:


    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
    [string[]]$MyParam
    )
    PROCESS {
    foreach ($Thing in $MyParam) {
    # $thing will be one string object.
    }
    }

    Now the -MyParam parameter can accept incoming strings, or it can accept incoming objects that have a MyParam property. Changing that:


    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
    [string[]]$ComputerName
    )
    PROCESS {
    foreach ($Computer in $ComputerName) {
    # $Computer will be one string object.
    }
    }

    Now the -ComputerName parameter can accept incoming strings, or it can accept incoming objects that have a ComputerName property. So, if you had a CSV file that had a ComputerName column, you could:


    Import-CSV data.csv | ./MyScript

    Again, assuming that CSV had a ComputerName column.

    "Learn PowerShell Toolmaking in a Month of Lunches" (not to make you buy another training thing) really goes through this entire progression, also.

    BUT... it's important when you post here to show an example of each step. Like, if I have no idea what the input looks like, it's tough for me to look at your code snippets and figure out what PowerShell is trying to do. Just a sample of "here's what the CSV file looks like," for example, and "here's what the first script is doing with it," can help a lot!

  • #14594
    Profile photo of Don Jones
    Don Jones
    Keymaster

    So, running Import-CSV against that file will produce two objects, each having a ComputerName property. That won't bind ByValue to a [string], but it can bind ByPropertyName:


    [Parameter(ValueFromPipelineByPropertyName=$True)]
    [string[]]$ComputerName

    But the parameter name (-ComputerName) and expected property name (ComputerName) HAVE TO BE SPELLED THE SAME for the magic to work. So it can't bind to -Csv, or -MyParam, or anything else.

    And you do have to knock it off with Out-String. You're just upsetting the shell.


    $Computers = Import-Csv Servers.txt | Select -Expand ComputerName

    Would give you an array of two [string] objects in $Computers. But when you do this:


    Write-Verbose "ComputerName $Computers"

    You're again missing some subtle things that the shell (v3 and later) does under the hood. You've given it a variable containing two [string] objects. It'll display them, but it'll control how it formats that display. You normally want to enumerate them.


    foreach ($computer in $computers) {
    Write-Verbose "Current computer is $computer"
    }

    So that you can work with each computer one at a time.

    In short, your entire Import-PKCSV.ps1 is redundant. You could just do this:


    Import-CSV servers.csv | .\Invoke-PKWSUSReport.ps1

    And it'd work fine. Your Import-PKCSV.ps1 is just confusing things. There's no need for it. PowerShell already knows how to import CSVs without your help. You've just written a function that takes the usable, object-based output of Import-CSV and turns it into a difficult array of strings. Yuck.

    Now, your error message is being generated by Get-Process. You didn't show me Get-Process in your code snippets, so I've no idea what you're doing with it.

  • #14596
    Profile photo of Paul Kelly
    Paul Kelly
    Participant

    Thanks! I appreciate your explanations! I had hopes to be able to pull from AD in the future and just use csv for now but I just simplified it. Here's my results that work.


    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$True)]
    [string]$csv
    )
    $Computers = Import-Csv $csv | Select -ExpandProperty ComputerName
    Write-Verbose "Computers $Computers"
    ForEach ($Computer in $Computers) {
    If ($Computer -eq $env:COMPUTERNAME) {
    #Force client to update server, only runs while agent is idle, automatically runs 20min after activity.
    Write-Verbose "$Computer wuauclt /r /reportnow"
    wuauclt /r /reportnow
    }
    ELSE {
    #Force client to update server, only runs while agent is idle, automatically runs 20min after activity.
    Write-Verbose "Invoke-Command -ComputerName $Computer {wuauclt /r /reportnow}"
    Invoke-Command -ComputerName $Computer {wuauclt /r /reportnow}
    }
    }


    .\Invoke-PKWSUSReport.ps1 -csv Servers.csv -Verbose

  • #14598
    Profile photo of Paul Kelly
    Paul Kelly
    Participant

    I fumbled the ForEach within the Process which made me give up and add the input to the tool. Thanks for the complete script and the PassThru tip. Super cool! I'm on my way to making tools properly :p

    Thanks again for your help and patience.

  • #14593
    Profile photo of Paul Kelly
    Paul Kelly
    Participant

    You're exactly right... I've been multitasking and skipped around... I see how I need to and will go through the full series from the beginning. I did see that book and after the videos may pick it up. I actually have your Windows Administrator's Automation Toolkit from MS Press :p

    I assume I'm screwing things up with between object/string. Here's all the info and the error I get.

    
    #Servers.csv
    ComputerName
    EVLAWSUS01
    EVLAUTL01
    
    
    #Import-PKCSV.ps1
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True)]
        [string]$csv
    )
    $ComputerName = Import-CSV $csv | Out-String
    Write-Verbose "ComputerName: $ComputerName"
    Write-Output $ComputerName
    
    
    #Invoke-PKWSUSReport.ps1
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
        [string[]]$ComputerName
    )
    Write-Verbose "Computername: $ComputerName"
    Process {
        ForEach ($Computer in $ComputerName) {
            Write-Verbose "ForEach: $Computer"
        }
    }
    
    
    #CoordinationScript.ps1
    Import-PKCSV.ps1 -csv Servers.csv -Verbose | Invoke-PKWSUSReport.ps1 -Verbose
    
    
    #Error
    Get-Process : Cannot evaluate parameter 'Name' because its argument is specified as a script block and there is no input. A script block cannot be evaluated without input.
    
  • #14597
    Profile photo of Don Jones
    Don Jones
    Keymaster

    well, the beauty of doing it right is that changing your input is easy.

    But you've not really done it right :).

    You've got a single script dealing with both input, work, and output. That's a poor design pattern. If you want to change this to work from AD, you actually have to change it. If you follow the right design pattern, it'll do everything.


    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
    [string[]]$ComputerName,
    [switch]$PassThru
    )
    PROCESS {
    ForEach ($Computer in $ComputerName) {
    If ($Computer -eq $env:COMPUTERNAME) {
    #Force client to update server, only runs while agent is idle, automatically runs 20min after activity.
    Write-Verbose "$Computer wuauclt /r /reportnow"
    wuauclt /r /reportnow
    }
    ELSE {
    #Force client to update server, only runs while agent is idle, automatically runs 20min after activity.
    Write-Verbose "Invoke-Command -ComputerName $Computer {wuauclt /r /reportnow}"
    Invoke-Command -ComputerName $Computer {wuauclt /r /reportnow}
    }
    if ($PassThru) { Write $Computer }
    }
    }

    If this is Invoke-WSUSUpdate.ps1...


    Import-CSV servers.csv | .\Invoke-WSUSUpdate.ps1 # Assumes CSV has ComputerName column
    Get-ADComputer -filter * | Select @{n='ComputerName';e={$_.Name}} | .\Invoke-WSUSUpdate.ps1
    Get-Content names.txt | .\Invoke-WSUSUpdate.ps1 # Assumes one computer name per line

    Tools that perform a task shouldn't worry about getting their input – it should come via a parameter, which can accept pipeline input. When the tool doesn't care where the input comes from, the tool can accept input from many places. PowerShell can get computer names from many sources – why limit yourself to just one by hardcoding it into the tool?

    My version an happily accept input from wherever I can get it. AD, a CSV, a text file, a database, whatever. I might have to write a tool (for example) to get computer names out of an SCCM database, but THAT tool can feed THIS one. Keeps everything flexible.

    And with a -PassThru switch, THIS tool can be made to output the computer name it just operated against, perhaps feeding a second tool that produces a report or double-checks the operation.

    Write tools that conform to PowerShell patterns and you end up working less.

You must be logged in to reply to this topic.