Search code examples
powershellinvoke-command

Save an output-file from a cmdlet run in Invoke-Command on a remote machine to a local file


I am trying to run the following command on multiple machines on a LAN (not on a domain). When I run without the Invoke-Command on the local machine it works perfectly. When I try to invoke, it can no longer find the file path on the machine I am running the command to as it is looking at the remote directory. I cannot get a sharedrive to function for this purpose. I had a similar question for which a hash table was suggested and successfully implemented. I cannot figure out how I would do that with the below.

Invoke-Command -Computername $computers -Credential {
 Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName,DisplayVersion,Publisher,InstallDate,InstallLocation | Format-Table -Property DisplayName,DisplayVersion,Publisher,InstallDate,InstallLocation -AutoSize | Out-File -Width 2048 "c:\scripts\ComputerInformation\SoftwareInformation\$env:COMPUTERNAME.software.txt"
        $applications = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*
        foreach ($application in $applications) {
            $hostname = $env:COMPUTERNAME
            $DisplayName = $application.DisplayName
            $displayVersion = $application.DisplayVersion
            $HelpLink = $application.HelpLink
            $IdentifyingNumber = $application.PSChildName
            $InstallDate = $application.InstallDate
            $RegOwner = $null
            $vendor = $application.Publisher
            if ($DisplayName -ne $null -or $DisplayVersion -ne $null -or $HelpLink -ne $null -or $IdentifyingNumber -ne $null -or $InstallDate -ne $null -or $vendor -ne $null ) {
                Add-Content -Path 'c:\scripts\Inventories\SoftwareInventory.csv' "$hostname,$DisplayName,$DisplayVersion,$HelpLink,$IdentifyingNumber,$InstallDate,$RegOwner,$Vendor"
            }

Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName,DisplayVersion,Publisher,InstallDate,InstallLocation | Format-Table -Property DisplayName,DisplayVersion,Publisher,InstallDate,InstallLocation -AutoSize | Out-File -Append -Width 2048 "c:\scripts\ComputerInformation\SoftwareInformation\$env:COMPUTERNAME.software.txt"
        $applications = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*
        foreach ($application in $applications) {
            $hostname = $env:COMPUTERNAME
            $DisplayName = $application.DisplayName
            $displayVersion = $application.DisplayVersion
            $HelpLink = $application.HelpLink
            $IdentifyingNumber = $application.PSChildName
            $InstallDate = $application.InstallDate
            $RegOwner = $null
            $vendor = $application.Publisher      
            if ($DisplayName -ne $null -or $DisplayVersion -ne $null -or $HelpLink -ne $null -or $IdentifyingNumber -ne $null -or $InstallDate -ne $null -or $vendor -ne $null ) {
                Add-Content -Path 'c:\scripts\Inventories\SoftwareInventory.csv' "$hostname,$DisplayName,$DisplayVersion,$HelpLink,$IdentifyingNumber,$InstallDate,$RegOwner,$Vendor"
            }
}

Solution

  • One way to fix this is to deal with the data locally after it's returned instead of when and where it's being processed / gathered.

    There are a few things I'm confused about. You are gathering up some string data and trying to get it into a file, then trying to get somewhat redundant other data into a second comma delimited file. Also you are missing an argument for credential.

    At any rate, for my example / demo I'm going to assume we only need the CSV data.

    $ScriptBlock =
    {
        $Properties = 
        @(
            'DisplayName'
            'DisplayVersion' 
            'HelpLink'
            'IdentifyingNumber'
            'InstallDate'
            'vendor'
        )
        
        Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* |
        ForEach-Object{
            :Inner ForEach( $Property in $Properties )
            {
                If( $_.$Property )
                {
                    [PSCustomObject][Ordered]@{
                        hostname          = $env:COMPUTERNAME
                        DisplayName       = $_.DisplayName
                        DisplayVersion    = $_.DisplayVersion
                        HelpLink          = $_.HelpLink
                        IdentifyingNumber = $_.PSChildName
                        InstallDate       = $_.InstallDate
                        RegOwner          = $null
                        Vendor            = $_.Publisher
                    }
                Break Inner
                }
            }
        }
    }
    
    Invoke-Command -Computername $computers -ScriptBlock $ScriptBlock |
    Export-Csv -Append 'c:\scripts\Inventories\SoftwareInventory.csv' -NoTypeInformation
    

    Note: I'm in a domain environment so I left -Credential off.

    So I'm returning custom objects that are suitable for direct output to CSV. But the key point is to emit what you want inside the script block remotely, let PowerShell return it to the local session where you can then work with it locally. If you have the data local then local file, no problem.

    A few other notes mostly sugar:

    1. I parked the script block in a variable. This is just a preference of mine, I find it easier to work with.
    2. Instead of using 1 long if condition I stored the properties in an array. Then later I looped the array testing each property. If any property hits I emit the object then break the inner loop, so we don't get duplicates. I don't know why you need the if condition in the first place but this just seemed like a more eloquent way to do it.

    Update

    Responding your comments, here's an example that generates sets of files:

    $CsvProps =
    @(
        'hostname'
        'DisplayName'
        'DisplayVersion' 
        'HelpLink'
        'IdentifyingNumber'
        'InstallDate'
        'vendor'
    )
    
    $TxtProps =
    @(
        'DisplayName'
        'DisplayVersion'
        'Publisher'
        'InstallDate'
        'InstallLocation'
    )
    
    $ScriptBlock =
    {
        Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* |
        ForEach-Object{
            [PSCustomObject][Ordered]@{
                hostname          = $env:COMPUTERNAME
                DisplayName       = $_.DisplayName
                DisplayVersion    = $_.DisplayVersion
                HelpLink          = $_.HelpLink
                IdentifyingNumber = $_.PSChildName
                InstallDate       = $_.InstallDate
                RegOwner          = $null
                Vendor            = $_.Publisher
                Publisher         = $_.Publisher
                InstallLocation   = $_.InstallLocation
            }
        }
    }
    
    $ReturnObjects = Invoke-Command -Computername $computers -ScriptBlock $ScriptBlock
    
    # Create Textfiles for each host:
    $ReturnObjects | 
    Group-Object -Property hostname |
    ForEach-Object{
        $FileName = $_.Name # This'll be the hostname, it was the group property.
        $_.Group |     
            Format-Table $TxtProps |
            Out-File -Width 2048 "c:\scripts\ComputerInformation\SoftwareInformation\$FileName.software.txt"
        }
    
    # Create CSV file:
    $ReturnObjects |
    ForEach-Object{
        :Inner ForEach( $Property in $_.PSObject.Properties.Name )
        {   # Write $_ to the pipeline if any property is valid
            If( $Property -eq 'hostname' ) { Continue Inner } # They will all have hostname.
            ElseIf( $_.$Property )
            {
                $_ # Write $_ down the pipeline
                Break Inner
            }
        }
        } |
    Select-Object $CsvProps |
    Export-Csv 'c:\scripts\Inventories\SoftwareInventory.csv' -NoTypeInformation -Append
    

    So this is a bit different than the previous version:

    1. The object in the script block has all the properties needed for both of the desired outputs.
    2. This is different than the method I mentioned earlier. I thought it was better to store the results in a variable, rather than try to extract both goals in 1 ForEach-Object loop.
    3. There is no if logic in the script block. This is so we can get back all objects and work with them locally so we can do the right filtering respective to the text & csv files. Again to be able to write the file locally we need the data local.
    4. Since there are different properties for the text files versus the CSV, there are different collections for each.
    5. The way I filter data for the CSV file is different, but the principal is the same.

    I realize my demo is a departure from your sample. However, I hope it demonstrates the key points and helps you get past the issue.

    FYI: If you have troubles because you're environment is a workgroup, check out Secrets of PowerShell Remoting. I believe there's a section on remoting in a workgroup.

    Let me know. Thanks.