Bulk Move Sequential Numbers (Suffix) - Images, JPEG, RAW etc - powershell

I need to move a sequential number or sometimes a random ID with letters.
As an example:
Australia_Brisbane_NP_©_Hello_World_1163
Australia_Brisbane_NP_©_Hello_World_1164
Australia_Brisbane_NP_©_Hello_World_1165
Australia_Brisbane_NP_©_Hello_World_AHDFGF
Australia_Brisbane_NP_©_Hello_World_GHRADQ
Australia_Brisbane_NP_©_Hello_World_QGASFS
What I need to do is have ©_Hello_World at the end and move the ID behind the ©, Example below:
Australia_Brisbane_NP_1165_©_Hello_World
Australia_Brisbane_NP_AHDFGF_©_Hello_World
The ideal script would allow me to specify between 1-15 characters at the end of the word without effecting the extension and move the 1-15 characters behind _©.
I have tried searching for a lot of different scripts however either they do not work or they are too complicated for me to adapt them to what is required.
I am unable to use any external software and as such I have to stick to PowerShell.

The basic "change filename" script is:
Get filenames Get-ChildItem -LiteralPath 'C:\Path\To\Files'
Pipe the results into Rename-Item
Use the property -NewName with a scriptblock {}
In the scriptblock, code to calculate the new name from the old name
Extract the filename out from the path and the extension
Change it
Put the path and the extension back on
I have tried searching for a lot of different scripts however either they do not work or they are too complicated for me to adapt them to what is required.
Text processing is all about details, details make code more complicated, and small details can invalidate whole approaches.
It's not at all clear to my why you say:
The ideal script would allow me to specify between 1-15 characters at the end of the word without effecting the extension and move the 1-15 characters behind _©.
Why would you benefit from specifying the character count, instead of having the ideal script move "all of them"?
This script should do it:
$count = Read-Host -Prompt 'How many characters to move?'
Get-ChildItem -LiteralPath 'C:\Path\To\Files' |
Rename-Item -NewName {
$newName = $_.BaseName -replace "(©_Hello_World)_(.{$($count)})", '$2_$1'
"$($_.DirectoryName)/$($newName + $_.Extension)"
}
Nb. it will move the count you asked for, even if there are more characters there.
If you don't need to specify the count, and just take all to the extension, then replace (.{$($count)}) with (.*), and remove the Read-Host line.

Related

Rename files in a folder using powershell, keeping the start and end of string in original filename

Currently trying to create a script that renames specific files within a chosen folder so that the resulting renamed files look like the following:
Original Filename: 45.09 - WrapperA12_rev1.DXF
Resultant Filename: 45.09_1.DXF
So the rev number is included as a suffix to the base filename, the extension is kept and the first 5 characters of the filename is kept (including the ".").
I can get fairly close by removing the hyphens, spaces and letters from the original filename using the -replace argument, but the resultant filename using the example above would be "45.0912_1", where the file extension is ".0912_1". This makes sense, but any attempt I've made to append the file extension (".DXF") to the filename hasn't worked.
$listdxf=gci -path $pathfolder -Filter *.DXF | Select-Object
$prenameDXF=$listdxf|rename-item -WhatIf -newname {$_.name -replace('[a-z]') -replace('-') -
replace('\s','')}
$prenameDXF
Any feedback on how I would go about doing this would be greatly appreciated.
For further clarification; the original filenames will always have the 4 numbers and the dot at the start of the filename - these need to be kept for the output name, the only other number I want is the number at the end of the filename that will always refer to the revision number, however this number may be variable (i.e; it could be 0 or 0.1,1,1.1 etc.). The Rev number will ALWAYS follow the underscore in the original filename. All other numbers and letters etc. in the original filename need to be removed. I'm assuming the solution might include assigning a variable to just return the first 4 numbers (i.e; XX.XX) as a substring maybe, while assigning a variable to the last few characters that follow the "_". Then maybe combine the two and add the ".DXF" file extension.
LATEST UPDATE: Following the responses here, I've been able to get the functionality nearly exactly where I need it to be.
I've been using the regex provided below, and with some slight changes adapted it to allow for some other things (to allow for spaces after "rev" and to allow for the rev number to be separated by a dot if present, i.e; rev1.1 etc.), but currently struggling to find a way of simply returning "0" if no "rev" is present in the file name. For example, if a filename is as follows: 31.90 - SADDLE SHIM.DXF - I wish for the rename regex script to return 31.90_0. The expression I'm currently using is as follows: '(\d{2}\.\d{2}).*?rev(\s?\d+\.\d+|\s?\d+).*(?=\.DXF)', '$1_$2'
I have tried putting a pipeline (if) after the capture block following the "rev" and then putting (0) in a new capture block, but that's not working. Any feedback on this would be greatly appreciated. Thanks again for the replies.
It looks like this regex could do the trick to rename your files with your desired format: (?<=\.\d+)\s.+(?=_rev)|rev.
Get-ChildItem -Filter *-*_rev*.dxf |
Rename-Item -NewName { $_.Name -replace '(?<=\.\d+)\s.+(?=_rev)|rev' }
However the above assumes all files will start with some digits followed by a dot followed by more digits and may or may not be 5 digits including dots. It also assumes there will be a white space after the remaining digits. It also assumes the files will end with rev followed by more digits after it's dxf extension.
This regex could work too (?<=^[\d.]{5})\s.+(?=_rev)|rev, however this one assumes only will capture the first 5 digits including one or more dots.
Per your update, you could try using switch with the -regex option. $Matches will contain the matches and you can reference the match groups by using the group number as the key (e.g. $Matches[1]). You may also reference as a property (e.g., $Matches.1)
Get-ChildItem c:\temp\powershell\testrename -File |
Rename-Item -NewName {
switch -Regex ($_.Name) {
'(\d{2}\.\d{2}).*?rev(\s?\d+\.\d+|\s?\d+).*(?=\.DXF)' {
"$($Matches.1)_$($Matches.2).DXF"
break
}
'(\d{2}\.\d{2}).*(?=\.DXF)' {
"$($Matches.1)_0.DXF"
break
}
default {
$_
}
}
} -WhatIf
Remove -WhatIf once done testing to perform rename action

PowerShell Get-VHD "is not an existing virtual hard disk file"

When creating a new VM in Hyper-V, to keep things organized, I use a particular naming convention when creating the associated VHDX files. The naming convention is the VMs FQDN followed by the SCSI controller attachment point followed by what the name of the drive is called or used for inside of the VM. I encapsulate the SCSI and Name parameters inside smooth and square brackets respectively. I find this tends to make things a little bit easier from a human perspective to match the VHDX files in Hyper-V to what the VM sees internally when needing to do maintenance tasks. It has also helped with scripting in the past. An example file name would look as follows...
servername.example.com(0-0)[OS].vhdx
This has worked well for quite some time, but recently I tried to run some PowerShell commands against the VHDX files and ran across a problem. Apparently the square brackets for the internal VM name are being parsed as RegEx or something inside of the PowerShell commandlet (I'm honestly just guessing on this). When I try to use Get-VHD on a file with the above naming convention it spits out an error as follows:
Get-VHD : 'E:\Hyper-V\servername.example.com\Virtual Hard Disks\servername.example.com(0-0)[OS].vhdx' is not an existing virtual hard disk file.
At line:1 char:12
+ $VhdPath | Get-VHD
+ ~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Get-VHD], VirtualizationException
+ FullyQualifiedErrorId : InvalidParameter,Microsoft.Vhd.PowerShell.Cmdlets.GetVHD
If I simply rename the VHDX file to exclude the "[OS]" portion of the naming convention the command works properly. The smooth brackets for the SCSI attachment point don't seem to bother it. I've tried doing a replace command to add a backtick ''`'' in front of the brackets to escape them, but the same error results. I've also tried double backticks to see if passing in a backtick helped... that at least showed a single backtick in the error it spat out. Suspecting RegEx, I tried the backslash as an escape character too... which had the interesting effect of converting all the backslashes in the file path into double backslashes in the error message. I tried defining the path variable via single and double quotes without success. I've also tried a couple of different ways of obtaining it via pipeline such as this example...
((Get-VM $ComputerName).HardDrives | Select -First 1).Path | Get-VHD
And, for what it's worth, as many VMs as I am attempting to process... I need to be able to run this via pipeline or some other automation scriptable method rather than hand coding a reference to each VHDX file.
Still thinking it may be something with RegEx, I attempted to escape the variable string with the following to no avail:
$VhdPathEscaped = [System.Text.RegularExpressions.Regex]::Escape($VhdPath)
Quite frankly, I'm out of ideas.
When I first ran across this problem was when I tried to compact a VHDX file with PowerShell. But, since the single VM I was working with needed to be offline for that function to run anyway, rather than fight the error with the VHDX name, I simply renamed it, compacted it, and reset the name back. However, for the work I'm trying to do now, I can't afford to take the VM offline as this script is going to run against a whole fleet of live VMs. So, I need to know how to properly escape those characters so the Get-VHD commandlet will accept those file names.
tl;dr:
A design limitation of Get-VHD prevents it from properly recognizing VHD paths that contain [ and ] (see bottom section for details).
Workaround: Use short (8.3) file paths assuming the file-system supports them:
$fso = New-Object -ComObject Scripting.FileSystemObject
$VhdPath |
ForEach-Object { $fso.GetFile((Convert-Path -LiteralPath $_)) } |
Get-VHD
Otherwise, your only options are (as you report, in your case the VHDs are located on a ReFS file-system, which does not support short names):
Rename your files (and folders, if applicable) to not contain [ or ].
Alternatively, if you can assume that your VHDs are attached to VMs, you can provide the VM(s) to which the VHD(s) of interests are attached as input to Get-VHD, via Get-VM (you may have to filter the output down to only the VHDs of interest):
(Get-VM $vmName).Id | Get-VHD
Background information:
It looks like Get-VHD only has a -Path parameter, not also a -LiteralPath parameter, which looks like a design flaw:
Having both parameters is customary for file-processing cmdlets (e.g. Get-ChildItem):
-Path accepts wildcard expressions to match potentially multiple files by a pattern.
-LiteralPath is used to pass literal (verbatim) paths, to be used as-is.
What you have is a literal path that happens to look like a wildcard expression, due to use of metacharacters [ and ]. In wildcard contexts, these metacharacter must normally be escaped - as `[ and `] - in order to be treated as literals, which the following (regex-based) -replace operation ensures[1] (even with arrays as input).
Unfortunately, this appears not to be enough for Get-VHD. (Though you can verify that it works in principle by piping to Get-Item instead, which also binds to -Path).
Even double `-escaping (-replace '[][]', '``$&') doesn't work (which is - unexpectedly required in come cases - see GitHub issue #7999).
# !! SHOULD work, but DOES NOT
# !! Ditto for -replace '[][]', '``$&'
$VhdPath -replace '[][]', '`$&' | Get-VHD
Note: Normally, a robust way to ensure that a cmdlet's -LiteralPath parameter is bound by pipeline input is to pipe the output from Get-ChildItem or Get-Item to it.[2] Given that Get-VHD lacks -LiteralPath, this is not an option, however:
# !! DOES NOT HELP, because Get-VHD has no -LiteralPath parameter.
Get-Item -LiteralPath $VhdPath | Get-VHD
[1] See this regex101.com page for an explanation of the regex ($0 is an alias of $& and refers to the text captured by the match at hand, i.e. either [ or ]). Alternatively, you could pass all paths individually to the [WildcardPattern]::Escape() method (e.g., [WildcardPattern]::Escape('a[0].txt') yields a`[0`].txt.
[2] See this answer for the specifics of how this binding, which happens via the provider-supplied .PSPath property, works.
Ok... So, I couldn't get the escape characters to be accepted by Get-VHD... be it by hand or programmatically. I gave it a go of passing it on the pipeline using Get-ChildItem too without success. However... I did manage to find an alternative for my particular use case. In addition to a path to a VHDX file, the Get-VHD command will also accept vmid, and disknumber as parameters. So, not that it's the way I wanted to go about obtaining what I need (because this method spits out info on all the attached drives), I can still manage to accomplish the task at hand by using the following example:
Get-VM $ComputerName | Select-Object -Property VMId | Get-VHD
By referencing them in this manner the Get-VHD commandlet is happy. This works for today's problem only because the VHDX files in question are attached to VMs. However, I'll still need to figure out about referencing unattached files at some point in the future. Which... Maybe ultimately require a slow and painful renaming of all the VHDX files to not use the square brackets in their name.

Conditional Rename of Multiple Files - Powershell

Scenario: Folder with more than one file(There are a maximum of 5 files). Each file starts with a character(does not repeat) followed by numbers. e.g: A123,B234,C123...
Objective: Rename the files according to a predetermined mapping. e.g: if A=1, B=2 etc. Then the File Starting with "A" becomes "1.", the file starting with "B" becomes "2." and so on. e.g: A123 => 1.A123
My Solution: I am not fluent in PowerShell but here is my attempt in achieving the above objective.
powershell "cd C:\Temp ; dir | ForEach-Object{if ($_.Name -Like "A*") {Rename-Item $_ "1.$_"} else {if ($_.Name -like "B*") {Rename-Item $_ "2.$_"} else{if($_.Name -like "C*"){Rename-Item $_ "3.$_"}}}}"
I needed the script to be executed from cmd and also in a specific folder (hence the cd and then the composed rename command).
This gets the job done but I would really appreciate if anyone could simplify things and show me a more prettier way at dealing with the situation.
So you can convert a letter to a number using something like:
[int][char]"F"
That will output 70. So, for your need you just need to get the first character of the file name, which is a simple SubString(0,1) call, then run it through ToUpper() to make sure you don't get any lower case letters, and then do the [int][char] bit to it, and subtract 64.
powershell "cd C:\Temp ; dir | ForEach-Object{$NewNameNum = [int][char]$_.Name.Substring(0,1).ToUpper() - 64;Rename-Item $_ "$NewNameNum.$_"}
Edit: Ok, so your original question is misleading, and should be edited to more accurately represent your request. If you are not assigning A=1, B=2, C=3 as a direct translation I can see 2 good options. First is a hashtable lookup.
PowerShell "$NmbrConv = #{'A'=3;'B'=1;'C'=9;'D'=2};dir c:\temp\*|%{$NewNameNum = $NmbrConv[$_.Name.Substring(0,1)];Rename-Item $_ "$NewNameNum.$_"}
This defines what letters convert to what numbers, then for each file just references the hashtable to get the number.
The other option is the Switch command. Running it in-line gets kind of ugly, but here's what it would look like formatted nicely.
Switch(GCI C:\Temp){
"^a" {$NewNameNum=3}
"^b" {$NewNameNum=1}
"^c" {$NewNameNum=9}
"^d" {$NewNameNum=2}
default {Rename-Item $_ "$NewNameNum.$_"}
}
Then if you need it all in one line you remove new lines and replace them with semicolons.
powershell 'Switch(GCI C:\Temp){"^a" {$NewNameNum=3};"^b" {$NewNameNum=1};"^c" {$NewNameNum=9};"^d" {$NewNameNum=2};default {Rename-Item $_ "$NewNameNum.$_"}}'

Q: Powershell - read and report special characters from file

I've got a huge directory listing of files, and I need to see what special characters exist in the file names - specifically nonstandard characters like you'd get using ALT codes.
I can export a directory listing to a file easily enough with:
get-childitem -path D:\files\ -File -Recurse >output.txt
What I need to do however, is pull out the special characters, and only the special characters from the text file. The only way I can think to easily quantify everything "special" (since there are a ton of possibilities in the that character set) would be to compare the text against a list of characters I'd want to keep, stored in a joined variable (a-z, 0-9, etc)
I can't quite figure out how to pull out the "good" characters, leaving only the special ones. Any ideas on where to start?
I take "special" characters to be anything that falls outside US ASCII.
That basically means any character with a numerical value of 128 or more, easy to inspect in a Where-Object filter:
Get-ChildItem -File -Recurse |Where-Object {
$_.Name.ToCharArray() -gt 127
}
This will return all files containing "special" characters in their name.
If you want to extract the special characters themselves, per file, use ForEach-Object:
Get-ChildItem -File -Recurse |ForEach-Object {
if(($Specials = $_.Name.ToCharArray() -gt 127)){
New-Object psobject -Property #{File=$_.FullName;Specials=$(-join $Specials)}
}
}
Look at piping your results to Select-String. With Select-String you can specify a list of regex values to search for.

Rename Files with Index(Excel)

Anyone have any ideas on how to rename files by finding an association with an index file?
I have a file/folder structure like the following:
Folder name = "Doe, John EO11-123"
Several files under this folder
The index file(MS Excel) has several columns. It contains the names in 2 columns(First and Last). It also has a column containing the number EO11-123.
What I would like to do is write maybe a script to look at the folder names in a directory, compare/find an associated value in the index file(like that number EO11-123) and then rename all the files under the folder using a 4th column value in the index.
So,
Folder name = "Doe, John EO11-123", index column1 contains same value "EO11-123", use column2 value "111111_000000" and rename all the files under that directory folder to "111111_000000_0", "111111_000000_1", "111111_000000_2" and so on.
This possible with powershell or vbscript?
Ok, I'll answer your questions in your comment first. Importing the data into PowerShell allows you to make an array in powershell that you can match against, or better yet make a HashTable to reference for your renaming purposes. I'll get into that later, but it's way better than trying to have PowerShell talk to Excel and use Excel's search functions because this way it's all in PowerShell and there's no third party application dependencies. As for importing, that script is a function that you can load into your current session, so you run that function and it will automatically take care of the import for you (it opens Excel, then opens the XLS(x) file, saves it as a temp CSV file, closes Excel, imports that CSV file into PowerShell, and then deletes the temp file).
Now, you did not state what your XLS file looks like, so I'm going to assume it's got a header row, and looks something like this:
FirstName | Last Name | Identifier | FileCode
Joe | Shmoe | XA22-573 | JS573
John | Doe | EO11-123 | JD123
If that's not your format, you'll need to either adapt my code, or your file, or both.
So, how do we do this? First, download, save, and if needed unblock the script to Import-XLS. Then we will dot source that file to load the function into the current PowerShell session. Once we have the function we will run it and assign the results to a variable. Then we can make an empty hashtable, and for each record in the imported array create an entry in the hashtable where the 'Identifier' property (in your example above that would be the one that has the value "EO11-123" in it), make that the Key, then make the entire record the value. So, so far we have this:
#Load function into current session
. C:\Path\To\Import-XLS.ps1
$RefArray = Import-XLS C:\Path\To\file.xls
$RefHash = #{}
$RefArray | ForEach( $RefHash.Add($_.Identifier, $_)}
Now you should be able to reference the identifier to access any of the properties for the associated record such as:
PS C:\> $RefHash['EO11-123'].FileCode
JD123
Now, we just need to extract that name from the folder, and rename all the files in it. Pretty straight forward from here.
Get-ChildItem c:\Path\to\Folders -directory | Where{$_.Name -match "(?<= )(\S+)$"}|
ForEach{
$Files = Get-ChildItem $_.FullName
$NewName = $RefHash['$($Matches[1])'].FileCode
For($i = 1;$i -lt $files.count;$i++){
$Files[$i] | Rename-Item -New "$NewName_$i"
}
}
Edit: Ok, let's break down the rename process here. It is a lot of piping here, so I'll try and take it step by step. First off we have Get-ChildItem that gets a list of folders for the path you specify. That part's straight forward enough. Then it pipes to a Where statement, that filters the results checking each one's name to see if it matches the Regular Expression "(?<= )(\S+)$". If you are unfamiliar with how regular expressions work you can see a fairly good breakdown of it at https://regex101.com/r/zW8sW1/1. What that does is matches any folders that have more than one "word" in the name, and captures the last "word". It saves that in the automatic variable $Matches, and since it captured text, that gets assigned to $Matches[1]. Now the code breaks down here because your CSV isn't laid out like I had assumed, and you want the files named differently. We'll have to make some adjustments on the fly.
So, those folder that pass the filter will get piped into a ForEach loop (which I had a typo in previously and had a ( instead of {, that's fixed now). So for each of those folders it starts off by getting a list of files within that folder and assigning them to the variable $Files. It also sets up the $NewName variable, but since you don't have a column in your CSV named 'FileCode' that line won't work for you. It uses the $Matches automatic variable that I mentioned earlier to reference the hashtable that we setup with all of the Identifier codes, and then looks at a property of that specific record to setup the new name to assign to files. Since what you want and what I assumed are different, and your CSV has different properties we'll re-work both the previous Where statement, and this line a little bit. Here's how that bit of the script will now read:
Get-ChildItem c:\Path\to\Folders -directory | Where{$_.Name -match "^(.+?), .*? (\S+)$"}|
ForEach{
$Files = Get-ChildItem $_.FullName
$NewName = $Matches[2] + "_" + $Matches[1]
That now matches the folder name in the Where statement and captures 2 things. The first thing it grabs is everything at the beginning of the name before the comma. Then it skips everything until it gets tho the last piece of text at the end of the name and captures everything after the last space. New breakdown on RegEx101: https://regex101.com/r/zW8sW1/2
So you want the ID_LName, which can be gotten from the folder name, there's really no need to even use your CSV file at this point I don't think. We build the new name of the files based off the automatic $Matches variable using the second capture group and the first capture group and putting an underscore between them. Then we just iterate through the files with a For loop basing it off how many files were found. So we start with the first file in the array $Files (record 0), add that to the $NewName with an underscore, and use that to rename the file.