Powershell Export to CSV from ForEach Loop - powershell

I'm new to Powershell but I've given it my best go.
I'm trying to create a script to copy a file to the All Users Desktop of all the XP machines in an Array. The script basically says "If the machine is pingable, copy the file, if not, don't." I then want to export this information into a CSV file for further analysis.
I've set it all but no matter what I do, it will only export the last PC that it ran though. It seems to run through all the PC's (tested with outputting to a txt file) but it will not log all the machines to a CSV. Can anyone give any advise?
$ArrComputers = "PC1", "PC2", "PC3"
foreach ($Computer in $ArrComputers) {
$Reachable = Test-Connection -Cn $Computer -BufferSize 16 -Count 1 -ea 0 -quiet
$Output = #()
#Is the machine reachable?
if($Reachable)
{
#If Yes, copy file
Copy-Item -Path "\\servername\filelocation" -Destination "\\$Computer\c$\Documents and Settings\All Users\Desktop\filename"
$details = "Copied"
}
else
{
#If not, don't copy the file
$details = "Not Copied"
}
#Store the information from this run into the array
$Output =New-Object -TypeName PSObject -Property #{
SystemName = $Computer
Reachable = $reachable
Result = $details
} | Select-Object SystemName,Reachable,Result
}
#Output the array to the CSV File
$Output | Export-Csv C:\GPoutput.csv
Write-output "Script has finished. Please check output files."

The problem is this:
#Store the information from this run into the array
$Output =New-Object -TypeName PSObject -Property #{
SystemName = $Computer
Reachable = $reachable
Result = $details
} | Select-Object SystemName,Reachable,Result
}
#Output the array to the CSV File
$Output | Export-Csv C:\GPoutput.csv
Each iteration of your foreach loop saves to $Output. Overwriting what was there previously, i.e., the previous iteration. Which means that only the very last iteration is saved to $Output and exported. Because you are running PowerShell v2, I would recommend saving the entire foreach loop into a variable and exporting that.
$Output = foreach ($Computer in $ArrComputers) {
New-Object -TypeName PSObject -Property #{
SystemName = $Computer
Reachable = $reachable
Result = $details
} | Select-Object SystemName,Reachable,Result
}
$Output | Export-Csv C:\GPoutput.csv

You would want to append the export-csv to add items to the csv file
Here is an example
foreach ($item in $ITGlueTest.data)
{
$item.attributes | export-csv C:\organization.csv -Append
}

Here you go. This uses PSCustomObject which enumerates data faster than New-Object. Also appends to the .csv file after each loop so no overwriting of the previous data.
foreach ($Computer in $ArrComputers) {
$Reachable = Test-Connection -Cn $Computer -BufferSize 16 -Count 1 -ea 0 -quiet
#Is the machine reachable?
if($Reachable)
{
#If Yes, copy file
Copy-Item -Path "\\servername\filelocation" -Destination "\\$Computer\c$\Documents and Settings\All Users\Desktop\filename"
$details = "Copied"
}
else
{
#If not, don't copy the file
$details = "Not Copied"
}
#Store the information from this run into the array
[PSCustomObject]#{
SystemName = $Computer
Reachable = $reachable
Result = $details
} | Export-Csv C:\yourcsv.csv -notype -Append
}

Related

Powershell - trying to merge 2 result in 1 txt/csv

I'm trying to make a daily script to check status of list of URLS and pinging servers.
I've tried to combine the csv, however, the output of $status code is different from the one in csv
$pathIn = "C:\\Users\\test\\Desktop\\URLList.txt"
$URLList = Get-Content -Path $pathIn
$names = gc "C:\\Users\\test\\Desktop\\hostnames.txt"
#status code
$result = foreach ($uri in $URLList) {
try {
$res = Invoke-WebRequest -Uri $uri -UseBasicParsing -DisableKeepAlive -Method Head -TimeoutSec 5 -ErrorAction Stop
$status = [int]$res.StatusCode
}
catch {
$status = [int]$_.Exception.Response.StatusCode.value__
}
# output a formatted string to capture in variable $result
"$status - $uri"
}
$result
#output to log file
$result | Export-Csv "C:\\Users\\test\\Desktop\\Logs.csv"
#ping
$output = $()
foreach ($name in $names) {
$results = #{ "Host Name" = $name }
if (Test-Connection -Computername $name -Count 5 -ea 0) {
$results["Results"] = "Up"
}
else {
$results["Results"] = "Down"
}
New-Object -TypeName PSObject -Property $results -OutVariable nameStatus
$output += $nameStatus
}
$output | Export-Csv "C:\\Users\\test\\Desktop\\hostname.csv"
#combine the 2 csvs into 1 excel file
$path = "C:\\Users\\test\\Desktop" #target folder
cd $path;
$csvs = Get-ChildItem .\*.csv
$csvCount = $csvs.Count
Write-Host "Detected the following CSV files: ($csvCount)"
foreach ($csv in $csvs) {
Write-Host " -"$csv.Name
}
Write-Host "--------------------"
$excelFileName = "daily $(get-Date -Format dd-MM-yyyy).xlsx"
Write-Host "Creating: $excelFileName"
foreach ($csv in $csvs) {
$csvPath = ".\" + $csv.Name
$worksheetName = $csv.Name.Replace(".csv", "")
Write-Host " - Adding $worksheetName to $excelFileName"
Import-Csv -Path $csvPath | Export-Excel -Path $excelFileName -WorkSheetname $worksheetName
}
Write-Host "--------------------"
cd $path;
Get-ChildItem \* -Include \*.csv -Recurse | Remove-Item
Write-Host "Cleaning up"
Output in PowerShell
200 - https://chargebacks911.com/play-404/
200 - https://www.google.com/
500 - httpstat.us/500/
Host Name Results
----------------
x.x.x.x Down
x.x.x.x Up
Detected the following CSV files: (2)
- daily 26-03-2022.csv
- Logs.csv
--------------------
Creating: daily26-03-2022.xlsx
- Adding daily 26-03-2022 to daily26-03-2022.xlsx
- Adding Logs to daily26-03-2022.xlsx
--------------------
Cleaning up
\----------------------------------
result in excel
\#Hostname
Host Name Results
x.x.x.x Down
x.x.x.x Up
\#Logs
Length
42
29
22
I would like to know
how to correct the output in "Logs" sheet
if there's anyway to simplify my script to make it cleaner
Welcome to SO. You're asking for a review or refactoring of your complete script. I think that's not how SO is supposed be used. Instead you may focus on one particular issue and ask about a specific problem you have with it.
I will focus only on the part with the query of the status of your servers. You should stop using Write-Host. Instead you should take advantage of PowerShells uinique feature - working with rich and powerful objects instead of stupid text. ;-)
I'd approach the task of querying a bunch of computers like this:
$ComputernameList = Get-Content -Path 'C:\Users\test\Desktop\hostnames.txt'
$Result =
foreach ($ComputerName in $ComputernameList) {
[PSCustomObject]#{
ComputerName = $ComputerName
Online = Test-Connection -ComputerName $ComputerName -Count 1 -Quiet
}
}
$result
Now you have a PowerShell object you can pipe to Export-Csv for example or use it for further steps.
For example filter for the offline computers:
$result | Where-Object -Property Online -NE -Value $true
If you insist to have a visual control during the runtime of the script you may use Write-Verbose or Write-Debug. This way you can switch on the output if needed but omit it when the script runs unattended.

CSV Output Unreadable

I'm trying to export the results of this simple script to a .csv file. I get the results but it either returns information about data of the results or a long jumble of data I'm sure how to parse correctly.
<#
Will ping all devices in list and display results as a simple UP or DOWN, color coded Red or Green.
#>
$Names = Get-Content -Path "C:\TestFolder\GetNames.txt"
$Output = #()
foreach ($name in $names)
{
if (Test-Connection -ComputerName $name -Count 1 -ErrorAction SilentlyContinue)
{
$Result1 += "$name, up"
Write-Host "$name, up" -ForegroundColor Green
}
else
{
$Result2 += "$name, down"
Write-Host "$name, down" -ForegroundColor Red
}
}
$Output += $Result1, $Result2
$Output = $Output | Select-Object
$Output | Export-Csv -Path 'C:\psCSVFiles\mycsv.csv' -NoTypeInformation
Results of this return:
Length
49768
25081
What am I doing wrong here? Thanks
Don't attempt to "format" the output strings manually - Export-Csv will take care of that part.
What you want to do is create objects with properties corresponding to the columns you want in your CSV:
$Names = Get-Content -Path "C:\TestFolder\GetNames.txt"
$Output = foreach ($name in $names) {
# test if computer is reachable/pingable
$isUp = Test-Connection -ComputerName $name -Count 1 -ErrorAction SilentlyContinue -Quiet
# construct output object with results
[pscustomobject]#{
ComputerName = $name
Status = if($isUp){ 'Up' }else{ 'Down' }
}
}
# Export to CSV
$Output |Export-Csv -Path 'C:\psCSVFiles\mycsv.csv' -NoTypeInformation
Alternatively, use Select-Object to modify the input strings directly using the pipeline:
Get-Content -Path "C:\TestFolder\GetNames.txt" |Select #{Name='ComputerName';Expression={$_}},#{Name='Status';Expression={if(Test-Connection -ComputerName $_ -Count 1 -ErrorAction SilentlyContinue -Quiet){'Up'}else{'Down'}}} |Export-Csv -Path 'C:\psCSVFiles\mycsv.csv' -NoTypeInformation

Powershell - looping through an array

I'm looking to search the C and E drives of all Windows servers in
Active Directory for any existing copies of putty.exe and their version.
The output needs to have the server name, full path to the executable,
and the file version. So far I have the following code (which right now is only using
two servers for testing:
$ComputerName = Get-ADComputer -filter "name -like 'computer01' -or name `
-like 'server01'" | select -ExpandProperty name
$OutputArr = #()
$findFiles = foreach($computer in $computername){
$file = Invoke-Command -computername $computer { Get-ChildItem -Path `
c:\, e:\ -Recurse | where-object{(!$_.psiscontainer -eq $true) -and `
($_.name -like "putty.exe")} | ForEach-Object -process {$_.fullname} }
$output = $OutputObj = New-Object -TypeName PSobject
$OutputObj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computer
$OutputObj | Add-Member -MemberType NoteProperty -Name FilePath -Value $file
$OutputArr += $OutputObj
Write-Verbose $OutputObj
}
$OutputArr | fl
The above code outputs the following array:
ComputerName : COMPUTER01
FilePath : {C:\Program Files\PuTTY\putty.exe, C:\Program Files (x86)\PuTTY\PUTTY.EXE}
ComputerName : SERVER01
FilePath : {C:\Program Files (x86)\putty\putty.exe, C:\Users\testuser\Desktop\Public Desktop\putty.exe}
This produces the correct data, but now I need to run another snippet of code against each
separate filepath under computername, but am not sure how to accomplish this, as it is
taking the full filepath with multiple entries.
Essentially, I need to separate each ComputerName in the array into multiple lines:
COMPUTER01,C:\Program Files\PuTTY\putty.exe
COMPUTER01,C:\Program Files (x86)\PuTTY\PUTTY.EXE
SERVER01,C:\Program Files (x86)\putty\putty.exe
Etc...
Is an array not the correct way to do it?
If you are working strictly with what you already have stored in $OutputArr, the following will work:
$out = foreach ($line in $OutputArr) {
if ($line.filepath.count -gt 1) {
foreach ($fp in $line.FilePath) {
[pscustomobject][ordered]#{ComputerName = $line.ComputerName; FilePath = $fp}
}
}
else {
$line
}
}
$out | ConvertTo-Csv -NoTypeInformation
The foreach loop creates new objects with properties ComputerName and FilePath and stores them in $out as an array of objects.
If you do not care about properties and just want a comma-delimited list, you can use the following:
foreach ($line in $OutputArr) {
if ($line.filepath.count -gt 1) {
foreach ($fp in $line.FilePath) {
"{0},{1}" -f $line.ComputerName,$fp
}
}
else {
"{0},{1}" -f $line.ComputerName,$line.FilePath
}
}
This does the same looping as the first solution but instead uses the format operator (-f) to format the output. Piping to ConvertTo-Csv formats the output to be comma-delimited with the properties as headers.
You could move your desired functionality into your code before you even store anything in $OutputArr. I feel like doing all this after all of the other looping to create $OutputArr is just adding inefficiency.
PowerShell can get tricky when doing remote sessions. The below script should be a good starting point for you. Here are some other areas for improvement:
Doing Get-ChildItem -Recurse at the root of a drive will use an inordinate amount of memory and you could cause unintentional page file expansion or even make a server unresponsive due to 100% memory usage. In my snippet below I am using a list of well known paths. If you need to to identify if putty.exe is started on additional machines, your monitoring solution hopefully has process performance data and you can search for putty.exe there.
Speaking of memory management, remote shells have limitations of how much memory they can use. If you run winrm get winrm/config/winrs you will see the upper limit.
If you are going to authenticate to additional resources from within your remote script blocks, you will need to set up authentication that supports double hop scenarios (CredSSP or Kerberos)
$computerNames = #('computer1','computer2')
foreach($computer in $computerNames)
{
<#
First Script Block checks well known paths for putty.exe
#>
$puttyResults = Invoke-Command -ComputerName $computer -ScriptBlock {
$wellKnownPaths = #()
$wellKnownPaths += Join-Path $env:USERPROFILE -ChildPath "Desktop"
$wellKnownPaths += "D:\tools\"
$wellKnownPaths += $env:Path.Split(';')
$puttyPaths = #()
foreach($path in $wellKnownPaths)
{
$puttyPaths += Get-ChildItem $path -Filter "putty.exe" -Recurse
}
if($puttyPaths.Count -gt 0)
{
$resultsArray = #()
foreach($path in $puttyPaths)
{
$resultsArray += [PSCustomObject]#{
ComputerName = $env:COMPUTERNAME
PuttyPath = $path.FullName
}
}
return $resultsArray
}
return $null
}
if($puttyResults -ne $null)
{
foreach($result in $puttyResults)
{
<#
Second script block takes action against putty.exe
#>
$puttyExists = Invoke-Command -ComputerName $computer -ArgumentList #($result.PuttyPath) -ScriptBlock {
Param(
$PuttyPath
)
return (Test-Path $PuttyPath)
}
if($puttyExists)
{
$msg = "Putty exists on '{0}', at '{1}'" -f $result.ComputerName, $result.PuttyPath
Write-Host $msg -ForegroundColor:Yellow
}
}
}
}
I am not sure what exactly you are wanting to do, but this should work for iterating through your custom object. Your Invoke-Command can be simplified also.
$file = Invoke-Command -computername $computer { Get-ChildItem -Path "C:\", "E:\" -Recurse -File -Filter "putty.exe" | Select -Property VersionInfo }
$OutputObj = New-Object -TypeName PSobject
$OutputObj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $env:COMPUTERNAME
$OutputObj | Add-Member -MemberType NoteProperty -Name FilePath -Value $file
$OutputArr += $OutputObj
foreach ($item in $OutputArr)
{
for ($i = 0; $i -lt $item.FilePath.Count; $i++)
{
Write-Output ([string]::Join(', ', $item.ComputerName, $item.FilePath[$i].VersionInfo.FileName, $item.FilePath[$i].VersionInfo.FileVersion))
}
}

Writing errors to CSV in Powershell

$csv = Get-Content c:\users\user\downloads\OutofContact.csv
foreach ($computer in $csv)
{
try{
$report = New-Object -TypeName PSObject -Property #{
ComputerName = (Resolve-DnsName $computer).Name
IPAddress = (Resolve-DnsName $computer).IPAddress
}
$report | select-object -Property ComputerName, IPAddress | Export-Csv -Path Results.csv -notype -append
}catch{
Write-Error "$computer not found" | Export-Csv -Path Results.csv -notype -append
}
}
I'm using the above code to check the DNS entries for a list of machines. Some of the machines do not exist in DNS and will throw an error. I want those machines to write the error into the CSV, however they just show up as blank rows.
How can I get the errors to write to the CSV as well?
I would refactor and optimize duplicate calls, and only add one object at the end...
Something like this:
#$csv = Get-Content c:\users\user\downloads\OutofContact.csv
# test
$csv = #('localhost', 'doesnotexist', 'localhost', 'doesnotexist')
$allReports = [System.Collections.ArrayList]::new()
foreach ($computer in $csv)
{
$report = [pscustomobject]#{
'ComputerName' = $computer
'IPAddress' = 'none'
'Status' = 'none'
}
try
{
# this can return multiple entries/ipaddresses
$dns = Resolve-DnsName $computer -ErrorAction Stop | Select -First 1
$report.ComputerName = $dns.Name
$report.IPAddress = $dns.IPAddress
$report.Status = 'ok'
}
catch
{
Write-Error "$computer not found"
}
finally
{
$null = $allReports.Add($report);
}
}
# write to csv file once...
$allReports | Export-Csv -Path c:\temp\Results.csv -NoTypeInformation #??? keep this? -Append
You will want to walk through the code and debug and change to your specific requirements.

How to export data to CSV in PowerShell?

foreach ($computer in $computerlist) {
if((Test-Connection -Cn $computer -BufferSize 16 -Count 1 -ea 0 -quiet))
{
foreach ($file in $REMOVE) {
Remove-Item "\\$computer\$DESTINATION\$file" -Recurse
Copy-Item E:\Code\powershell\shortcuts\* "\\$computer\$DESTINATION\"
}
} else {
Write-Host "\\$computer\$DESTINATION\"
}
}
I want to export Write-Host "\$computer\$DESTINATION\" to the CSV files so I know which computers were offline when the script ran.
I am running this from a Windows 7 machine
This solution creates a psobject and adds each object to an array, it then creates the csv by piping the contents of the array through Export-CSV.
$results = #()
foreach ($computer in $computerlist) {
if((Test-Connection -Cn $computer -BufferSize 16 -Count 1 -ea 0 -quiet))
{
foreach ($file in $REMOVE) {
Remove-Item "\\$computer\$DESTINATION\$file" -Recurse
Copy-Item E:\Code\powershell\shortcuts\* "\\$computer\$DESTINATION\"
}
} else {
$details = #{
Date = get-date
ComputerName = $Computer
Destination = $Destination
}
$results += New-Object PSObject -Property $details
}
}
$results | export-csv -Path c:\temp\so.csv -NoTypeInformation
If you pipe a string object to a csv you will get its length written to the csv, this is because these are properties of the string, See here for more information.
This is why I create a new object first.
Try the following:
write-output "test" | convertto-csv -NoTypeInformation
This will give you:
"Length"
"4"
If you use the Get-Member on Write-Output as follows:
write-output "test" | Get-Member -MemberType Property
You will see that it has one property - 'length':
TypeName: System.String
Name MemberType Definition
---- ---------- ----------
Length Property System.Int32 Length {get;}
This is why Length will be written to the csv file.
Update: Appending a CSV
Not the most efficient way if the file gets large...
$csvFileName = "c:\temp\so.csv"
$results = #()
if (Test-Path $csvFileName)
{
$results += Import-Csv -Path $csvFileName
}
foreach ($computer in $computerlist) {
if((Test-Connection -Cn $computer -BufferSize 16 -Count 1 -ea 0 -quiet))
{
foreach ($file in $REMOVE) {
Remove-Item "\\$computer\$DESTINATION\$file" -Recurse
Copy-Item E:\Code\powershell\shortcuts\* "\\$computer\$DESTINATION\"
}
} else {
$details = #{
Date = get-date
ComputerName = $Computer
Destination = $Destination
}
$results += New-Object PSObject -Property $details
}
}
$results | export-csv -Path $csvFileName -NoTypeInformation
simply use the Out-File cmd but DON'T forget to give an encoding type:
-Encoding UTF8
so use it so:
$log | Out-File -Append C:\as\whatever.csv -Encoding UTF8
-Append is required if you want to write in the file more then once.
what you are searching for is the Export-Csv file.csv
try using Get-Help Export-Csv to see whats possible
also Out-File -FilePath "file.csv" will work
You can always use the
echo "Column1`tColumn2`tColumn3..." >> results.csv
You will need to put "`t" between the columns to separates the variables into their own column. Here is the way I wrote my script:
echo "Host`tState" >> results.csv
$names = Get-Content "hostlist.txt"
foreach ($name in $names) {
$count = 0
$count2 = 13490
if ( Test-Connection -ComputerName $name -Count 1 -ErrorAction SilentlyContinue ) {
echo "$name`tUp" >> results.csv
}
else {
echo "$name`tDown" >> results.csv
}
$count++
Write-Progress -Activity "Gathering Information" -status "Pinging Hosts..." -percentComplete ($count / $count2 *100)
}
This is the easiest way to me. The output I get is :
Host|State
----------
H1 |Up
H2 |UP
H3 |Down
You can play around with the look, but that's the basic idea. The $count is just a progress bar if you want to spice up the look