I want a script that can help me check for the name of keyset (column a) in Sample.cvs and then replace the current command(column b) with new command (column c) in the Source text file.
CSV file: Sample.csv
A. | B. | C.
Manock | 2B | 2ab
Sterling | 3F | 3sf
Source file text: Source.txt
keyset "Manock"
(
key("SELECT")
command ("display/app=%disapp% "2B")
);
So desired output:
keyset "Manock"
(
key("SELECT")
command ("display/app=%disapp% "2ab")
);
Powershell Script:
New-Item -Path "C:\Users\e076200\Desktop\ks_update\source.txt" -ItemType File -Force
$data = Get-Content C:\Users\e076200\Desktop\ks_update\source.ddl
Add-Content -Value $data -Path "C:\Users\e076200\Desktop\ks_update\source.txt"
$foundline = $false
$a = 0
$Etxt = foreach($line in Get-Content C:\Users\e076200\Desktop\ks_update\source.txt)
{
if ($line -match 'keyset "Manock"' )
{
$a = 0
$foundline = $true
}
$a= $a + 1
if($line -match "display/app" -and $a -eq 5 -and $foundline -eq $true)
{
$line = $line.replace('2b' , '2ab')
$line
}
else
{
$line
}
}
$Etxt | Set-Content C:\Users\e076200\Desktop\ks_update\source.txt -Force
$users = Import-CSV -Path:\Users\e076200\Desktop\ks_update\sample.csv
I've figured out how to find and replace one line in the file directly. I've also figured out how to import the csv. I need help on how to make the logic parameterized and use column A of CSV as the match piece and column c as the replacement piece.
Script Explanation.
New-Item -Path "C:\Users\e076200\Desktop\ks_update\source.txt" -ItemType File -Force
New-Item creates new text file # location defined by -Path using name specified at the end, source.
ItemType to define type of document, -Force is force command.
$data = Get-Content C:\Users\e076200\Desktop\ks_update\source.ddl
Retrieves ddl and stores in variable.
Add-Content -Value $data -Path "C:\Users\e076200\Desktop\ks_update\source.txt"
Transfers content from variable to new text file created.
$foundline = $false
conditional variable defined for when keyset identifier is found.
$a = 0
counter defined for if statement.
$Etxt = foreach($line in Get-Content C:\Users\e076200\Desktop\ks_update\source.txt)
$Etxt - for loop
$line - variable for each line in txt
{
if ($line -match 'keyset "Manock"' )
{
$a = 0
$foundline = $true
}
If keyset identifier is found, set counter to 0 and set conditional variable to true
$a= $a + 1
if($line -match "display/app" -and $a -eq 5 -and $foundline -eq $true)
{
$line = $line.replace('2b' , '2ab')
$line
Match found, PS runs logic, line with keyset identifier == 0 + 1....up until line = 5 where we find item to be replaced.
For redundancy, line reader set to check for line identifier, ("display/app") on expected line.
If Redundant check if met and counter is 5 then we replace word with the line.replace function.
Overwritten data is returned in $line
}
else
{
$line
}
Else retain line
}
$Etxt | Set-Content C:\Users\e076200\Desktop\ks_update\source.txt -Force
Updated text file
$users = Import-CSV -Path:\Users\e076200\Desktop\ks_update\sample.csv
Imports Reference csv file
Please make explanation as dumbed down as possible. Thank you.
I've been working on a little side project of listing files compressed in nested zip files.
I've cooked up a script that does just that, but only if the depth of zip files is known.
In in example below the zip file has additional zips in it and then anthoer in one of them.
Add-Type -AssemblyName System.IO.Compression.Filesystem
$path = "PATH"
$CSV_Path = "CSV_PATH"
$zipFile = Get-ChildItem $path -recurse -Filter "*.zip"
$rootArchive = [System.IO.Compression.zipfile]::OpenRead($zipFile.fullname)
$rootArchive.Entries | Select #{l = 'Source Zip'; e = {} }, #{l = "FullName"; e = { $_.FullName.Substring(0, $rootArchive.Fullname.Lastindexof('\')) } }, Name | Export-csv $CSV_Path -notypeinformation
$archivesLevel2 = $rootArchive.Entries | Where { $_.Name -like "*.zip" }
foreach ($archive in $archivesLevel2)
{
(New-object System.IO.Compression.ZipArchive ($archive.Open())).Entries | Select #{l = 'Source Zip'; e = { $archive.name } }, #{l = "FullName"; e = { $archive.FullName.Substring(0, $_.Fullname.Lastindexof('\')) } }, Name | Export-Csv $CSV_Path -NoTypeInformation -append;
New-object System.IO.Compression.ZipArchive($archive.Open()) -OutVariable +lastArchiveLevel2
}
$archivesLevel3 = $lastArchiveLevel2.entries | Where { $_.Name -like "*.zip" }
foreach ($archive in $archivesLevel3)
{
(New-Object System.IO.Compression.ZipArchive ($archive.Open())).Entries | Select #{l = 'Source Zip'; e = { $archive.name } }, #{l = "FullName"; e = { $archive.FullName.Substring(0, $_.Fullname.Lastindexof('\')) } }, Name | Export-Csv $CSV_Path -NoTypeInformation -append
}
What I ask of you is to help me modify this to accomodate an unknown depth of inner zip files. Is that even possible?
Here's an example on how to do it using a Queue object, which allow you to recursively go through all depths of your zip file in one go.
As requested, here are some comments to explain what is going on.
Add-Type -AssemblyName System.IO.Compression.Filesystem
$path = "PATH"
$CSV_Path = "CSV_PATH"
$Queue = [System.Collections.Queue]::New()
$zipFiles = Get-ChildItem $path -recurse -Filter "*.zip"
# All records will be stored here
$Output = [System.Collections.Generic.List[PSObject]]::new()
# Main logic. Used when looking at the root zip and any zip entries.
# ScriptBlock is used to prevent code duplication.
$ProcessEntries = {
Param($Entries)
$Entries | % {
# Put all zip in the queue for future processing
if ([System.IO.Path]::GetExtension($entry) -eq '.zip') { $Queue.Enqueue($_) }
# Add a Source Zip property with the parent zip since we want this informations in the csv export and it is not available otherwise.
$_ | Add-Member -MemberType NoteProperty -Name 'Source Zip' -Value $zip.name
# Every entries, zip or not, need to be part of the output
$output.Add($_)
}
}
# Your initial Get-ChildItem to find zip file implicate there could be multiple root zip files, so a loop is required.
Foreach ($zip in $zipFiles) {
$archive = [System.IO.Compression.zipfile]::OpenRead($zip.fullname)
# The $ProcessEntries scriptblock is invoked to fill the Queue and the output.
. $ProcessEntries $archive.Entries
# Should the Zip file have no zip entries, this loop will never be entered.
# Otherwise, the loop will continue as long as zip entries are detected while processing any child zip.
while ($Queue.Count -gt 0) {
# Removing item from the queue to avoid reprocessing it again.
$Item = $Queue.Dequeue()
$archive = New-object System.IO.Compression.ZipArchive ($Item.open())
# We call the main scriptblock again to fill the queue and the output.
. $ProcessEntries $archive.Entries
}
}
$Output | Select 'Source Zip', FullName, Name | Export-Csv $CSV_Path -NoTypeInformation
References
Queue
Here you have a little example of how recursion would look like, basically, you loop over the .Entries property of ZipFile and check if the extension of each item is .zip, if it is, then you pass that entry to your function.
EDIT: Un-deleting this answer mainly to show how this could be approached using a recursive function, my previous answer was inaccurate. I was using [ZipFile]::OpenRead(..) to read the nested .zip files which seemed to work correctly on Linux (.NET Core) however it clearly does not work when using Windows PowerShell. The correct approach would be to use [ZipArchive]::new($nestedZip.Open()) as Sage Pourpre's helpful answer shows.
using namespace System.IO
using namespace System.IO.Compression
function Get-ZipFile {
[cmdletbinding()]
param(
[parameter(ValueFromPipeline)]
[object]$Path,
[parameter(DontShow)]
[int]$Nesting = -1
)
begin { $Nesting++ }
process {
try
{
$zip = if(-not $Nesting) {
[ZipFile]::OpenRead($Path)
}
else {
[ZipArchive]::new($Path.Open())
}
foreach($entry in $zip.Entries) {
[pscustomobject]#{
Nesting = $Nesting
Parent = $Path.Name
Contents = $entry.FullName
}
if([Path]::GetExtension($entry) -eq '.zip') {
Get-ZipFile -Path $entry -Nesting $Nesting
}
}
}
catch
{
$PSCmdlet.WriteError($_)
}
finally
{
if($null -ne $zip) {
$zip.Dispose()
}
}
}
}
Get-ChildItem *.zip | Get-ZipFile
I am not a scripter, please if anyone can help me with modify below script by removing UCPID value but keep only servername
Currently below script looking two columns from csv file, now I want to change the behavior to only look for ServerName because now CSV file have only one column which containing server only in each row and update related XML.
$data = Import-Csv .\MyFile.csv
$luTable = #{}
# Create Keys in Lookup Table
$data | % {
if (!$luTable.ContainsKey("$($_.ServerName)")) { $luTable["$($_.UCPID)"] = New-Object System.Collections.ArrayList }
}
$luTable.Keys | % {
$key = $_ # Store Key
$data | where UCPID -Match $_ | select ServerName | % {
$luTable[$key].Add($_.ServerName)
}
}
# Build XML Files
$luTable.Keys | % {
$key = $_
$filetext = gc ".\MyXML.xml"
$filetext = $filetext.Replace("#Title#", $key)
$targets = ""
$luTable[$key] | % {
$targets += "<ComputerName>$($_)</ComputerName>"
}
$filetext = $filetext.Replace("#computername#", $targets)
sc -Path ".\$($key).xml" -Value $filetext
}
I tried deleting below code but its not helping.
# Create Keys in Lookup Table
$data | % {
if (!$luTable.ContainsKey("$($_.ServerName)")) { $luTable["$($_.UCPID)"] = New-Object System.Collections.ArrayList }
}
//CSV file content
ServerName
Server1
Server2
Server3
Server4
Server5
//XML - location where I want server to be copied
<AnnounceOffer>false</AnnounceOffer>
<OfferCategory>false</OfferCategory>
<OfferDescriptionHTML>false</OfferDescriptionHTML>
</SettingsLocks>
<IsUrgent>false</IsUrgent>
<Target>
#computername#
</Target>
</SingleAction>
</BES>
#computername# must be replaced with below-
<ComputerName>Server1</ComputerName>
<ComputerName>Server2</ComputerName>
<ComputerName>Server3</ComputerName>
<ComputerName>Server4</ComputerName>
If your XML looks like this:
<BES>
<SingleAction>
<SettingsLocks>
<AnnounceOffer>false</AnnounceOffer>
<OfferCategory>false</OfferCategory>
<OfferDescriptionHTML>false</OfferDescriptionHTML>
</SettingsLocks>
<IsUrgent>false</IsUrgent>
<Target>
#computername#
</Target>
</SingleAction>
</BES>
Then here's two alternatives for you:
Method 1: use XML functionality of PowerShell
# load the xml from file
$xml= New-Object System.XML.XMLDocument
$xml.Load("D:\Test\MyXML.xml")
# select the node with the #computername# placeholder inside
$targetNode = $xml.SelectSingleNode('//BES/SingleAction/Target')
$targetNode.'#text' = '' # remove the placeholder text
# read the servernames from file and create and insert new nodes for each of them
(Import-Csv -Path 'D:\Test\AllServers.csv').ServerName |
ForEach-Object {
$newNode = $xml.CreateElement('ComputerName')
$newNode.InnerText = $_
$targetNode.AppendChild($newNode)
}
# save the updated XML
$xml.Save('D:\Test\NewXml.xml')
Method 2: treat the xml as plain text and do a simple textual -replace on it
# read the XML as multiline text
$xml = Get-Content -Path 'D:\Test\MyXML.xml' -Raw
# find the line where the #computername# placeholder is and get the indentation value
$indent = ' ' * ($xml | Select-String -Pattern '(?m)^(\s+)#computername#').Matches[0].Groups[1].Length
# read the servernames from file and construct a multiline string
$servers = ((Import-Csv -Path 'D:\Test\AllServers.csv').ServerName |
ForEach-Object { "$indent<ComputerName>$_</ComputerName>" }) -join [environment]::NewLine
# now replace in the xml and write to (new) file
$xml -replace "(?m)^$indent#computername#", $servers | Set-Content -Path 'D:\Test\NewXml.xml' -Encoding UTF8
Result in both cases:
<BES>
<SingleAction>
<SettingsLocks>
<AnnounceOffer>false</AnnounceOffer>
<OfferCategory>false</OfferCategory>
<OfferDescriptionHTML>false</OfferDescriptionHTML>
</SettingsLocks>
<IsUrgent>false</IsUrgent>
<Target>
<ComputerName>Server1</ComputerName>
<ComputerName>Server2</ComputerName>
<ComputerName>Server3</ComputerName>
<ComputerName>Server4</ComputerName>
<ComputerName>Server5</ComputerName>
</Target>
</SingleAction>
</BES>
I have the following code
function ping-test($hosts) {
$conn = [System.Collections.ArrayList]#($hosts)
[int]$hostsamount = $conn.Count
foreach($co in $conn)
{
$check = Test-Connection $co -Count 3 -ErrorAction SilentlyContinue
$zugriffzeit = $check | select ResponseTime | Measure-Object ResponseTime -Average
$avg = [system.math]::Round($zugriffzeit.Average)
$zeit = Get-Date -Format HH:mm:ss
if($check -eq $null)
{
$pcre = Write-Output $co
$pire = Write-Output 'False'
$zure = $null
}
else
{
$pcre = Write-Output $co
$pire = Write-Output 'True'
$zure = Write-Output "$avg ms"
$zure = $zure.Replace(' ','')
}
[void]$re.Add([PSCustomObject] #{PCName=$pcre; PingResult=$pire; Zugriffszeit=$zure; Zeit=$zeit} )
**$log = "Host:{0} Ping: {1} Zugriffszeit: {2} Zeit: {3}" -f $pcre, $pire, $zure, $zeit
$log >> $logpath**
[int]$recount = $re.Count
[int]$eff = $recount - $hostsamount
try {
$re.RemoveRange(0, $eff)
}
catch{
Write-Host $Error
}
}
return $re
}
I use the following code(is in that function)
$log = "Host:{0} Ping: {1} Zugriffszeit: {2} Zeit: {3}" -f $pcre, $pire, $zure, $zeit
$log >> $logpath
the Question is: I want to form a table with the Colums "Host", "Ping", "Zugriffszeit", and "Zeit".
How can I form this table and save as a .txt or .log file somewhere??
Thx for the help
Use the same data as you're outputting!
To export to csv (if you want to re-use the data programmatically later):
$re |Export-Csv $logpath -NoTypeInformation
If you want to ever format it in a nice table again, it's as easy as:
Import-Csv $logpath |Format-Table
If you simply want nicely tabulated output in your log file:
$re |Format-Table |Out-String |Out-File $logfile
#MathiasR.Jessen showed import and export to csv.
But if you are bound to use .txt or .log files (As the aspect of your question says) then use PSCustomObject and Out-File
[PSCustomObject]#{
Host = $pcre
Ping = $pire
Zugriffszeit = $zure
Zeit = $zeit
} | Out-File $logpath
Later import it like:
Get-Content $logpath
I have two csv files. They both have SamAccountName in common. User records may or may not have a match found for every record between both files (THIS IS VERY IMPORTANT TO NOTE).
I am trying to basically just merge all columns (and their values) into one file (based from the SamAccountNames found in the first file...).
If the SamAccountName is not found in the 2nd file, it should add all null values for that user record in the merged file (since the record was found in the first file).
If the SamAccountName is found in the 2nd file, but not in the first, it should ignore merging that record.
Number of columns in each file may vary (5, 10, 2, so forth...).
Function MergeTwoCsvFiles
{
Param ([String]$baseFile, [String]$fileToBeMerged, [String]$columnTitleLineInFileToBeMerged)
$baseFileCsvContents = Import-Csv $baseFile
$fileToBeMergedCsvContents = Import-Csv $fileToBeMerged
$baseFileContents = Get-Content $baseFile
$baseFileContents[0] += "," + $columnTitleLineInFileToBeMerged
$baseFileCsvContents | ForEach-Object {
$matchFound = $False
$baseSameAccountName = $_.SamAccountName
[String]$mergedLineInFile = $_
[String]$lineMatchFound = $fileToBeMergedCsvContents | Where-Object {$_.SamAccountName -eq $baseSameAccountName}
Write-Host '$mergedLineInFile =' $mergedLineInFile
Write-Host '$lineMatchFound =' $lineMatchFound
Exit
}
}
The problem is, the record in the file is being written as a hash table instead of a string like line (if you were to view it as .txt). So I'm not really sure how to do this...
Adding results csv example files...
First CSV File
"SamAccountName","sn","GivenName"
"PBrain","Pinky","Brain"
"JSteward","John","Steward"
"JDoe","John","Doe"
"SDoo","Scooby","Doo"
Second CSV File
"SamAccountName","employeeNumber","userAccountControl","mail"
"KYasunori","678213","546","KYasunori#mystuff.com"
"JSteward","43518790","512","JSteward#mystuff.com"
"JKibogabi","24356","546","JKibogabi#mystuff.com"
"JDoe","902187u4","1114624","JDoe#mystuff.com"
"CStrife","54627","512","CStrife#mystuff.com"
Expected Merged CSV File
"SamAccountName","sn","GivenName","employeeNumber","userAccountControl","mail"
"PBrain","Pinky","Brain","","",""
"JSteward","John","Steward","43518790","512","JSteward#mystuff.com"
"JDoe","John","Doe","902187u4","1114624","JDoe#mystuff.com"
"SDoo","Scooby","Doo","","",""
Note: This will be part of a loop process in merging multiple files, so I would like to avoid hardcoding the title names (with $_.SamAccountName as an exception)
Trying suggestion from "restless 1987" (Not Working)
$baseFileCsvContents = Import-Csv 'D:\Scripts\Powershell\Tests\base.csv'
$fileToBeMergedCsvContents = Import-Csv 'D:\Scripts\Powershell\Tests\lookup.csv'
$resultsFile = 'D:\Scripts\Powershell\Tests\MergedResults.csv'
$resultsFileContents = #()
$baseFileContents = Get-Content 'D:\Scripts\Powershell\Tests\base.csv'
$recordsMatched = compare-object $baseFileCsvContents $fileToBeMergedCsvContents -Property SamAccountName
switch ($recordsMatched)
{
'<=' {}
'=>' {}
'==' {$resultsFileContents += $_}
}
$resultsFileCsv = $resultsFileContents | ConvertTo-Csv
$resultsFileCsv | Export-Csv $resultsFile -NoTypeInformation -Force
Output gives a blank file :(
The code below outputs the desired results based on the inputs you provided.
function CombineSkip1($s1, $s2){
$s3 = $s1 -split ','
$s2 -split ',' | select -Skip 1 | % {$s3 += $_}
$s4 = $s3 -join ', '
$s4
}
Write-Output "------Combine files------"
# content
$c1 = Get-Content D:\junk\test1.csv
$c2 = Get-Content D:\junk\test2.csv
# users in both files, could be a better way to do this
$t1 = $c1 | ConvertFrom-Csv
$t2 = $c2 | ConvertFrom-Csv
$users = $t1 | Select SamAccountName
# generate final, combined output
$combined = #()
$combined += CombineSkip1 $c1[0] $c2[0]
$c2PropCount = ($c2[0] -split ',').Count - 1
$filler = (', ""' * $c2PropCount)
for ($i = 1; $i -lt $c1.Count; $i++){
$user = $c1[$i].Split(',')[0]
$u2 = $c2 | where {([string]$_).StartsWith($user)}
if ($u2)
{
$combined += CombineSkip1 $c1[$i] $u2
}
else
{
$combined += ($c1[$i] + $filler)
}
}
# write to output and file
Write-Output $combined
$combined | Set-Content -Path D:\junk\test3.csv -Force
You can use compare-object for that purpose. Use -property samaccountname with it. For example:
$a = 1,2,3,4,5
$b = 4,5,6,7
$side = compare-object $a $b
switch ($side){
'<=' {is not in $a}
'=>' {is not in $b}
'==' { is on both sides}
}
When you have all the data in your output-variable, trow it at convertto-csv and write it in a file
After an entire day, I finally came up with something that works...
...
Edit
Reason: breaking the inner loop and removing the found element from the array will be much faster when merging files with thousands of records...
Function GetTitlesFromFileToBeMerged
{
Param ($csvFile)
[String]$fileToBeMergedTitles = Get-Content $fileToBeMerged -TotalCount 1
[String[]]$fileToBeMergedTitles = ($fileToBeMergedTitles -replace "`",`"", "|").Trim()
[String[]]$fileToBeMergedTitles = ($fileToBeMergedTitles -replace "`"", "").Trim()
[String[]]$fileToBeMergedTitles = ($fileToBeMergedTitles -replace "SamAccountName", "").Trim()
[String[]]$listOfColumnTitles = $fileToBeMergedTitles.Split('|',[System.StringSplitOptions]::RemoveEmptyEntries)
Write-Output $listOfColumnTitles
}
$baseFile = 'D:\Scripts\Powershell\Tests\base.csv'
$fileToBeMerged = 'D:\Scripts\Powershell\Tests\lookup.csv'
$baseFileCsvContents = Import-Csv $baseFile
$baseFileContents = Get-Content $baseFile
$fileToBeMergedCsvContents = Import-Csv $fileToBeMerged
[System.Collections.Generic.List[System.Object]]$fileToBeMergedContents = Get-Content $fileToBeMerged
$resultsFile = 'D:\Scripts\Powershell\Tests\MergedResults.csv'
$resultsFileContents = #()
[String]$baseFileTitles = $baseFileContents[0]
[String]$fileToBeMergedTitles = (Get-Content $fileToBeMerged -TotalCount 1) -replace "`"SamAccountName`",", ""
$resultsFileContents += $baseFileTitles + "," + $fileToBeMergedTitles
[String]$lineMatchNotFound = ""
$arrayFileToBeMergedTitles = GetTitlesFromFileToBeMerged $fileToBeMerged
For ($valueNum = 0; $valueNum -lt $arrayFileToBeMergedTitles.Length; $valueNum++)
{
$lineMatchNotFound += ",`"`""
}
$baseLineCounter = 1
$baseFileCsvContents | ForEach-Object {
$baseSameAccountName = $_.SamAccountName
[String]$baseLineInFile = $baseFileContents[$baseLineCounter]
$lineMatchCounter = 1
$lineMatchFound = ""
:inner
ForEach ($line in $fileToBeMergedContents) {
If ($line -like "*$baseSameAccountName*") {
[String]$lineMatchFound = "," + ($line -replace '^"[^"]*",', "")
$fileToBeMergedContents.RemoveAt($lineMatchCounter)
break inner
}; $lineMatchCounter++
}
If (!($lineMatchFound))
{
[String]$lineMatchFound = $lineMatchNotFound
}
$mergedLine = $baseLineInFile + $lineMatchFound
$resultsFileContents += $mergedLine
$baseLineCounter++
}
ForEach ($line in $resultsFileContents)
{
Write-Host $line
}
$resultsFileContents | Set-Content $resultsFile -Force
I'm very sure this is not the best approach and there is something better that would handle this much faster. If anyone has any ideas, I'm open to them. Thanks.