How to rename files with powershell if there are duplicate files? - powershell

Get-ChildItem -Filter *_* | Foreach-Object -Process {
$NewName = [Regex]::Match($_.Name,"^[^ _]*").Value +'.jpg'
$_ | Rename-Item -NewName $NewName
}
I have been using this code to rename files
For example, 569_SOM_TEST.jpg to 569
but if there is 569_TOB_TEST.jpg, It gives an error Rename-Item : Cannot create a file when that file already exists.
I want it to make it 569-1.
How can I accomplish this?

To prevent renaming clashes, you can put the below helper function at the top of your script:
function Rename-FileUnique {
# Renames a file. If a file with that name already exists,
# the function will create a unique filename by appending '(x)' after the
# name, but before the extension. The 'x' is a numeric value.
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)]
[string]$Path,
[Parameter(Mandatory = $true, Position = 1)]
[string]$NewName,
[switch]$PassThru
)
# Throw a bit nicer error than with [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
if (!(Test-Path -Path $Path -PathType Leaf)){
Throw [System.IO.FileNotFoundException] "Rename-FileUnique: The file '$Path' could not be found."
}
# split the new filename into a basename and an extension variable
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($NewName)
$extension = [System.IO.Path]::GetExtension($NewName) # this includes the dot
$folder = Split-Path -Path $Path -Parent
# get an array of all filenames (name only) of the files with a similar name already present in the folder
$allFiles = #(Get-ChildItem $folder -Filter "$baseName*$extension" -File | Select-Object -ExpandProperty Name)
# for PowerShell version < 3.0 use this
# $allFiles = #(Get-ChildItem $folder -Filter "$baseName*$extension" | Where-Object { !($_.PSIsContainer) } | Select-Object -ExpandProperty Name)
# construct the new filename / strip the path from the file name
$NewName = $baseName + $extension # or use $NewName = Split-Path $NewName -Leaf
if ($allFiles.Count) {
$count = 1
while ($allFiles -contains $NewName) {
$NewName = "{0}-{1}{2}" -f $baseName, $count++, $extension
}
}
Write-Verbose "Renaming '$Path' to '$NewName'"
Rename-Item -Path $Path -NewName $NewName -Force -PassThru:$PassThru
}
and use it like:
Get-ChildItem -Filter '*_*.jpg' | Foreach-Object {
# create the proposed new filename
$newName = '{0}.jpg' -f ($_.Name -split '_')[0]
$_ | Rename-FileUnique -NewName $newName
}
This will ensure that any proposed new filename gets a not already used index number attached to its basename.

Related

PowerShell Copy All Files in Folders and Sub=Folders Not older than 300 minutes

I am trying to copy all files in folders and sub-folders not older than 300 minutes, but the code I got working only copies the files in the main folder, it doesn't copy the files in subfolders.
At the destination I don't want to maintain the folder structure of the original files, I just want to put all the origin files into a single specific destination folder.
This is the code I have:
Powershell -NoL -NoP -C "&{$ts=New-TimeSpan -M 300;"^
"Get-ChildItem "C:\Origin" -Filter '*.dat'|?{"^
"$_.LastWriteTime -gt ((Get-Date)-$ts)}|"^
%%{Copy-Item $_.FullName 'C:\Destination'}}"
Could someone help me out please?
Thanks in advance.
Here's a modified script for you you can save as "Copy-Unique.ps1" you can run from a batch file.
function Copy-Unique {
# Copies files to a destination. If a file with the same name already exists in the destination,
# the function will create a unique filename by appending '(x)' after the name, but before the extension.
# The 'x' is a numeric sequence value.
[CmdletBinding(SupportsShouldProcess)] # add support for -WhatIf switch
Param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[Alias("Path")]
[ValidateScript({Test-Path -Path $_ -PathType Container})]
[string]$SourceFolder,
[Parameter(Mandatory = $true, Position = 1)]
[string]$DestinationFolder,
[Parameter(Mandatory = $false)]
[int]$NewerThanMinutes = -1,
[Parameter(Mandatory = $false)]
[string]$Filter = '*',
[switch]$Recurse
)
# create the destination path if it does not exist
if (!(Test-Path -Path $DestinationFolder -PathType Container)) {
Write-Verbose "Creating folder '$DestinationFolder'"
$null = New-Item -Path $DestinationFolder -ItemType 'Directory' -Force
}
# get a list of file FullNames in this source folder
$sourceFiles = #(Get-ChildItem -Path $SourceFolder -Filter $Filter -File -Recurse:$Recurse)
# if you want only files not older than x minutes, apply an extra filter
if ($NewerThanMinutes -gt 0) {
$sourceFiles = #($sourceFiles | Where-Object { $_.LastWriteTime -gt (Get-Date).AddMinutes(-$NewerThanMinutes) })
}
foreach ($file in $sourceFiles) {
# get an array of all filenames (names only) of the files with a similar name already present in the destination folder
$destFiles = #((Get-ChildItem $DestinationFolder -File -Filter "$($file.BaseName)*$($file.Extension)").Name)
# for PowerShell version < 3.0 use this
# $destFiles = #(Get-ChildItem $DestinationFolder -Filter "$baseName*$extension" | Where-Object { !($_.PSIsContainer) } | Select-Object -ExpandProperty Name)
# construct the new filename
$newName = $file.Name
$count = 1
while ($destFiles -contains $newName) {
$newName = "{0}({1}){2}" -f $file.BaseName, $count++, $file.Extension
}
# use Join-Path to create a FullName for the file
$newFile = Join-Path -Path $DestinationFolder -ChildPath $newName
Write-Verbose "Copying '$($file.FullName)' as '$newFile'"
$file | Copy-Item -Destination $newFile -Force
}
}
# you can change the folder paths, file pattern to filter etc. here
$destFolder = Join-Path -Path 'C:\Destination' -ChildPath ('{0:yyyy-MM-dd_HH-mm}' -f (Get-Date))
Copy-Unique -SourceFolder "C:\Origin" -DestinationFolder $destFolder -Filter '*.dat' -Recurse -NewerThanMinutes 300
Changed the code to now take a datetime object to compare against rather than an amount of minutes. This perhaps makes the code easier to understand, but certainly more flexible.
function Copy-Unique {
# Copies files to a destination. If a file with the same name already exists in the destination,
# the function will create a unique filename by appending '(x)' after the name, but before the extension.
# The 'x' is a numeric sequence value.
[CmdletBinding(SupportsShouldProcess)] # add support for -WhatIf switch
Param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[Alias("Path")]
[ValidateScript({Test-Path -Path $_ -PathType Container})]
[string]$SourceFolder,
[Parameter(Mandatory = $true, Position = 1)]
[string]$DestinationFolder,
[string]$Filter = '*',
[datetime]$NewerThan = [datetime]::MinValue,
[switch]$Recurse
)
# create the destination path if it does not exist
if (!(Test-Path -Path $DestinationFolder -PathType Container)) {
Write-Verbose "Creating folder '$DestinationFolder'"
$null = New-Item -Path $DestinationFolder -ItemType 'Directory' -Force
}
# get a list of file FullNames in this source folder
$sourceFiles = #(Get-ChildItem -Path $SourceFolder -Filter $Filter -File -Recurse:$Recurse)
# if you want only files newer than a certain date, apply an extra filter
if ($NewerThan -gt [datetime]::MinValue) {
$sourceFiles = #($sourceFiles | Where-Object { $_.LastWriteTime -gt $NewerThan })
}
foreach ($file in $sourceFiles) {
# get an array of all filenames (names only) of the files with a similar name already present in the destination folder
$destFiles = #((Get-ChildItem $DestinationFolder -File -Filter "$($file.BaseName)*$($file.Extension)").Name)
# for PowerShell version < 3.0 use this
# $destFiles = #(Get-ChildItem $DestinationFolder -Filter "$baseName*$extension" | Where-Object { !($_.PSIsContainer) } | Select-Object -ExpandProperty Name)
# construct the new filename
$newName = $file.Name
$count = 1
while ($destFiles -contains $newName) {
$newName = "{0}({1}){2}" -f $file.BaseName, $count++, $file.Extension
}
# use Join-Path to create a FullName for the file
$newFile = Join-Path -Path $DestinationFolder -ChildPath $newName
Write-Verbose "Copying '$($file.FullName)' as '$newFile'"
$file | Copy-Item -Destination $newFile -Force
}
}
# you can change the folder paths, file pattern to filter etc. here
$destFolder = Join-Path -Path 'D:\Destination' -ChildPath ('{0:yyyy-MM-dd_HH-mm}' -f (Get-Date))
Copy-Unique -SourceFolder "C:\Origin" -DestinationFolder $destFolder -Filter '*.dat' -Recurse -NewerThan (Get-Date).AddMinutes(-300)
When you have saved the above code to let's say 'C:\Scripts\Copy-Unique.ps1' you can then call it from a batch file like:
Powershell.exe -NoLogo -NoProfile -File "C:\Scripts\Copy-Unique.ps1"

Powershell move file with backup (like mv --backup=numbered)

I'm looking if there's a PS command that'd be equal to mv --backup=numbered, and can't find anything.
In essence, move 'file' to 'file.old', but if 'file.old' exists, 'file' should be moved to 'file.old.2'.
For now the closest I found is from this link: https://www.pdq.com/blog/copy-individual-files-and-rename-duplicates/:
$SourceFile = "C:\Temp\File.txt"
$DestinationFile = "C:\Temp\NonexistentDirectory\File.txt"
If (Test-Path $DestinationFile) {
    $i = 0
    While (Test-Path $DestinationFile) {
        $i += 1
        $DestinationFile = "C:\Temp\NonexistentDirectory\File$i.txt"
    }
} Else {
    New-Item -ItemType File -Path $DestinationFile -Force
}
Copy-Item -Path $SourceFile -Destination $DestinationFile -Force
It seems quite awful to have this amount of code. Is there anything simpler available?
Indeed there is no built-in function to do that. However, it should not be a problem to use a function of your own for that purpose.
How about this:
function Copy-FileNumbered {
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateScript({Test-Path -Path $_ -PathType Leaf})]
[string]$SourceFile,
[Parameter(Mandatory = $true, Position = 1)]
[string]$DestinationFile
)
# get the directory of the destination file and create if it does not exist
$directory = Split-Path -Path $DestinationFile -Parent
if (!(Test-Path -Path $directory -PathType Container)) {
New-Item -Path $directory -ItemType 'Directory' -Force
}
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($DestinationFile)
$extension = [System.IO.Path]::GetExtension($DestinationFile) # this includes the dot
$allFiles = Get-ChildItem $directory | Where-Object {$_.PSIsContainer -eq $false} | Foreach-Object {$_.Name}
$newFile = $baseName + $extension
$count = 1
while ($allFiles -contains $newFile) {
$newFile = "{0}({1}){2}" -f $baseName, $count, $extension
$count++
}
Copy-Item -Path $SourceFile -Destination (Join-Path $directory $newFile) -Force
}
This will create a new file in the destination like File(1).txt
Of course, if you rather have names like File.2.txt, just change the format template "{0}({1}){2}" to "{0}.{1}{2}"
Use the function like
$SourceFile = "C:\Temp\File.txt"
$DestinationFile = "C:\Temp\NonexistentDirectory\File.txt"
Copy-FileNumbered -SourceFile $SourceFile -DestinationFile $DestinationFile

How do I move multiple files with the same name and extension that are in subfolders into one main folder [duplicate]

I have a powershell script that takes all of the files in one directory, renames the first one and moves it, and then goes to the next file. Sometimes there will be multiple files that get renamed to the same name (because of the system that it's going to, not ideal and will have to change) and was overwriting files that shouldn't have been getting overwritten with -force. I need all of the files to move but also have unique names so we have them available in the destination location. Is there an easy way to have it automatically rename so it would look like:
123.txt
123(1).txt
123(2).txt
or
123.txt
123_1.txt
123_2.txt
There's no built-in way to do that. Give this a try:
$src = "d:\temp"
$dest = "d:\temp1"
$num=1
Get-ChildItem -Path $src -Filter *.txt -Recurse | ForEach-Object {
$nextName = Join-Path -Path $dest -ChildPath $_.name
while(Test-Path -Path $nextName)
{
$nextName = Join-Path $dest ($_.BaseName + "_$num" + $_.Extension)
$num+=1
}
$_ | Move-Item -Destination $nextName
}
#Fixed solution basing on previous answer (variable $num moved into for each loop):
$src = "d:\temp"
$dest = "d:\temp1"
Get-ChildItem -Path $src -Filter *.txt -Recurse | ForEach-Object {
$num=1
$nextName = Join-Path -Path $dest -ChildPath $_.name
while(Test-Path -Path $nextName)
{
$nextName = Join-Path $dest ($_.BaseName + "_$num" + $_.Extension)
$num+=1
}
$_ | Move-Item -Destination $nextName
}
So I'm way late to the party here but... I kinda liked the responses just didn't like the lack of it being in a function so... I modified it for re-usability.
I added the Name field because I'm using this as part of another process where I am using a regex substring process to take a file name which contains <owner>-<date_string>-<hash>.<extension> and moving them into a <file_path>/<owner>/<date_string>.<extension> format.
function Move-Item-AutoRename {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline)]
[String]$Source,
[Parameter(Mandatory = $true)]
[String]$Destination,
[Parameter(Mandatory = $true)]
[String]$Name
)
PROCESS {
$count = 1
[System.IO.FileInfo]$nextName = Join-Path -Path $Destination -ChildPath $Name
while (Test-Path -Path $nextName) {
$nextName = Join-Path -Path $Destination ($Name.Split(".")[0] + "_$($count)" + $nextName.Extension)
$count += 1
}
Move-Item -Path $Source -Destination $nextName
}
}
Since I am sure others may be interested in the other part of this solution I will include it just for sharing sake.
function Import-UnsortedFiles {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline)]
[String]$Source,
[Parameter(Mandatory = $true)]
[String]$Destination
)
PROCESS {
Get-ChildItem -Path $Source -Include *.xml, *.json | ForEach-Object {
$results = $_.Name | Select-String -Pattern '^(.*)-([0-9]*_[0-9]*)-.*(\..*)$'
[System.IO.FileInfo] $dest = (Join-Path -Path $Target -ChildPath "$($results.Matches.Groups[1])/$($results.Matches.Groups[2])$($results.Matches.Groups[3])");
$test = (Test-Path -Path $dest.Directory.FullName -PathType Container)
if ($test -eq $false) {
New-Item -Path $dest.Directory.FullName -ItemType Directory;
}
Move-Item-AutoRename -Source $_.FullName -Destination $dest.Directory.FullName -Name $dest.Name
}
}
}
Call this by invoking Import-UnsortedFiles -Source $Source -Destination $Target

How might I update this code to run a second time but only on a zip archive already unzipped?

Here is the code I am currently using:
# Don't include "\" at the end of $NewSource - it will stop the script from
# matching first-level subfolders
$ignore = "somename"
$files = gci $NewSource -recurse | Where {
$_.Extension -match "zip||prd" -and $_.FullName -notlike $ignore
}
foreach ($file in $files) {
$NewSource = $file.FullName
# Join-Path is a standard Powershell cmdLet
$destination = Join-Path (Split-Path -Parent $file.FullName) $file.BaseName
Write-Host -Fore green $destination
$destination = "-o" + $destination
# Start-Process needs the path to the exe and then the arguments passed
# separately. You can also add -wait to have the process complete before
# moving to the next
Start-Process -FilePath "C:\Program Files\7-Zip\7z.exe" -ArgumentList "x -y $NewSource $destination" -Wait
}
However, once it is finished I need to go back through the new directories and unzip my .prd files that are created only after unzipping the .zip archives. Need some help here as my tries aren't working and currently unzip and overwrite all the previously unzipped .prd and .zip files.
I already told you that $_.Extension -match "zip||prd" matches all extensions, because of the empty string between the two | characters in the regular expression (all strings contain the empty string).
Also, the -notlike and -like operators behave exactly like the -ne and -eq operators when comparing a value with a pattern that doesn't have wildcards in it, so your second condition will match all files whose full name isn't exactly "somename".
Change this:
$ignore = "somename"
$files = gci $NewSource -recurse | Where {
$_.Extension -match "zip||prd" -and $_.FullName -notlike $ignore
}
into this:
$ignore = "*somename*"
$files = gci $NewSource -recurse | Where {
$_.Extension -match "zip|prd" -and $_.FullName -notlike $ignore
}
and the code should do what you expect.
As an alternative you could build a list of the paths you want to ignore
$ignore = 'C:\path\to\first.zip',
'C:\other\path\to\second.zip',
'C:\some\file.prd',
...
and use the -notin (PowerShell v3 or newer) or -notcontains operator to exclude those files:
$_.FullName -notin $ignore
$ignore -notcontains $_.FullName
As a side note, I'd use the call operator and splatting instead of Start-Process for invoking 7zip.exe:
$destination = Join-Path (Split-Path -Parent $file.FullName) $file.BaseName
$params = 'x', '-y', $NewSource, "-o$destination"
& "${env:ProgramFiles}\7-Zip\7z.exe" #params
To also extract .prd files that were extracted from the zip archives add another step to your loop.
foreach ($file in $files) {
...
& "${env:ProgramFiles}\7-Zip\7z.exe" #params
Get-ChildItem $destination | Where-Object {
$_.Extension -eq 'prd'
} | ForEach-Object {
# extract matching file here, procedure is the
# same as with the files in the outer loop
}
}
You may want to wrap the code for building the destination path and extracting the file in a function that reads paths from the pipeline and calls itself recursively if the destination path contains .prd files.
function Invoke-Unzip {
[CmdletBinding()]
Param(
[Parameter(
Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true
)]
[ValidateScript({Test-Path -LiteralPath $_})]
[string]$FullName
)
$newSource = $FullName
...
& "${env:ProgramFiles}\7-Zip\7z.exe" #params
Get-ChildItem $destination |
Where-Object { $_.Extension -eq 'prd' } |
Invoke-Unzip
}

Powershell Move-Item Rename If File Exists

I have a powershell script that takes all of the files in one directory, renames the first one and moves it, and then goes to the next file. Sometimes there will be multiple files that get renamed to the same name (because of the system that it's going to, not ideal and will have to change) and was overwriting files that shouldn't have been getting overwritten with -force. I need all of the files to move but also have unique names so we have them available in the destination location. Is there an easy way to have it automatically rename so it would look like:
123.txt
123(1).txt
123(2).txt
or
123.txt
123_1.txt
123_2.txt
There's no built-in way to do that. Give this a try:
$src = "d:\temp"
$dest = "d:\temp1"
$num=1
Get-ChildItem -Path $src -Filter *.txt -Recurse | ForEach-Object {
$nextName = Join-Path -Path $dest -ChildPath $_.name
while(Test-Path -Path $nextName)
{
$nextName = Join-Path $dest ($_.BaseName + "_$num" + $_.Extension)
$num+=1
}
$_ | Move-Item -Destination $nextName
}
#Fixed solution basing on previous answer (variable $num moved into for each loop):
$src = "d:\temp"
$dest = "d:\temp1"
Get-ChildItem -Path $src -Filter *.txt -Recurse | ForEach-Object {
$num=1
$nextName = Join-Path -Path $dest -ChildPath $_.name
while(Test-Path -Path $nextName)
{
$nextName = Join-Path $dest ($_.BaseName + "_$num" + $_.Extension)
$num+=1
}
$_ | Move-Item -Destination $nextName
}
So I'm way late to the party here but... I kinda liked the responses just didn't like the lack of it being in a function so... I modified it for re-usability.
I added the Name field because I'm using this as part of another process where I am using a regex substring process to take a file name which contains <owner>-<date_string>-<hash>.<extension> and moving them into a <file_path>/<owner>/<date_string>.<extension> format.
function Move-Item-AutoRename {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline)]
[String]$Source,
[Parameter(Mandatory = $true)]
[String]$Destination,
[Parameter(Mandatory = $true)]
[String]$Name
)
PROCESS {
$count = 1
[System.IO.FileInfo]$nextName = Join-Path -Path $Destination -ChildPath $Name
while (Test-Path -Path $nextName) {
$nextName = Join-Path -Path $Destination ($Name.Split(".")[0] + "_$($count)" + $nextName.Extension)
$count += 1
}
Move-Item -Path $Source -Destination $nextName
}
}
Since I am sure others may be interested in the other part of this solution I will include it just for sharing sake.
function Import-UnsortedFiles {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline)]
[String]$Source,
[Parameter(Mandatory = $true)]
[String]$Destination
)
PROCESS {
Get-ChildItem -Path $Source -Include *.xml, *.json | ForEach-Object {
$results = $_.Name | Select-String -Pattern '^(.*)-([0-9]*_[0-9]*)-.*(\..*)$'
[System.IO.FileInfo] $dest = (Join-Path -Path $Target -ChildPath "$($results.Matches.Groups[1])/$($results.Matches.Groups[2])$($results.Matches.Groups[3])");
$test = (Test-Path -Path $dest.Directory.FullName -PathType Container)
if ($test -eq $false) {
New-Item -Path $dest.Directory.FullName -ItemType Directory;
}
Move-Item-AutoRename -Source $_.FullName -Destination $dest.Directory.FullName -Name $dest.Name
}
}
}
Call this by invoking Import-UnsortedFiles -Source $Source -Destination $Target