For-each file replace string based on a CSV Dictionary (Powershell)? - powershell

Working to configure a conversion script where replacements are done in files based on what is in the CSV file. Like a dictionary or lookup, rather than directly storing in the code.
i.e.
File1.txt
Jim
Tim
Jan
Greg
Mark
CSV File
DEV,UAT,PROD
John,Jimothy,Timothy
Jimothy,Frank,Karen
Jim,Max,Lisa
So if converting DEV > UAT file1.txt would replace Jim with Max:
Max
Tim
Jan
Greg
Mark
Below is where I'm at currently convertReferences.ps1
#Declare Function Library
. $PSScriptRoot\functionLibrary.ps1
#Get Variables
$global:Dictionary = Import-Csv -Path $PSScriptRoot\IDDictionary.csv
#Input Location
$InputLocation = Read-Host 'Enter Input Location' ['DEV/UAT']
If(!(test-path $PSScriptRoot\$InputLocation))
{
New-Item -ItemType Directory -Force -Path $PSScriptRoot\$InputLocation
}
#Get Output Location
$OutLocation = Read-Host 'Enter an Output Location' ['UAT/PROD']
If(!(test-path $PSScriptRoot\$OutLocation))
{
New-Item -ItemType Directory -Force -Path $PSScriptRoot\$OutLocation
}
#Call Function Convert-DEV
if ($InputLocation -eq 'DEV'){
$Files | convert-DEV -InputLocation $InputLocation -OutLocation $OutLocation}
else {Write-host "NO VALID INPUT DECLARED - PLEASE RUN AGAIN" -ForegroundColor RED
<# Action when all if and elseif conditions are false #>
}
The Function itself is below:
function convert-DEV {
[cmdletbinding()]
param(
#Input Path
[Parameter(Mandatory)]
[ValidateSet('DEV')]
[string]
$InputLocation,
#Files
[Parameter(Mandatory, ValueFromPipeline)]
[string]
$Files,
#Output Path
[parameter()]
[ValidateSet('UAT')]
[string]
$OutLocation
)
process{
Write-host "Replacing Variables in: " $Files -ForegroundColor Blue
$Staging = $Files
(Get-Content $Files | foreach {$_ -replace $Global:Dictionary.DEV , $Global:Dictionary.UAT}) |
Set-Content $Files
(Get-Content $Staging | foreach {Copy-Item -Path $Staging -Destination $PSScriptRoot\$OutLocation})
Write-host "UPDATED File has been copied to: " -ForegroundColor Red $PSScriptRoot\$OutLocation `n `n
}
}
Any thoughts on how to reach my desired output?

You can use this function to get you started, this is assuming the CSV and the File are placed in the same location:
$csv = Import-Csv .\ReferenceTable.csv
function Replace {
[cmdletbinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string] $File,
[Parameter(Mandatory)]
[ValidateSet('DEV', 'UAT', 'PROD')]
[string] $InputLocation,
[Parameter(Mandatory)]
[ValidateSet('DEV', 'UAT', 'PROD')]
[string] $OutLocation,
[Parameter(Mandatory)]
[object[]] $ReferenceTable
)
begin {
$map = #{}
foreach($i in $ReferenceTable) {
$map[$i.$InputLocation] = $i.$OutLocation
}
}
process {
foreach($line in (Get-Content $File).Trim()) {
if($map.ContainsKey($line)) {
$map[$line]
continue
}
$line
}
}
}
Get-ChildItem .\File1.txt | Replace -InputLocation DEV -OutLocation UAT -ReferenceTable $csv

After some further work and the help from Santiago above I have the below which works as I wanted:
function Start-Replace {
[cmdletbinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string] $File,
[Parameter(Mandatory)]
[ValidateSet('DEV','UAT','PROD')]
[string] $InputLocation,
[Parameter(Mandatory)]
[ValidateSet('DEV', 'UAT', 'PROD')]
[string] $OutLocation,
[Parameter(Mandatory)]
[object[]] $ReferenceTable
)
begin {
#Create Map Hashtable/array
$map = #{}
#For each row in Reference list select the value in the input column and add to map array. After match and add output column value to array
foreach($row in $ReferenceTable)
{
$map[$row.$InputLocation] = $row.$OutLocation
}
}
#Replace
process {
$outname = split-path $file -leaf
$input = Get-Content $file
Foreach ($key in $map.Keys) {
$input = $input.Replace($key, $map.$key)
}
Set-Content -Path $PSScriptRoot\$OutLocation\$outlocation-$outname -value $input
write-host $outlocation'-'$outname
}
}

Related

Output list of files to log

I'm writing a script where I'm trying to output the results from a Get-ChildItem command to a log file. The script below is simplified to show the issue I'm having. For example, the WriteLog function is used several times in the actual Script. The file listing is not the only thing to be added to the log file.
The snippet below writes a long run-on line of all full filenames to the log.
$FilePath = "G:\Folder"
$LogPathName = "G:\Folder\TestLog.log"
Function WriteLog {
Param ([string]$LogString)
$Stamp = Get-Date
$LogMessage = "$Stamp - $LogString"
Add-Content $LogPathName -value $LogMessage
}
$FileList = Get-ChildItem –Path $FilePath -include ('*.csv', '*.xlsx')
writelog $FileList
I want each filename to begin on a new line--like a list. How can I do this?
Leaving your function WriteLog as is, the workaround is to iterate over each element of the array returned by Get-ChildItem so that the function appends to the file line-by-line:
foreach($item in $FileList) {
WriteLog $item
}
However a more elegant way of approaching this would be to leverage ValueFromPipeline, then you could simply pipe Get-ChildItem into your function and let the process block handle each element. You can also add a -PassThru switch to it in case you also want the same object be returned as output for later manipulation. And lastly, it may be worth adding a new -Path parameter to make it reusable.
function Write-Log {
param(
[Parameter(ValueFromPipeline, Mandatory)]
[object] $InputObject,
[parameter(Mandatory)]
[string] $Path,
[parameter()]
[switch] $PassThru
)
begin { $sb = [System.Text.StringBuilder]::new() }
process {
$sb = $sb.AppendLine("$(Get-Date) - $InputObject")
if($PassThru.IsPresent) { $InputObject }
}
end { Add-Content $Path -Value $sb.ToString() -NoNewline }
}
$FileList = Get-ChildItem -Path .\* -Include '*.csv', '*.xlsx' |
Write-Log -Path "G:\Folder\TestLog.log" -PassThru

Comparing my folders with their back up folders to see if my backup solution is working

Please bare in mind I am a complete novice when it comes to powershell.
I am looking for a code that helps compare my folders with their back up folders, that are backed up with robocopy. I need to produce a notification for the user providing details.
#Create the balloon tip notification
function ShowBalloonTipInfo
{
[CmdletBinding()]
param
(
[Parameter()]
$Text,
[Parameter()]
$Title,
#It must be 'None','Info','Warning','Error'
$Icon = 'Info'
)
Add-Type -AssemblyName System.Windows.Forms
#So your function would have to check whether there is already an icon that you can reuse. This is done by using a "shared variable", which really is a variable that has "script:" scope.
if ($script:balloonToolTip -eq $null)
{
#we will need to add the System.Windows.Forms assembly into our PowerShell session before we can make use of the NotifyIcon class.
$script:balloonToolTip = New-Object System.Windows.Forms.NotifyIcon
}
$path = Get-Process -id $pid | Select-Object -ExpandProperty Path
$balloonToolTip.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($path)
$balloonToolTip.BalloonTipIcon = $Icon
$balloonToolTip.BalloonTipText = $Text
$balloonToolTip.BalloonTipTitle = $Title
$balloonToolTip.Visible = $true
#I thought to display the tool tip for one seconds so i used 2000 milliseconds when I call ShowBalloonTip.
$balloonToolTip.ShowBalloonTip(2000)
}
}
Thank you very much to #mhu for helping me fix the error message. I now get the correct balloon tip notification but it says there are 1414 files that are different between my documents and its mirror.
Refactored your code, hope this helps...
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# Create the balloon tip notification
function Show-BalloonTipInfo
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[String] $Text,
[String] $Title,
[ValidateSet('None','Info','Warning','Error')]
[String] $Icon = 'Info'
)
Add-Type -AssemblyName "System.Windows.Forms"
$path = Get-Process -Id $pid | Select-Object -ExpandProperty "Path"
$balloonToolTip = New-Object -TypeName "System.Windows.Forms.NotifyIcon"
$balloonToolTip.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($path)
$balloonToolTip.BalloonTipIcon = $Icon
$balloonToolTip.BalloonTipText = $Text
$balloonToolTip.BalloonTipTitle = $Title
$balloonToolTip.Visible = $true
#I thought to display the tool tip for one seconds so i used 2000 milliseconds when I call ShowBalloonTip.
$balloonToolTip.ShowBalloonTip(2000)
}
function Get-Directories
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[String] $Path,
[Parameter(Mandatory = $true)]
[String] $Exclude
)
$pathLength = $Path.Length
if (Test-Path -Path $Path -PathType Container)
{
return Get-ChildItem -Path $Path -Recurse -Exclude $Exclude -File -ErrorAction SilentlyContinue |
Foreach-Object { $_.FullName.Substring($pathLength + 1) } |
Sort-Object
}
}
function Write-Log
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[String] $Message,
[Parameter(Mandatory = $true)]
[String] $Path
)
$timestamp = Get-Date -format "dd-MM-yyyy HH:mm:ss"
$text = "$($timestamp): $message"
Write-Information -MessageData $text -InformationAction Continue
Add-Content -Path $Path -Value $text -Force -Encoding utf8
}
$logFile = "$env:Temp\sync.log"
# get content count
$folders = #(
#{ "Path" = [Environment]::GetFolderPath("MyDocuments"); "Exclude" = "*.pst,*.ost,*.iso"; "Files" = #() },
#{ "Path" = "D:\Documents_mirror"; "Exclude" = "*.pst,*.ost,*.iso"; "Files" = #() },
#{ "Path" = [Environment]::GetFolderPath("Desktop"); "Exclude" = "*.pst,*.ost,*.iso,*.lnk"; "Files" = #() },
#{ "Path" = "D:\Desktop_mirror"; "Exclude" = "*.pst,*.ost,*.iso,*.lnk"; "Files" = #() }
)
foreach ($folder in $folders)
{
Write-Log -Message "Searching for files in '$($folder.Path)'..." -Path $logFile
$folder.Files = #(Get-Directories -Path $folder.Path -Exclude $folder.Exclude)
}
Write-Log -Message "Comparing..." -Path $logFile
$documentsDiffCount = #(Compare-Object -ReferenceObject $folders[0].Files -DifferenceObject $folders[1].Files).Count
$desktopDiffCount = #(Compare-Object -ReferenceObject $folders[2].Files -DifferenceObject $folders[3].Files).Count
$message = ""
if (($documentsDiffCount -ge 1) -and ($documentsDiffCount -ge 1))
{
$message = "There are $documentsDiffCount file(s) in your documents and $desktopDiffCount file(s) on your desktop "
}
elseif ($documentsDiffCount -ge 1)
{
$message = "There are $documentsDiffCount file(s) in your documents "
}
elseif ($documentsDiffCount -ge 1)
{
$message = "There are $desktopDiffCount file(s) on your desktop "
}
if ($message)
{
$message += "that were not backed up. Please open robocopy and click 'Run Profile'"
}
else
{
$message = "Your files have been successfully backed up."
}
Write-Log -Message "Compare result: $message" -Path $logFile
Show-BalloonTipInfo -Text "Back up: $message"

Replace command in powershell is deleting the whole content

I am new to powershell. I create a powershell script which need to search a string in the path provided in parameters and replace that string. But actually it is replacing entire file content with new string.
I am using Powershell in Windows 10 OS.
Code:
param(
[Parameter(Mandatory=$true, ParameterSetName="Path", Position=0,HelpMessage='Data folder Path')]
[string] $Path,
[Parameter(Mandatory=$true, HelpMessage='Input the string to be replaced')]
[string] $Input,
[Parameter(Mandatory=$true,HelpMessage='Input the new string that need to be replaced')]
[string] $Replace
)
$a = Test-Path $Path
IF ($a -eq $True) {Write-Host "Path Exists"} ELSE {Write-Host "Path Doesnot exits"}
$configFiles = Get-ChildItem -Path $Path -include *.pro, *.rux -recurse
$Append = join-path -path $path \*
$b = test-path $Append -include *.pro, *.rux
If($b -eq $True) {
foreach ($file in $configFiles)
{
(Get-Content $file.PSPath) |
Foreach-Object { $_ -replace [regex]::Escape($Input), $Replace } |
Set-Content $file.PSPath
}
$wshell = New-Object -ComObject Wscript.Shell
$wshell.Popup("Operation Completed",0,"Done",0x0)
}
As best I can read this without directly reproducing it, this is where it goes wrong:
(get-content $file.pspath) gets the entire content of the file, not its name.
Your "foreach" then regexes every line in the file, and finally "set-content" replaces the contents of the file, not its path.
If you want to change the name of a file, you are looking for Rename-Item, not Set-Content. If you want the name of a file $file.Name will do, you don't need Get-Content, which will ... get its content :)
This should be a working solution.
Param(
[Parameter(Mandatory,
ParameterSetName='Path',
Position=0,
HelpMessage='Data folder Path')]
[String]
$Path,
[Parameter(Mandatory,
HelpMessage='Input the string to be replaced')]
[String]
$StringToReplace,
[Parameter(Mandatory,
HelpMessage='Input the new string that need to be replaced')]
[String]
$ReplacementString
)
If (!(Test-Path $Path)) {
Write-Host 'Path does not exist'
Return
}
Get-ChildItem -Path $Path -Include *.pro,*.rux -Recurse |
? { $_.Name -like "*$StringToReplace*" } |
% { Rename-Item $_ $($ReplacementString+$_.Extension) }
(New-Object -ComObject Wscript.Shell).Popup("Operation Completed",0,"Done",0x0)

Pattern matching in powershell to rename folder

I am trying to script to rename username folders, but some of the folders are named USERNAME_S11-121-121.... I want to verify path using test-path with wild cards and rename-item doesn't take wild cards.
Is there a way I can use test-path and rename-item with just username and exclude everything except username.
I don't know if test-path $path* is correct way of doing it?
Function Reset-Profile {
param(
[parameter(Mandatory = $true, Position = 0)]
[string] $UserName,
[parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
$Server
)
process{
$SelectedUser = $UserName
$randomNumber = Get-Random
Foreach($UserName in $Server){
$paths = #(
"\\$Server\c$\users\$SelectedUser" #local profile
"\\server01\users$\$SelectedUser", #roaming profile
"\\server02\usersupmprd$\$SelectedUser" #roaming profile
)
foreach ($path in $paths)
{
if(test-path $path or test-path $path*)
{
Rename-Item -path $path -NewName "$SelectedUser.$randomNumber"
break;
write-host profile renamed under $path to
$SelectedUser.$randomNumber
}
else{ write-host path not found}
}
}
}
}
Untested, but try this:
Function Reset-Profile {
[cmdletbinding()]
param(
[parameter(Mandatory = $true, Position = 0)]
[string] $UserName,
[parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[string[]] $Server
)
Process{
$randomNumber = Get-Random
ForEach($Host in $Server)
{
$paths = #("\\$Host\c$\users\$UserName", #local profile
"\\server01\users$\$UserName", #roaming profile
"\\server02\usersupmprd$\$UserName") #roaming profile
ForEach ($path in $paths)
{
if(test-path "$path")
{
$CurrentPath = Get-Item "$path"
}
elseif(test-path $path_*)
{
write-host $path
$CurrentPath = Get-Item "$($path)_*" #it's necessary to put $path_ in parenthesis otherwise it conflicts with some internal command and gets the path of the directory I am working in.
write-host $CurrentPath
}
else {
Write-Warning "Path not found"
}
if($CurrentPath.Count() -eq 1)
{
Rename-Item -Path $CurrentPath -NewName "$UserName.$randomNumber"
Write-Verbose "Profile renamed under $path to $UserName.$randomNumber"
} elseif ($CurrentPath.Count -gt 1) {
Write-Warning "Multiple matches for $path"
} else {
Write-Warning "Path $path not found"
}
}
}
}
}
Explanation:
Added [string[]] in front of $Server so that it explicitly accepts array input (as well as a single string).
Changed ForEach($Username in $Server) to ForEach($Host in $Server) and made use of $Host so that it loops through the servers correctly.
Uses Get-ChildItem $Path* to get any matching paths. Then checks if a single path has been returned, if it has then it will do the rename, if it's multiple
or if there was no match it will warn you as such.
Changed write-host to write-verbose which will require use of the -verbose switch to be seen. Write-host is an anti-pattern, particularly in a Function.
How to execute:
I suggest you run this as follows initially (because i've added [cmdletbinding()] at the top of your function it now supports -WhatIf and -Verbose which should pass in to the Rename-Item cmdlet and will show you what it does. If it looks right, simply remove -WhatIf:
Reset-Profile -Username TestUsername -Server Localhost -WhatIf -Verbose

How to compare files under 2 folders with PowerShell;

I am a newbie in PowerShell, and trying to learn the things from few forums and msdn. Now i got some requirement from my group of learners.
I am trying to compare 2 folder's files with each other in powershell, for effective file comparison i am using MD5 Hashes.
Till now i have created a code like this,
[Cmdletbinding()]
Param
(
[Parameter(Position=0, Mandatory)][ValidateScript({ Test-Path -Path $_ })][string]$SourceFolder,
[Parameter(Position=1, Mandatory)][ValidateScript({ Test-Path -Path $_ })][string]$DestinationFolder
)
$SourceFolderList =#()
$DestinationFolderList =#()
$Sourcefiles = #(Get-ChildItem -Path $SourceFolder -Filter *.log)
foreach($srcFile in $Sourcefiles )
{
$SourceFolderHash = [ordered]#{}
$SourceFolderHash.Name = $srcFile.Name
$SourceFolderHash.FullName = $srcFile.FullName
$obj = New-Object PSObject -Property $SourceFolderHash
$SourceFolderList+= $obj
}
$Destfiles = #(Get-ChildItem -Path $DestinationFolder -Filter *.log)
foreach($Destfile in $Destfiles )
{
$DestinationFolderHash = [ordered]#{}
$DestinationFolderHash.Name = $Destfile.Name
$DestinationFolderHash.FullName = $Destfile.FullName
$obj = New-Object PSObject -Property $DestinationFolderHash
$DestinationFolderList+= $obj
}
$SourceFolderList =#() &
$DestinationFolderList =#() are Arrays with Name & FullName properties.
Now i am trying to create a new array with values which matches in the $SourceFolderList & $DestinationFolderList ( I hope i am going in the right way?!)
But the problem is, i am not sure how to loop through each item in the Arrays and get the fullnames of each file from 2 folders to pass as params to MD5hash Function.
I am trying in this way
##1
For ($i =$j=0; $i -le $SourceFolderList.Count -and $j -le $DestinationFolderList.Count; $i++ -and $j++)
{
$file1Name = $SourceFolderList[$i].Name
$file1Path = $SourceFolderList[$i].FullName
$file2Name = $DestinationFolderList[$j].Name
$file2Path = $DestinationFolderList[$j].FullName
}
##2
foreach( $file in $SourceFolderList)
{
if($DestinationFolderList.Name -contains $file.Name )
{
Write-Host $file.Name -ForegroundColor Cyan
Write-Host $DestinationFolderList.($file.Name).FullName -ForegroundColor Yellow
}
}
In the 1st way i am not getting correct File Paths << Index is mismatching for Destination folder's file paths >>
In the 2nd Way i am not at all getting the Full Path of file.
Please correct me if am going in the wrong way to achieve my requirement.
And please help me to solve this issue.
I think your're making your task more difficult that it is, by gathering file info into the arrays. Why don't you just iterate over the files in the source folder and compare their hashes with hashes of files in the destination folder on the fly:
function Compare-Folders
{
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[string]$Source,
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
[string]$Destinaton,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$Filter
)
Process
{
# Iterate over files in source folder, skip folders
Get-ChildItem -Path $Source -Filter $Filter | Where-Object {!$_.PsIsContainer} | ForEach-Object {
# Generate file name in destination folder
$DstFileName = Resolve-Path -Path (Join-Path -Path $Destinaton -ChildPath (Split-Path -Path $_.FullName -Leaf))
# Create hashtable with filenames and hashes
$Result = #{
SourceFile = $_.FullName
SourceFileHash = (Get-FileHash -Path $_.FullName -Algorithm MD5).Hash
DestinationFile = $DstFileName
DestinationFileHash = (Get-FileHash -Path $DstFileName -Algorithm MD5).Hash
}
# Check if file hashes are equal and add result to hashtable
$Result.Add('IsEqual', ($Result.SourceFileHash -eq $Result.DestinationFileHash))
# Output PsObject from hashtable
New-Object -TypeName psobject -Property $Result |
Select-Object -Property SourceFile, SourceFileHash , DestinationFile, DestinationFileHash, IsEqual
}
}
}