My PowerShell script creates a log file, but when I run the script for the second time, it tells me that the testfile1.log file already exists.
How do I make the script if it finds testfile1.log, it creates testfile2.log, and if this also exists, it creates testfile3.log, and so on..
New-Item -Path $path -Name "testfile1.log" -ItemType "file"
You could do it this way, first get all the files in the desired path and sort them by the ending digits on their name. If no files are found create the testfile1.log, if there were files found, get the last sorted file (the one with the highest ending digit) extract the ending digits and add +1 to the count and use it to create the new file.
$files = Get-ChildItem $path -Filter testfile*.log | Sort-Object {
$_.BaseName -replace '\D' -as [int]
}
if(-not $files)
{
New-Item -Path $path -Name "testfile1.log" -ItemType File
}
else
{
[int]$number = $files[-1].BaseName -replace '\D'
$number++
New-Item -Path $path -Name "testfile$number.log" -ItemType File
}
An alternative method, based on this answer could be
$path = 'D:\Test'
$log = 'testfile'
$index = ((Get-ChildItem -Path $path -Filter "$log*.log" -File |
Where-Object { $_.BaseName -match "$log\d+$" } |
Select-Object #{Name = 'index'; Expression = {[int]($_.BaseName -replace '\D')}}).index |
Measure-Object -Maximum).Maximum + 1
# create the new file
New-Item -Path (Join-Path -Path $path -ChildPath "$log${index}.log") -ItemType File
A concise solution that also builds on this answer (see there for an explanation of the core technique):
$path = '.' # Output dir.
$nameTemplate = 'testfile{0}.log' # {0} is the sequence-number placeholder
New-Item -ItemType File -Path $path -Name (
$nameTemplate -f (1 + (
# Find all existing log files
Get-ChildItem (Join-Path $path $nameTemplate.Replace('{0}', '*')) |
Measure-Object -Maximum {
# Extract the embedded sequence number.
$_.Name -replace [regex]::Escape($nameTemplate).Replace('\{0}', '(\d+)'), '$1'
}
).Maximum)
) -WhatIf
Note: The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.
Note:
The above uses a complex -replace operation to reliably extract the sequence number from existing file names; if you know that that only one number is present in each given file name, $_.BaseName -replace '\D' (removing all non-digit characters) will do in the Measure-Object call above.
If you wanted to use zero-padded, fixed-width sequence numbers, you can adjust (all occurrences of) the {0} placeholder accordingly; e.g, to create sequence numbers 01, 02, ... 99, use {0:00} - see the Composite formatting help topic, which describes the string formatting language also used by PowerShell's -foperator
Related
i´ve got little bit stucked with my powershell script.
I would like to run through multiple folders, grab files based on their last modified date and copy them to a new location.
There i have to rename them to a specific convention based on it´s original Filename.
What i wrote only runs through the first part and copy files successfully but not rename them afterwards. Of course when i run the script a second time it renames the files...
File convention is:
120_00001_000_002222_202201_20220124_121833_Formular - Copy.pdf
result should be
2222_120_Memory 01-2022_012022.pdf
this is what i got already
$path = "G:\Temp"
$Target = "K:\Local"
$Max_days = "-60" #Max Days past
$Curr_date = Get-Date
$files = get-childitem $Target *.pdf
Get-ChildItem -Path $path -Recurse -Filter 120_*.pdf |
Where-Object {
$_.LastWriteTime `
-gt (Get-Date $Curr_date.AddDays($Max_days)) `
} | ForEach-Object { $_ | Copy-Item -Destination $Target -Force -PassThru }
foreach($pdf in $files)
{
$split = $pdf.name -replace ".pdf" -split "_"
$newname = "$($split[3].TrimStart("0"))_$($split[0])_$("Memory") $($split[4].Substring($split[4].Length - 2, 2))-$($split[5].Substring(0,4))_$($split[4].Substring($split[4].Length - 2, 2))$($split[5].Substring(0,4))$($pdf.Extension)"
write-verbose "Original: $($pdf.name)" -verbose
write-verbose "NewName: $($newname)" -verbose
Rename-Item $pdf.FullName -NewName $newname -verbose
}
Thanks in adavnced
Edited the Question to more precision.
As commented, you could do this in one loop and rename the file while copying.
Try below:
$path = 'G:\Temp'
$Target = 'K:\Local'
$Max_days = -60 # Max Days in the past
$refDate = (Get-Date).AddDays($Max_days).Date # set to midnight
# get the files of interest
Get-ChildItem -Path $path -Recurse -Filter '120_*_*_*_*_*_*_*.pdf' -File |
Where-Object { $_.LastWriteTime -gt $refDate } |
ForEach-Object {
# rename the file to match the new file naming convention
$split = $_.BaseName -split "_"
# just for clarity, using this example:
# '120_00001_000_002222_202201_20220124_121833_Formular - Copy.pdf'
# $split[0] --> 120 used unchanged
# $split[1] --> 00001 unused
# $split[2] --> 000 unused
# $split[3] --> 002222 used without leading zeros
# $split[4] --> 202201 used, only the last two digits (month)
# $split[5] --> 20220124 used, only the first four digits (year)
# $split[6] --> 121833 unused
# $split[7] --> Formular - Copy unused
# these elements are used more than once, so for convenience store in separate variables
$month = $split[4].Substring($split[4].Length - 2, 2)
$year = $split[5].Substring(0,4)
# construct the new file name
$newName = '{0}_{1}_Memory {2}-{3}_{2}{3}{4}' -f $split[3].TrimStart("0"),
$split[0],
$month,
$year,
$_.Extension
# construct the complete target path and filename
$targetFile = Join-Path -Path $Target -ChildPath $newName
# now copy the file with a new name to the target folder
$_ | Copy-Item -Destination $targetFile -Force
}
I've used the -f Format operator to construct the new filename, because I believe this makes the code easier to read.
I did not take into consideration that naming collisions might occur (file with that new name already in the target folder).
If that can happen, you need to tell us what strategy to use.
Perhaps append an index number to the file in brackets like Windows does?
I've got a set of files in the following format
I'd like to change the file names to the following format
I've used the following code:
`$i = 21
dir | ForEach-Object {$_ | Rename-Item -NewName ('00816-101998-XX-A-2-50-{0:D3} Detalj.{1}' -f $i++, $_.Extension)}`
This code starts renaming with file name A.10.11.11
How do I get it to start from the first file name i.e. 8.A.10.11.8?
Thank you!
Looking at your question, I think this is what you intend to do.
The list of current filenames should be sorted by the integer value the filenames start with.
Next you want to rename them using a counter that starts with value 21
To do this, you can use a ForEach-Object loop, but the -NewName parameter of Rename-Item can also contain a scriptblock containing the action to perform.
$i = 21
(Get-ChildItem -Path 'D:\Test' -Filter '*.pdf' -File) |
Sort-Object #{Expression = {[int]($_.Name -split ' ')[0]}} |
Rename-Item -NewName { '00816-101998-XX-A-2-50-{0:D3} Detalj{1}' -f $script:i++, $_.Extension }
P.S. The image shows the filename starts with a number followed by a space, while in the question you give an example where the number is followed by a dot .. If that is the case in you file names, change [int]($_.Name -split ' ')[0] into [int]($_.Name -split '\.')[0]
1. We need to address and increment the value of the counter $i using the $script:i scoping syntax, otherwise $i does not exist in the NewName scriptblock
2. To avoid renaming files twice, enclose the Get-ChildItem part in the code in between brackets
param(
$directory = "d:\tmp",
[regex]$rx = "(?<=8.A.10.11.)\d+",
[string]$newPrefix = "00816-101998-XX-A-2-50-"
)
<#
8..20 | ForEach-Object {
$filePath = Join-Path $directory -ChildPath "8.A.10.11.$_.pdf"
Out-File -InputObject "some text" -FilePath $filePath -Encoding default
}
#>
Get-ChildItem $directory | ForEach-Object{
Rename-Item $_.FullName -NewName "$newPrefix$((($rx.Matches($_.BaseName)).value).padleft(3,'0'))$($_.Extension)"
}
I have about 2500 CSV files each around 20MB in terms of file size. I am trying to filter out certain rows from each file and save that to a new file.
So, if i have :
File 1 :
Row1
Row2
Row3
File 2 :
Row2
Row3
and so on..
If i filter for all files and select "Row2" as filter text the new folder should have all the files with only rows that match the filter text.
Looking through some forums, I came up with the following that might help me filter the rows, but Im not sure how I can do it recursively, plus I also don't know if this is a fast enough method. Any help is appreciated.
Get-Content "C:\Path to file" | Where{$_ -match "Rowfiltertext*"} | Out-File "Path to Out file"
I'm using windows so I guess Powershell type of solution would be the best here.
The text to be filtered will always be in the first column.
Thanks
Siddhant
Here's two fast ways of searching for a string inside (text) files:
1) using switch
$searchPattern = [regex]::Escape('Rowfiltertext') # for safety escape regex special characters
$sourcePath = 'X:\Path\To\The\Csv\Files'
$outputPath = 'X:\FilteredCsv.txt'
# if you also need to search inside subfolders, append -Recurse to the Get-ChildItem cmdlet
Get-ChildItem -Path $sourcePath -Filter '*.csv' -File | ForEach-Object {
# iterate through the lines in the file and output the ones that match the search pattern
switch -Regex -File $_.FullName {
$searchPattern { $_ }
}
} | Set-Content -Path $outputPath # add -PassThru to also show on screen
2) using Select-String
$searchPattern = [regex]::Escape('Rowfiltertext') # for safety escape regex special characters
$sourcePath = 'X:\Path\To\The\Csv\Files'
$outputPath = 'X:\FilteredCsv.txt'
# if you also need to search inside subfolders, append -Recurse to the Get-ChildItem cmdlet
Get-ChildItem -Path $sourcePath -Filter '*.csv' -File | ForEach-Object {
($_ | Select-String -Pattern $searchPattern).Line
} | Set-Content -Path $outputPath # add -PassThru to also show on screen
In case you want to output a new csv file for every original file,
use:
3) using switch
$searchPattern = [regex]::Escape('Rowfiltertext') # for safety escape regex special characters
$sourcePath = 'X:\Path\To\The\Csv\Files'
$outputPath = 'X:\FilteredCsv'
if (!(Test-Path -Path $outputPath -PathType Container)) {
$null = New-Item -Path $outputPath -ItemType Directory
}
# if you also need to search inside subfolders, append -Recurse to the Get-ChildItem cmdlet
(Get-ChildItem -Path $sourcePath -Filter '*.csv' -File) | ForEach-Object {
# create a full target filename for the filtered output csv
$outFile = Join-Path -Path $outputPath -ChildPath ('New_{0}' -f $_.Name)
# iterate through the lines in the file and output the ones that match the search pattern
$result = switch -Regex -File $_.FullName {
$searchPattern { $_ }
}
$result | Set-Content -Path $outFile # add -PassThru to also show on screen
}
4) using Select-String
$searchPattern = [regex]::Escape('Rowfiltertext') # for safety escape regex special characters
$sourcePath = 'X:\Path\To\The\Csv\Files'
$outputPath = 'X:\FilteredCsv'
# if you also need to search inside subfolders, append -Recurse to the Get-ChildItem cmdlet
(Get-ChildItem -Path $sourcePath -Filter '*.csv' -File) | ForEach-Object {
# create a full target filename for the filtered output csv
$outFile = Join-Path -Path $outputPath -ChildPath ('New_{0}' -f $_.Name)
($_ | Select-String -Pattern $searchPattern).Line | Set-Content -Path $outFile # add -PassThru to also show on screen
}
Hope that helps
Re. "fast enough method":
Get-Content is extremely slow.
You could use "System.IO.StreamReader" instead, i.e. read the complete file content into a string, then split this string into rows and so on, e.g.:
[System.IO.FileStream]$objFileStream = New-Object System.IO.FileStream($Csv.FullName, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
[System.IO.StreamReader]$objStreamReader = New-Object System.IO.StreamReader($objFileStream, [System.Text.Encoding]::UTF8)
$strFileContent = ($objStreamReader.ReadToEnd())
$objStreamReader.Close()
$objStreamReader.Dispose()
$objFileStream.Close()
$objFileStream.Dispose()
[string[]]$arrFileContent = $strFileContent -split("`r`n")
I am using the code below to filter out files depending on the headers in the file.
It works like a charm, but I have a problem with that it takes all the files in the $InputDirectory.
I would like to limit it so it only takes files that are 1-2 hours old.
There are two ways where I can get the date for this process.
Filename contains timestamp = XXXXXXXXXXX_XXXXXXXX_valuereport_YYYYMMDDhhmmss.csv
The timestamp the file was created (please note we are talking about 800K-1M files in the directory and more is added every hour, so the fastest way would be appreciated.
So how do I insert something in my code, so it besides the header, only takes files that are <1-2 hours old?
Sorry about the code example, I am new to this site and not sure how to get it in the right order.
Nothing yet.
foreach ($FilePath in (Get-ChildItem $InputDirectory -File) | Select-Object -ExpandProperty FullName) {
$Header = Get-Content $FilePath -First 1
# test for a string in the header line that distincts it from the other files
if ($Header -match ';energy,Wh,') {
# the substring ';energy,Wh,' defines this file as a 'HeatMeter' file
Copy-Item -Path $FilePath -Destination $OutputPathHeat
} elseif ($Header -match ';fabrication-no,,inst-value,0,0,0;datetime,,inst-value,0,0,0;volume,m3') {
# the substring ';datetime,,inst-value,0,0,0;volume,m3' defines this file as a 'WaterMeter' file
Copy-Item -Path $FilePath -Destination $OutputPathWater
} else {
# if all key substrings above did not match, move to the 'Other' directory
Copy-Item -Path $FilePath -Destination $OutputPathOther
}
There are several ways to filter a directory listing. The easiest way is to pipe the result of Get-ChildItem through Where-Object like:
Get-ChildItem -Path $InputDirectory -File |
Where-Object { $_.CreationTime -gt (Get-Date).AddHours(-2) } |
Select-Object -ExpandProperty FullName |
ForEach-Object {
$FilePath = $_
$Header = Get-Content $FilePath -First 1
# test for a string in the header line that distincts it from the other files
if ($Header -match ';energy,Wh,') {
# the substring ';energy,Wh,' defines this file as a 'HeatMeter' file
Copy-Item -Path $FilePath -Destination $OutputPathHeat
}
elseif ($Header -match ';fabrication-no,,inst-value,0,0,0;datetime,,inst-value,0,0,0;volume,m3') {
# the substring ';datetime,,inst-value,0,0,0;volume,m3' defines this file as a 'WaterMeter' file
Copy-Item -Path $FilePath -Destination $OutputPathWater
}
else {
# if all key substrings above did not match, move to the 'Other' directory
Copy-Item -Path $FilePath -Destination $OutputPathOther
}
}
It checks that the CreationTime is greater than now - 2h. Note that the last modified (LastWriteTime) timestamp may also be suitable for your use case.
I need to bulk rename files in a file share that
contain a specific character, namely a tilde ~ and
have the file extension in capital letters or none at all.
The goal would be to replace the tilde with a simple -, keep the file extension, if there is one, but transform it into lowercase letters.
I've had success with the first part of the script that finds the files
$PATH = "\\<Fileservername>\<Folder>\"
$pattern = "*~*"
Get-ChildItem $PATH -Recurse | where {$_.Name -like $pattern}
What I'm struggling with is the second part of the script the renaming.
I've found two topics here:
Powershell renaming a specific Character
PowerShell Regex Bulk Replace Filenames
I haven't been able to adapt those solutions to my need plus there may be additional steps to take in order to convert the given file name from capital letters to lowercase letters or skip this if the file has no file extension.
An example would be to rename ACUJLH~H to ACUJLH-H and KYA3BM~Q.PDF to KYA3BM-Q.pdf.
Here's my contribution. I have added the -File switch to the Get-ChildItem cmdlet so it will look for files only and will not try and handle directory names.
Also, I have changed the replace pattern to ~+ so files that have repeating tildes will be replaced with a single - character. (KYA3BM~~~~Q.PDF becomes KYA3BM-Q.pdf)
$path = "D:\Code\PowerShell\StackOverflow"
$pattern = "*~*"
Get-ChildItem $path -Recurse -File | Where-Object {$_.Name -like $pattern} |
ForEach-Object {
$directory = $_.DirectoryName # or [System.IO.Path]::GetDirectoryName($_.FullName) or use Split-Path $_.FullName -Parent
$filename = $_.BaseName -replace '~+', '-' # or [System.IO.Path]::GetFileNameWithoutExtension($_.Name) -replace '~+', '-'
$extension = $_.Extension # or [System.IO.Path]::GetExtension($_.Name)
if (![string]::IsNullOrEmpty($extension)) { $filename += $extension.ToLower() }
$newname = Join-Path -Path $directory -ChildPath $filename
Rename-Item -LiteralPath $_.FullName -NewName $newName -Force
}
You will need to filter files those meets your criteria. Then using ForEach-Object compare for extensions and build new file names for every found item. Finally, using Rename-Item cmdlet you make the change.
$PATH = '\\<Fileservername>\<Folder>\'
Get-ChildItem $PATH -Recurse -Include '*~*' | ForEach-Object {
[String]$Extension = [System.IO.Path]::GetExtension($_)
[String]$NewFileName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) -replace '~','-'
if ($Extension){ $NewFileName += $Extension.ToLower() }
Rename-Item $_.FullName $(Join-Path ([System.IO.Path]::GetDirectoryName($_)) $NewFileName) -Force
}