Rachel5309
Rachel5309

Reputation: 53

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"
            }
}

Upvotes: 0

Views: 2544

Answers (2)

Steven
Steven

Reputation: 7067

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.

Upvotes: 1

RetiredGeek
RetiredGeek

Reputation: 3158

Rachel,

Try it this way.

#--- Set the path for location of NetComps.txt ---
$NetPath = "\\MYBOOKLIVE\CMShared\Misc"
[Array]$Computer = Get-Content "$NetPath\NetComps.txt"

$OutPath = '\\DellXPS8920\BEKDocs-DellXPS8920\Test.txt'

ForEach ($Comp in $Computer) {

  $TCArgs = @{ComputerName = "$Comp"
              Quiet        = $True
              Count        = 1}

  If (Test-Connection @TCArgs) {
    
    Try {
       $ICArgs = @{ComputerName = "$Comp"
                   ScriptBlock  = {gci -Path "G:\BEKDocs\Transfer\*.*"}
                   ErrorAction  = "Stop"
                  }
       $x = Invoke-Command @ICArgs  | Out-String

       If ($x -eq "") {
         Add-Content -Path "$OutPath" "Computer: $Comp - Directory Found but EMPTY"
       }
       Else {Add-Content -Path "$OutPath" "$x"}

    }
    Catch {Add-Content -Path "$OutPath" "Computer: $Comp - Directory Not Found"}

  } #End If (Test...
}   #End ForEach...

HTH

Upvotes: 1

Related Questions