ForEach-Object piped to CSV is only showing last element - powershell

I'm trying to export to csv the scheduled tasks for multiple remote machines. I'm using a modified version of this script. I'm trying to export one csv per machine that lists all of the scheduled tasks. Currently my code just exports the last task for each machine.
foreach ($computerName in $computerNames) {
$Schedule.connect($computerName)
$AllFolders = Get-AllTaskSubFolders
foreach ($Folder in $AllFolders) {
if ($Tasks = $Folder.GetTasks(0)) {
$TASKS | % {[array]$results += $_}
$Tasks | Foreach-Object {
New-Object -TypeName PSCustomObject -Property #{
'Name' = $_.name
'Path' = $_.path
'Server' = $computername
} | Export-Csv $("C:\Users\MyName\Desktop\" + $computername + ".csv")
}
}
}
}
I've tried putting the Export-Csv at the end of each of the curly braces and none output what I want. Any ideas what I'm doing wrong?

EDIT
I'm not sure this fully addresses the issue, as some parts of the code seem weired to me such as $tasks = $folder.GetTasks. However OP title clearly says that the CSV is showing the last element meaning to me that his code mostly works except for the Export-CSV part.
Export-Csv expects an array as input, so the code below uses the elements in $task to generate the array of objects. Select-Object is there to decide in which order the properties are stored in the CSV.
Try this:
foreach ($computerName in $computerNames) {
$Schedule.connect($computerName)
$AllFolders = Get-AllTaskSubFolders
$result = #()
foreach ($Folder in $AllFolders) {
if ($Tasks = $Folder.GetTasks(0)) {
$TASKS | % {[array]$results += $_}
$Tasks | Foreach-Object {
$result += New-Object -TypeName PSCustomObject -Property #{
Name = $_.name;
Path = $_.path;
Server = $computername
}
}
}
}
$result | select Name, Path, Server | Export-Csv $($exportPath + $computername + ".csv")
}
This new version saves results on a per computer basis.

You are not changing either $exportPath or $computername in your loop, so each time through your outer loop, the csv file is being overwritten. Also, this
if ($Tasks = $Folder.GetTasks(0)) {
may be wrong. If you're trying to determine if $Tasks is equal to $Folder.GetTasks(0), you'd need this:
if ($Tasks -eq $Folder.GetTasks(0)) {

Related

Trying to add PSCustomObjects to an ArrayList on a remote machine

I have some experience with PowerShell, and usually Google or searching forums like these yields the answers when I have questions - but not this time.
I'm trying to collect the number of .log files in a directory on a remote server, then I'd like to store the location (drive letter and folder path) and the count in an array list for later. So far everything is working as I'd expect, but I'm running into trouble adding my PSCustomObjects to the array list. I'm not sure if it's because I'm executing on a remote server or if something else is causing the problem. Here is my code:
$server = Read-Host -Prompt 'Please enter the server name'
[System.Collections.ArrayList]$returnObj = #()
Invoke-Command -ComputerName $server {
$drives = Get-PSDrive -PSProvider FileSystem |
Where-Object {$_.Description -like "ExVol*"} |
Select-Object Root
foreach ($d in $drives) {
Set-Location -Path $d.Root
$folders = Get-ChildItem -Path $d.Root |
Where-Object {$_.Name -like "*.log"} |
Select-Object Name
foreach ($f in $folders) {
$count = (Get-ChildItem -Path $f.Name).Count
$obj = [PSCustomObject]#{
LogFolder = $d.Root.Trim() + $f.Name
LogFileCount = $count
}
Write-Host $obj
$returnObj.Add($obj | Select-Object DatabaseFolder,LogFileCount)
}
}
}
$returnObj
In this format I get a syntax error on the line
$returnObj.Add($obj | Select-Object DatabaseFolder,LogFileCount)
If I change the above line to $returnObj.Add($obj) I avoid the syntax error, but instead I get an error saying I cannot call a method on a null valued expression.
I've tried creating the ArrayList inside the Invoke-Command and I've tried using New-Object instead of PSCustomObject to no avail.
I think your mixing stuff a bit up, this will do:
$returnObj = Invoke-Command -ComputerName $server {
$drives = Get-PSDrive -PSProvider FileSystem |
Where-Object {$_.Description -like "ExVol*"} |
Select-Object Root
foreach ($d in $drives) {
Set-Location -Path $d.Root
$folders = Get-ChildItem -Path $d.Root |
Where-Object {$_.Name -like "*.log"} |
Select-Object Name
foreach ($f in $folders) {
$count = (Get-ChildItem -Path $f.Name).Count
[PSCustomObject]#{
LogFolder = $d.Root.Trim() + $f.Name
LogFileCount = $count
}
}
}
}
$returnObj
The problem is this line:
[System.Collections.ArrayList]$returnObj = #()
is declared outside of the Invoke-Command -ScriptBlock. This means it's not available within the session on the remote machine, and as such can not be used there.
On a side note, you cannot fill an array like you fill a Hashtable with data.
Arrays are filled like $MyArray = #(); $MyArray += 'MyValue'
Hashtables like $MyHash=#{}; $MyHash.SomeKey = 'SomeValue' or as you indicated $MyHash.Add('SomeKey', 'SomeValue')
ArrayLists are filled like [System.Collections.ArrayList]$MyArrayList = #(); $MyArrayList.Add('SomeValue')
I hope this makes it a bit more clear. The return values can always be catched before the Invoke-Command or even before a simple foreach (). For example $result = 0..3 | ForEach-Object {$_} is perfectly valid too.
You need to actually return your object from the remote system to your local system since you cannot use your $returnObj within the remote session.
As an example:
$returnValue = Invoke-Command -ComputerName $server {
$obj = [PSCustomObject]#{
LogFolder = $d.Root.Trim() + $f.Name
LogFileCount = $count
}
#return the object via the pipline
$obj
}
$returnObj.Add($returnValue | Select-Object DatabaseFolder,LogFileCount)
The above example is lacking of proper error handling, therefore you would get an error if the remote system is not reachable but it's a start.

Check Multiple Computers if Path Exists and Export to CSV

I want to make a script that will check whether or not a directory exists on each computer in "computers.csv".
This is what I've come up with:
$results = #()
$computers = Get-Content "C:\Users\me\Desktop\Notes\Computers.csv"
foreach ($computer in $computers) {
$path = Test-Path "\\$computer\c$\Program Files\Folder\"
if ($path -eq $true)
$Output = "True"
else
$Output = "False"
}
$details = #{
Computer_Name = $computer
Output = $Output
}
$results += New-Object PSObject -Property $details
$results |
Select-Object -Property Computer_Name,Output |
Export-Csv c:\results.csv -NoTypeInformation
Script is failing and I'm not entirely sure why. I need the script to export to a CSV due to how many computers are being queried.
You've got several syntax errors. You're missing brackets with if and else, and your foreach closing bracket is in the wrong place. Try this:
$results = #()
$computers = Get-Content "C:\Users\me\Desktop\Notes\Computers.csv"
foreach ($computer in $computers) {
$path = Test-Path "\\$computer\c$\Program Files\Folder\"
If ($path -eq $true) {
$Output = "True"
}
Else {
$Output = "False"
}
$details = #{
Computer_Name = $computer
Output = $Output
}
$results += New-Object PSObject -Property $details
}
$results | select-object -property Computer_Name, Output | Export-csv c:\results.csv -NoTypeInformation
That said, this pattern is one that should be avoided:
$results = #()
foreach ($item in $set) {
$results += $item
}
$results
The problem is that $results += $item copies the entire array into a new array and then adds the new item. It's a huge overhead as the size of the array increases.
Try something like this instead:
Get-Content "C:\Users\me\Desktop\Notes\Computers.csv" | ForEach-Object {
[PSCustomObject]#{
Computer_Name = $_
Output = Test-Path "\\$_\c$\Program Files\Folder\"
}
} | Export-Csv -Path C:\results.csv -NoTypeInformation

Powershell - an empty pipe element is not allowed

I keep receiving the following error message - "An empty pipe element is not allowed" whenever I try to pipe out my results to a csv file. Any idea why this might be happening?
$apps = Import-CSV apps.csv
$computers = Import-CSV compobj.csv
foreach($computer in $computers) {
$computerLob = $computer.lob
$lobApps = $apps | ? {$_.lob -eq $computerLob}
foreach($app in $lobApps){
$appLocation = $app.location
$installed=Test-Path "\\$computerHostname\$appLocation"
$computerHostname = $computer.hostname
$results = new-object psobject -property #{Computer=$computer.hostname;App=$app.appname;Installed=$installed} | select Computer,App,Installed
$results
}
} | Export-csv "results.csv" -NoTypeInformation
I've tried doing this:
$results | Export-csv "results.csv" -NoTypeInformation
within the foreach loop but it only returns the last record.
A foreach loop doesn't ouput to the pipeline. You can make it do that by making the loop a sub-expression:
$apps = Import-CSV apps.csv
$computers = Import-CSV compobj.csv
$(foreach($computer in $computers) {
$computerLob = $computer.lob
$lobApps = $apps | ? {$_.lob -eq $computerLob}
foreach($app in $lobApps){
$appLocation = $app.location
$installed=Test-Path "\\$computerHostname\$appLocation"
$computerHostname = $computer.hostname
$results = new-object psobject -property #{Computer=$computer.hostname;App=$app.appname;Installed=$installed} | select Computer,App,Installed
$results
}
}) | Export-csv "results.csv" -NoTypeInformation
I believe the problem you are having is around the use of foreach and the the pipeline, you are processing your items in the foreach statement but still expecting them to be on the pipeline.
This is a common error and is explained in more detail in the article Essential PowerShell: Understanding foreach and Essential PowerShell: Understanding foreach (Addendum).
You should be able to achieve what you want like this:
$apps = Import-CSV apps.csv
$computers = Import-CSV compobj.csv
$computers | foreach {
$computer = $_
$computerLob = $computer.lob
$lobApps = $apps | ? {$_.lob -eq $computerLob}
$lobApps | foreach {
$app = $_
$appLocation = $app.location
$installed=Test-Path "\\$computerHostname\$appLocation"
$computerHostname = $computer.hostname
new-object psobject -property #{Computer=$computer.hostname;App=$app.appname;Installed=$installed}
}
} | Export-csv "results.csv" -NoTypeInformation
Some of flow-control expressions do not stream their output.
But they can be assigned to variables, but no output streaming.
You can put your expression output stream in a Subexpression $() and then pipe with another command.
You can't pipe from ANY powershell statements. I'm not sure why.
if .. else
Switch
Do .. while
ForEach
For
While
EDIT: Another way. You can stream from a call operator and a scriptblock (or a function). Put the foreach or any other statement inside it. You won't have to wait until it finishes.
& { foreach ($i in 1..3) { $i } } | measure
Count : 3
Average :
Sum :
Maximum :
Minimum :
StandardDeviation :
Property :

Powershell Software Audit Output -csv format separated columns

My current Powershell script spits out a .txt doc of the computer name, and software that I would like to be able to import into a csv file, with each computer name being a new column.
currently the output looks like this:
PC1=
productname
SoftwareA
SoftwareB
PC2=
productname
SoftwareA
SoftwareB
how can I script this to appropriately sort this data? a straight import to csv will have all of this info in a single column. Is there something I can throw on the foreach loop to have it write to the next column? Or could I have each loop write to it's own .txt, and then grab each .csv and have them import into a new sheet
here's the source code:
$ComputerNames = get-content ".\Computers.txt"
foreach ($Computer in $ComputerNames)
{$arryStandardSoftware = get-content -path ".\StandardSoftware.txt"| Foreach-Object{$_.trim()}
$AuditResult = (Get-WMIObject -namespace "root\cimv2\sms" -class sms_installedsoftware -computername "$computer"|
Select-Object productname|Where-Object{$arryStandardSoftware -notcontains "$($_.productname)"})
echo "$Computer ="$AuditResult | out-file ".\SoftwareAudit.txt" -append}
What you want is an array of arrays.
ComputerA ComputerB ComputerC
SoftwareX SoftwareX SoftwareY
SoftwareY SoftwareZ SoftwareZ
SoftwareZ SoftwareA
SoftwareB
To get this result, you need to compile each array as you loop through the WMI results. Find the length of the longest array and then write out each row.
Here is a brute force approach to doing that:
$ComputerNames = get-content ".\Computers.txt"
$ComputerIndex = 0
$MasterArray = New-Object object[] $ComputerNames.Count
#collect the list in an array of arrays
foreach ($Computer in $ComputerNames) {
$arryStandardSoftware = get-content -path ".\StandardSoftware.txt"| Foreach-Object{$_.trim()}
$AuditResult = (Get-WMIObject -namespace "root\cimv2\sms" -class sms_installedsoftware -computername "$computer"|
Select-Object productname|Where-Object{$arryStandardSoftware -notcontains "$($_.productname)"})
$SoftwareArray = #()
$SoftwareArray += $Computer
$AuditResult | % { $SoftwareArray += $_.productname }
$MasterArray[$ComputerIndex] = $SoftwareArray
$ComputerIndex += 1
}
In the previous loop, an array is built for each computer. The first element is the computer name and the rest of the array is the list of software.
Now find out which of the arrays is the longest.
$longest = 0
for ($i=0;$i -lt $MasterArray.Count; $i++) {
if ($MasterArray[$i].Count -gt $longest){
$longest = $MasterArray[$i].Count
}
}
Once we know the maximum column length, we can iterate through all the arrays, building the rows which will be output to the CSV file.
$MyOutput = $null
for ($i=0;$i -lt $longest; $i++) {
$row = ""
for ($j=0;$j -lt $MasterArray.Count; $j++) {
if ($i -lt $MasterArray[$j].Count){
$row += $MasterArray[$j][$i]
}
if ($j -lt ($MasterArray.Count - 1) ){
$row += "`t"
}
}
$MyOutput += ($row + "`r`n")
}
$MyOutput > 'My.csv'
Like I said, this is a brute force approach, but the requirement to have each computer's software list as a column restricts the output options available.
The power of powershell is objects (PsCustomObject). In order to output each computer as a column, you can construct custom object and add new property to it, using computer name as property name (so long the computer name does not contain spaces or special characters).
The following script should output something like this:
ProductName, PC1, PC2
SoftwareA, true, false
SoftwareB, false, true
Haven't tested it, but you should get the basic idea.
$ComputerNames = get-content ".\Computers.txt"
$arryStandardSoftware = get-content -path ".\StandardSoftware.txt"| Foreach-Object{$_.trim()}
$reports = $arryStandardSoftware | select #{N="ProductName";E={$_}}
foreach ($Computer in $ComputerNames)
{
$installed = Get-WMIObject -namespace "root\cimv2\sms" -class sms_installedsoftware -computername "$computer" | select ProductName
foreach ($r in $reports)
{
Add-Member -InputObject $r -MemberType NoteProperty -Name $Computer -Value ($installed -contains $r.ProductName)
}
}
$reports | export-csv .\SoftwareAudit.txt -NoTypeInformation

Gather info through several foreach and then export-csv at the end of script

I have a script that scans all my servers in my domains and outputs to two separate CSV files - one simple and one extensive.
I write one line at the time to my csv.. This results in thousands of file-open and file-close.. I've lurked around and understand that I should first gather all the info and write it all in one sweep at the end of the script. But how do I do this with export-csv (preferably using a function)?
And is there a way I can use the same function for short and long list?
The script performs numerous tasks on each domain/server, but I've trimmed it down to this for your viewing pleasure:
$domains = "no","se","dk"
# Loop through specified forests
foreach ($domain in $domains) {
# List all servers
$servers = Get-QADComputer
# Looping through the servers
foreach ($server in $servers) {
# GENERATE LONGLIST #
# Ping server
if (Test-Connection -ComputerName $server.name -count 1 -Quiet )
{
$Reachable = "Yes"
# Fetch IP address
$ipaddress = [System.Net.Dns]::GetHostAddresses($Server.name)|select-object IPAddressToString -expandproperty IPAddressToString
# Formatting output and export all info to CSV
New-Object -TypeName PSObject -Property #{
SystemName = ($server.name).ToLower()
Reachable = $Reachable
Domain = $server.domain
IPAddress = $IPAddress
} | Select-Object SystemName,Domain,IPAddress| Export-Csv -Path "longexport.csv" -append
}
else # Can't reach server
{
$reachable = "No"
$IPAddress = "Unknown"
# Formatting output and export all info to CSV
New-Object -TypeName PSObject -Property #{
SystemName = ($server.name).ToLower()
Reachable = $Reachable
Domain = $server.domain
} | Select-Object SystemName,Domain,IPAddress| Export-Csv -Path "shortexport.csv" -append
}
}
}
(and let me just say that I know that I cannot do -append with export-csv, but I am using a function that let's me do this..)
You are exporting the same amount of properties to each file so I'm not sure I understand why one of them is considered long and one short. Anyway, I suggest the following, don't assign all computers to a variable, it can take up a lot of RAM, instead use a streaming way (one object at a time) and use foreach-object. Also, since I find no difference in the files I output to the file at the end of each domain operation (one per domain). And with another twick you could write only once to the file.
$domains = "no","se","dk"
foreach ($domain in $domains) {
Get-QADComputer -Service $domain -SizeLimit 0 | Foreach-Object {
$reachable = Test-Connection -ComputerName $_.Name -count 1 -Quiet
if($reachable)
{
$IPAddress = [System.Net.Dns]::GetHostAddresses($_.Name)|select-object IPAddressToString -expandproperty IPAddressToString
}
else
{
$IPAddress = $null
}
New-Object -TypeName PSObject -Property #{
SystemName = $_.Name.ToLower()
Reachable = $reachable
Domain = $_.Domain
IPAddress = $IPAddress
} | Select-Object SystemName,Domain,IPAddress
} | Export-Csv -Path export.csv -Append
}
You'll need to keep data in memory to prevent multiple file open/closes.
You can do this by adding your data to an array like this:
$myData = #()
$myData += New-Object -TypeName PSObject -Property #{
SystemName = ($server.name).ToLower()
Reachable = $Reachable
Domain = $server.domain
} | Select-Object SystemName,Domain,IPAddress
Then at the end of processing convert the array to CSV using $myData | ConvertTo-CSV | Out-File C:\Data.csv or just $myData | Export-Csv C:\Data.csv.