I have a working script for detecting Log4j but trying to export all the result to CSV.
The current script was able to export but is giving me multiple rows of exact result.
$hostnames = "PC1"
foreach ($hostname in $hostnames){
Write-Host "Starting..."
$List = Invoke-Command -ComputerName $hostname {Get-ChildItem 'C:\' -Recurse -Force -Include *.jar -ea 0 | foreach {select-string "JndiLookup.class" $_} | select -exp Path}
if ($List -ne $null)
{
$List1 = [system.String]::Join("`r`n", $List)
$file_location = "C:\Powershell\log4j-test.csv"
$global:name = #{N="Computer Name";E={$hostname}}
$global:lists = #{N="Affected Location";E={$List1}}
Write-Host "Done.."
$name, $lists | Export-CSV $file_location -noTypeInformation -Force -Append -Encoding UTF8
}
}
You're probably looking for something like this:
[pscustomobject] #{
'Computer Name' = $hostname
'Affected Location' = $List -join "`r`n"
} | Export-CSV $file_location -noTypeInformation -Force -Append -Encoding UTF8
Note that each single CSV row created in each iteration will (potentially) span multiple lines, due to using newlines ("`r`n") as separators in the Affected Location column.
As for what you tried:
Your hashtable literals (e.g. #{N="Computer Name";E={$hostname}}) use the syntax of a calculated property, yet you're not using a cmdlet - typically Select-Object - that is capable of creating calculated properties.
More generally, Export-Csv and ConvertTo-Csv in Windows PowerShell do not meaningfully support hashtables as input objects - you'll need to provide [pscustomobject] instances instead, which you can easily create by casting[1] a hashtable literal to [pscustomobject], as shown above
By contrast, in PowerShell (Core) 7+ you may now use hashtables directly as input, though you'll probably want to use [ordered] hashtables for predictable column ordering.
[1] Strictly speaking, the form [pscustomobject] #{ ... } isn't a cast, but built-in syntactic sugar for creating [pscustomobject] instances as literals. This is evidenced by the fact that the key-definition order in the hashtable literal is maintained in this case, whereas it isn't in stand-alone hashtable literals.
Related
I'm trying to compare 2 files that both contain email addresses. For example text1.csv has 800 addresses and text2.csv has 200. Most of the email addresses in text2.csv are in text1.csv but not all of them. I need to export all the email addresses from text2.csv that are NOT in text1.csv. The property on both files is email
I tried something like this but I dont get the correct outcome:
Compare-Object -ReferenceObject (Get-Content -Path C:\scripts\test1.csv) -DifferenceObject (Get-Content -Path C:\scripts\test2.csv) | Where-Object{ $_.SideIndicator -eq "=>" }
I also tried this without result
$file2 | Where-Object { $_.email -NotIn $file1 }
By using Get-Content, you end up comparing whole lines from your input files.
In order to compare column values from CSV files, you must first convert your input files to objects, using Import-Csv, whose properties (reflecting the column values) you can then compare; in your case, the .email property:
Compare-Object -PassThru (Import-Csv C:\scripts\test1.csv).email `
(Import-Csv C:\scripts\test2.csv).email |
Where-Object{ $_.SideIndicator -eq '=>' }
Note:
Compare-Object's -PassThru switch directly passes the differing objects through, decorated with a .SideIndicator ETS property, instead of wrapping them in a [pscustomobject] wrapper whose .InputObject contains the respective original object.
Even though, with multiple data rows in the CSV, Import-Csv returns an array of objects, you can access .email directly on that array in order to get the property values of all elements in the array, which is a convenient feature known as member-access enumeration.
For brevity, I'm using positional arguments above (e.g., C:\scripts\test1.csv instead of -Path C:\scripts\test1.csv).
I'd probably do something like this:
$emails = (Import-Csv -Path 'X:\file1.csv').email
$result = Import-Csv -Path 'X:\file2.csv' |
Where-Object { $emails -notcontains $_.email }
# output to new file
$result | Export-Csv -Path 'X:\missingEmails.csv' -NoTypeInformation
Data mapping project, in house system to new vendor system. First step is find all the occurrences of current database field names (or column names to be precise) in the C# .cs source files. Trying to use Powershell. Have recently created PS searches with Get-ChildItem and Select-String that work well but the search string array was small and easily hard coded inline. But the application being ported has a couple hundred column names and significant amounts of code. So armed with a text file of all the column names Pipleline would seem like a god tool to create a the basic cross ref for further analysis. However, I was not able to get the Pipeline to work with an external variable anyplace other than first step. Trying using -PipelineVariable, $_. and global variable. Did not find anything specific after lots of searching. P.S. This is my first question to StackoOverflow, be kind please.
Here is what I hoped would work but do dice so far.
$inputFile = "C:\DataColumnsNames.txt"
$outputFile = "C:\DataColumnsUsages.txt"
$arr = [string[]](Get-Content $inputfile)
foreach ($s in $arr) {
Get-ChildItem -Path "C:ProjectFolder\*" -Filter *.cs -Recurse -ErrorAction SilentlyContinue -Force |
Select-String $s | Select-Object Path, LineNumber, line | Export-csv $outputfile
}
Did find that this will print the list one time but not twice. In fact it seems using the variable in this way results in processing simply skipping any further pipeline steps.
foreach ($s in $arr) {Write-Host $s | Write $s}
If it isn't possible to do this in Powershell easily my fallback is to do with C# although would much rather get the level up with PowerShell if anyone can point me to the correct understanding of how to do things in the Pipepline, or alternatively construct an equivalent function. Seems like such a natural fit for Powershell.
Thanks.
You're calling Export-csv $outputfile in a loop, which rewrites the whole file in every iteration, so that only the last iteration's output will end up in the file.
While you could use -Append to iteratively append to the output file, it is worth aking a step back: Select-String can accept an array of patterns, causing a line that matches any of them to be considered a match.
Therefore, your code can be simplified as follows:
$inputFile = 'C:\DataColumnsNames.txt'
$outputFile = 'C:\DataColumnsUsages.txt'
Get-ChildItem C:\ProjectFolder -Filter *.cs -Recurse -Force -ea SilentlyContinue |
Select-String -Pattern (Get-Content $inputFile) |
Select-Object Path, LineNumber, line |
Export-csv $outputfile
-Pattern (Get-Content $inputFile) passes the lines of input file $inputFile as an array of patterns to match.
By default, these lines are interpreted as regexes (regular expressions); to ensure that they're treated as literals, add -SimpleMatch to the Select-String call.
This answer to a follow-up question shows how to include the specific pattern among the multiple ones passed to -Pattern that matched on each line in the output.
I think you want to append each occurrence to the csv file. And you need to get the content of the file. Try this:
$inputFile = "C:\DataColumnsNames.txt"
$outputFile = "C:\DataColumnsUsages.txt"
$arr [string[]](Get-Content $inputfile)
foreach ($s in $arr) {
Get-ChildItem -Path "C:ProjectFolder\*" -Filter *.cs -Recurse -ErrorAction SilentlyContinue -Force | Foreach {
Get-Content "$_.Fullname" | Select-String $s | Select-Object Path, LineNumber, line | Export-csv -Append -Path "$outputfile"
}
}
-Append was not introduced before powershell v3.0 (Windows 8) then try this:
$inputFile = "C:\DataColumnsNames.txt"
$outputFile = "C:\DataColumnsUsages.txt"
$arr [string[]](Get-Content $inputfile)
foreach ($s in $arr) {
Get-ChildItem -Path "C:ProjectFolder\*" -Filter *.cs -Recurse -ErrorAction SilentlyContinue -Force | Foreach {
Get-Content "$_.Fullname" | Select-String $s | Select-Object Path, LineNumber, line | ConvertTo-CSV -NoTypeInformation | Select-Object -Skip 1 | Out-File -Append -Path "$outputfile"
}
}
I have the following powershell script that renames files from one location to another with a sequential filename. Ultimately, these file changes need to be mapped, as in original - new. Currently, I just have a Write-Host cmdlet and I just copy the cmd windows output into a txt file then run through a python script I wrote to spit out the original and renamed files into an excel file. I was wondering if there was an easier way to do this in the initial ps script. Even something tab delimited would be easily copy-pasteable into an excel file.
Set-Location -Path "C:\Users\mmcintyre\Desktop\grail_new"
$destLoc = "C:\Users\mmcintyre\Desktop\renamed"
$countRef = [ref] 0
Get-ChildItem -Filter *.pdf -Recurse |
Copy-Item -WhatIf -Destination { '{0}\{1}.pdf' -f $destLoc,++$countRef.Value }
Any help would be greatly appreciated.
Edit: I am currently using PS 2.0.
The following outputting old-name/new-name pairs to a TSV file in addition to the copy operation (PSv3+ syntax):
$countRef = [ref] 0
Get-ChildItem -Filter *.pdf -Recurse | ForEach-Object {
$newFullName = '{0}\{1}.pdf' -f $destLoc, ++$countRef.Value
Copy-Item -WhatIf -LiteralPath $_.FullName -Destination $newFullName
[pscustomobject] #{
Old = $_.FullName
New = $newFullName
}
} | Export-Csv -Delimiter "`t" NameMappings.tsv
This creates a TSV (tab-separated values) file with columns named Old and New that contain the old and new full filenames, respectively.
PSv2: The [pscustomobject] #{ ... } syntactic sugar for creating custom objects from hashtables is not available in v2, so New-Object must be used:
$countRef = [ref] 0
Get-ChildItem -Filter *.pdf -Recurse | ForEach-Object {
$newFullName = '{0}\{1}.pdf' -f $destLoc, ++$countRef.Value
Copy-Item -WhatIf -LiteralPath $_.FullName -Destination $newFullName
New-Object PSCustomObject -Property #{
Old = $_.FullName
New = $newFullName
}
} | Export-Csv -Delimiter "`t" NameMappings.tsv
Caveat: -Property accepts a hashtable[1]
, which means that its key ordering is not guaranteed, so the ordering of properties of the resulting object will typically not reflect the input order - in this case it just happens to do so.
If the resulting property order is undesired, you have two options:
Slow, but convenient: insert a Select-Object call with the properties in the desired order (e.g., Select-Object Old, New).
More cumbersome: Construct the object empty at first New-Object PSCustomObject, and then attach properties one by one in the desired order with individual Add-Member calls.
[1] The PSv3+ [pscustomobject] #{ ... } syntax is seemingly also hashtable-based, but it is parsed in a way that preserves the key order; i.e., as if you had implicitly used [ordered] #{ ... }.
With help of Powershell I need to find registry key, where Value Displayname like 'Cisco', and get from this key data from Value name 'Uninstallstring'.
I know this sounds somewhat strange, but this application has a different uninstall string on each computer.
So
ForEach-Object -InputObject (Get-ChildItem 'HKLM:\software\microsoft\windows\currentversion\uninstall') {
$single_item = $_ | Get-Item
$single_item_properties = $single_item | Get-ItemProperty | Select-Object -Property DisplayName,UninstallString | Where-Object {($_.DisplayName -like '*Cisco*TSP*')}
$uninstall_str = ($single_item_properties | Select UninstallString)
$str_to_execute=$uninstall_str.UninstallString -replace '" .*','"'
$str_to_execute
Start-Process -FilePath $str_to_execute -ArgumentList '-s','-f1"\\sandbox\Common.Installs\Utils\un.iss"' -Wait -PassThru
}
This script gives us the error
UninstallString
"C:\Program Files (x86)\InstallShield Installation Information{01A05F96-E34D-4308-965C-65DCA4AF114D}\setup.exe"
Start-Process : This command cannot be executed due to the error: The system cannot find the file specified.
The problem is that the result is in not String type.
And I can't convert it into String.
There are several issues here.
Although ForEach-Object does have the -InputObject parameter, it's primarily designed to take pipeline input. As the documentation says,
When you use the InputObject parameter with ForEach-Object, instead of piping command results to ForEach-Object, the InputObject value—even if the value is a collection that is the result of a command, such as –InputObject (Get-Process)—is treated as a single object. Because InputObject cannot return individual properties from an array or collection of objects, it is recommended that if you use ForEach-Object to perform operations on a collection of objects for those objects that have specific values in defined properties, you use ForEach-Object in the pipeline
In your code, the scriptblock gets executed only once, and $single_item is actually an array containing the entire output of Get-ChildItem. In order to iterate over the results one element at a time, you need to either pipe the output to ForEach-Object...
Get-ChildItem 'HKLM:\software\microsoft\...' | ForEach-Object {
...or better yet, use the foreach control structure, which generally runs faster (though it uses more memory):
foreach ($single_item in (Get-ChildItem 'HKLM:\software\microsoft\...')) {
Piping each element to Get-Item is superfluous. You can pipe directly to Get-ItemProperty.
You don't need to select a property in order to filter on it with Where-Object. So, it's superfluous to use Select-Object -Property DisplayName,UninstallString, which creates a PSCustomObject with two properties, and then retrieve the UninstallString property of that PSCustomObject later in the code. Just filter with Where-Object first, then get the value of UninstallString with | Select -ExpandProperty UninstallString.
(See this answer for an explanation of the reason for using the -ExpandPropery switch.)
Using -ExpandProperty, you'll get errors if any keys are returned that do not have an UninstallString property, so you might want to add -ErrorAction SilentlyContinue to your Select-Object statement.
Putting it all together:
foreach ($single_item in (Get-ChildItem 'HKLM:\software\microsoft\windows\currentversion\uninstall')) {
$uninstall_str = $single_item `
| Get-ItemProperty `
| ?{$_.DisplayName -like '*Cisco*TSP*'} `
| Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue
$str_to_execute = $uninstall_str -replace '" .*','"'
Start-Process -FilePath $str_to_execute -ArgumentList '-s','-f1"\\sandbox\Common.Installs\Utils\un.iss"' -Wait -PassThru }
}
If there's guaranteed to be no more than one matching item, you can do it more compactly like this:
$str_to_execute = (
Get-ChildItem 'HKLM:\software\microsoft\windows\currentversion\uninstall' `
| Get-ItemProperty `
| ?{$_.DisplayName -like 'Microsoft*'}
).uninstallstring $uninstall_str -replace '" .*','"'
The more compact version will actually work even if there are multiple matching keys in PowerShell v3+, but in v2 it would return null if there's more than one.
BTW, ? is an abbreviation for Where-Object. Similarly, % is an abbreviation for ForEach-Object (only if you use it as a filter in a pipeline, but that's how you should be using it anyway.)
The output you show does not seem to match the code you provided. When I execute your code I get the uninstallstring based on my own search. When I first saw the output I thought "Oh... he forgot that he was passing an object with a property UninstallString to Start-Process". But when I looked at your code I could see that you accounted for that by doing $uninstall_str.UninstallString. So I am not sure what is exactly wrong but one improvement would be to use -ExpandProperty of Select-Object
$uninstall_str = $single_item_properties | Select -ExpandProperty UninstallString
$str_to_execute=$uninstall_str -replace '" .*','"'
But we can make a one liner if you prefer.
$str_to_execute = ($single_item_properties).UninstallString -replace '" .*','"'
I have a few lines of powershell code that looks in a remote directory
Get-ChildItem "\\box_lab001\f$\output files" -force |
Where-Object {!$_.PsIsContainer -AND $_.lastWriteTime -lt (Get-Date).AddMinutes(-5) } |
Select-Object LastWriteTime,#{n="Path";e={convert-path $_.PSPath}} |
Tee-Object "\\\box_lab001\c$\Users\john\Documents\output files_root.txt" |
Remove-Item -force
What I'm looking to do is make this scalable across multiple boxes where if a user sees an issue on box_lab01, trough 10. Then he can run the script with a switch that will ask for input. It would then run the command separately, replacing box_lab### each time, possible?
C:\powershell.ps1 -input
what boxes are having the issue? use three digit numbers only, comma separated
Yes.
You can use Read-Host to prompt for input. You can use param(...) to add parameters to a script:
param($input = $null)
if ($input) {
$foo = Read-Host -Prompt $input
}
You can then get the individual numbers by using -split:
$numbers = $foo -split ','
Loop over them:
$numbers | ForEach-Object {
...
}
You can use $_ within the block to refer to the current number.
You want to add a parameter that takes an array of values as input. You can then use these to check each machine:
[CmdletBinding()]
param(
[int[]]
# The numbers of the machines whose output files should be removed.
$MachineNumbers
)
$MachineNumbers | ForEach-Object {
$machineRoot = '\\box_lab{0:d3}' -f $_
Get-ChildItem ('{0}\f$\output files' -f $machineRoot) -force |
Where-Object {!$_.PsIsContainer -AND $_.lastWriteTime -lt (Get-Date).AddMinutes(-5) } |
Select-Object LastWriteTime,#{n="Path";e={convert-path $_.PSPath}} |
Tee-Object ('{0}\c$\Users\john\Documents\output files_root.txt' -f $machineRoot) |
Remove-Item -force
The code ('\\box_lab{{0:d3}}' -f $_) converts each number passed from the user into a zero-padded, three character string (which appears to be your compuer naming schem). You would then call your script like this:
Remove-OutputFiles -MachineNumbers (1..10)
Remove-OutputFiles -MachineNumbers 1,2,3,4,5
You could give the MachineNumbers parameter a reasonable default, so that if no parameters are passed, it hits a default set of machines.
I would also uae the [CmdletBinding()] attribute to your script so you can pass -WhatIf to your script and see what files will be deleted without actually deleting them:
Remove-OutputFiles -MachineNumbers (1..3) -WhatIf