Powershell help - export to CSV not working - exporting EXE version number - help required...

Copper Contributor

Hi, I am struggling to get this code below to output to a CSV file. I know I am doing something wrong.

I need to get a version number from an EXE file on multiple devices.

Thanks in advance all :)


Script Details :

$ExportPath = "C:\Temp"
$ComputerName = Get-Content “C:\Temp\DeviceList.txt”

foreach ($Computer in $ComputerName){

$filePath = “\$computer\c$\Admin\program.exe”
$fileVersion = (Get-Command $filePath).FileVersionInfo.FileVersion

Write-Host “$computer The file version is $fileVersion”
}

6 Replies

@gower666 

 

Hi, Gary.

 

There's at least three different ways you can retrieve file remote file information:

 

  1. WinRM;
  2. SMB
  3. WMI.

 

I'll cover the first two with examples. WMI shares the benefits of WinRM but I don't use WMI much these days, so I'm skipping it.

 

WinRM

Windows Remote Management has been around since about Vista/Server 2008, meaning it's very well established. That said, many people can't be bothered enabling it for remote connections and even fewer configure it to run securely over TLS. (The best and easiest way to enable WinRM for remote connections in a domain environment is via Group Policy, but that's beyond the scope of this discussion.)

 

So, you may or may not be well-positioned to leverage WinRM.

 

This example leverages Invoke-Command, which is far more efficient than using a foreach statement since Invoke-Command can process multiple computers in parallel (i.e. asynchronously), while foreach cannot (it processes one after the other, or "synchronously" - and I'm deliberately focusing on Windows PowerShell here, as it ships "out of the box", where PowerShell does not).

 

Example

This version would run over when using a non-TLS WinRM endpoint.

 

$ExportPath = "D:\Data\Temp\forum\fileInfo.csv";
Invoke-Command -ComputerName (Get-Content -Path "D:\Data\temp\forum\forum.txt") -ScriptBlock {
    Get-Item -Path "C:\Data\SteamSetup.exe" -ErrorAction:SilentlyContinue |
        ForEach-Object {
            [PSCustomObject] @{
                FileName = $_.VersionInfo.FileName;
                Version = $_.VersionInfo.FileVersion;
            }
        }
} |
    Select-Object -Property PSComputerName, Version, FileName |
        Export-Csv -Path $ExportPath -NoTypeInformation;

 

Output

LainRobertson_0-1720002138686.png

 

SMB

SMB is best known as "file and print", which is the approach you've taken in your initial example.

 

SMB is almost always enabled and accessible by default, and is positively ancient - it's been around in Windows NT since inception, which puts it around the early 1990's (though there have been multiple major revisions and it's still a very useful protocol).

 

The downside to the SMB approach is that it's most commonly going to be handled synchronously via a foreach statement. You can put extra effort into the coding to adopt an asynchronous approach, but most people go for easy ahead of efficient, meaning that rarely happens.

 

Because each computer is processed one at a time, this method is becomes more inefficient as more clients are unreachable for whatever reason.

 

Example
$ExportPath = "D:\Data\Temp\forum\fileInfo.csv";
Get-Content -Path "D:\Data\temp\forum\forum.txt" |
    ForEach-Object {
        Get-Item -Path "\\$_\c$\\Data\SteamSetup.exe" -ErrorAction:SilentlyContinue |
            ForEach-Object {
                [PSCustomObject] @{
                    FileName = $_.VersionInfo.FileName;
                    Version = $_.VersionInfo.FileVersion;
                }
            }
    } |
        Select-Object -Property Version, FileName |
            Export-Csv -Path $ExportPath -NoTypeInformation;

 

Output

LainRobertson_1-1720002216241.png

 

WMI

As mentioned, WMI is much the same as WinRM where the primary benefit is that is can process multiple computers at once via the Get-CimInstance commandlet, which - like Invoke-Command - can take multiple computer names in the -ComputerName parameter.

 

So, like the WinRM approach, it scales better when checking large numbers of computers.

 

Cheers,

Lain

@LainRobertson wow that was quick, thank you so much, that worked like a charm (2nd script). Thank you so so much, have a lovey day :)

@gower666 

 

One other quick explanation is the difference between Write-Host and Write-Object - which is the default pathway used when "outputting" to a device (be that file or screen):

 

  • Write-Host writes strings to the information stream, which - put most plainly - means writing messages to the screen, not outputting data to the default stream;
  • Write-Object writes an object to the standard output stream.

 

It's actually more complex than that but simple explanations are a good place to start.

 

The point is that you want to leverage writing objects to the data stream, not strings to the display.

 

For example, if you have the following object:

 

LainRobertson_0-1720006622530.png

 

And you send that output to a CSV file, as shown below:

 

[PSCustomObject] @{ city = "Perth"; PostCode = 6000; } |
   Export-Csv -NoTypeInformation -Path "D:\Data\Temp\Forum\forum.csv";


You get a well-formed CSV file, as we wrote an object out on the data stream (or pipeline:(

 

LainRobertson_1-1720006780511.png

 

But if we write a string (via the information stream) using:

 

Write-Host -Object '"Perth","6000"' | Export-Csv -NoTypeInformation -Path "D:\Data\Temp\Forum\forum.csv";

 

You can see the output has actually gone to the screen:

 

LainRobertson_2-1720007026175.png

 

And nothing has gone to the file, resulting in an empty file:

 

LainRobertson_3-1720007080837.png

 

 

Cheers,

Lain

Morning Lain,

Can you use the same script to extract a files modified date on remote machines?

Similar to this command line (sorry I am new to PowerShell and I really appreciate your help).

Script Example (I want this 2nd script to run against a list of computers in a txt file, and output / append the created file date for a specific file to a csv file):

 

-----------

Script 1 :

-----------


$filePath = "C:\temp\results.txt"
$creationDate = [System.IO.File]::GetCreationTime($filePath)
Write-Output "The file was created on: $creationDate"

 

 

There is also this 2nd script which does the local machine, however I am struggling to get this to work against remote machines (40 in total).

 

----------

Script 2 :

-----------

 

$files = 'C:\path\to\first\dll','C:\path\to\second\dll' $computer = New-Object System.Object $computer | Add-Member -type NoteProperty -Name CompName -value $env:ComputerName foreach ($file in $files){ $lastWrite = Get-Item $file | select LastWriteTime $computer | Add-Member -type NoteProperty -Name $file -value $lastWrite.LastWriteTime } $computer | Export-Csv -Path "\\path\to\server\share" -Append -NoTypeInformation

@gower666 

 

Hi, Gary.

 

Please ensure you post sample code in a code box, as it's impossible to make sense of when it's dumped as unformatted text in the thread. This is what the second script looks like after posting:

 

LainRobertson_0-1720154236239.png

 

And when that wall of text is copied-and-pasted into an IDE, it comes through as a single line, which is just as unreadable - particularly if the semicolon hasn't been used to demarcate separate commands.

 

Moving onto your new question, you can easily piggyback off the example scripts above.

 

While I'm not a fan of the SMB approach as it's slow and doesn't scale well, here's a modified SMB example that gives you the creation and modification timestamps.

 

For the creation and modification dates, we're simply grabbing them from the existing file object returned from Get-Item.

 

I've also demonstrated how you can check multiple files through pre-pending a string array in front of the Get-Item, where the relative file paths are specified as separate strings.

 

There's other, neater ways this can done, but by sticking as close as is possible to the original scripts above, I'm hoping it helps understand the concepts underpinning the changes, the most important being that you're working with objects.

 

Before getting into the example script, notice how there's multiple timestamps to choose from on the file objects returned by Get-Item? This data is what we're going to leverage.

 

LainRobertson_1-1720154666045.png

 

Example
$ExportPath = "D:\Data\Temp\forum\fileInfo.csv";

Get-Content -Path "D:\Data\temp\forum\forum.txt" |
    ForEach-Object {
        $Name = $_;

        @(
            "c$\Data\SteamSetup.exe"
            , "c$\Data\query.txt"
            , "c$\Data\secedit.inf"
        ) |
            ForEach-Object {
                Get-Item -Path "\\$Name\$_" -ErrorAction:SilentlyContinue |
                    ForEach-Object {
                        [PSCustomObject] @{
                            Version = $_.VersionInfo.FileVersion;
                            WhenCreated = $_.CreationTime;
                            WhenChanged = $_.LastWriteTime;
                            FileName = $_.VersionInfo.FileName;
                        }
                    }
            }
    } |
        Export-Csv -Path $ExportPath -NoTypeInformation;

 

Output

LainRobertson_2-1720154767085.png

 

 

Cheers,

Lain

Thank you so much, worked again like a treat. Have a lovely weekend and thanks for all your help and assistance, it really is appreciated :)