Powershell, rename-item doesn't work as expected - powershell

I have a bunch of jpg image files named in the following pattern:
0001-rand01_012.jpg
0002-rand03_034.jpg
I want to rename them by removing the first 5 characters to get the form:
rand01_012.jpg
etc..
I use the following command:
Get-ChildItem | Rename-Item -NewName {$_.name.Substring(5)}
When using this with -whatif flag i get the expected message saying:
Performing the operation "Rename File" on target "Item: C:\Users\xxxx\Documents\xx xx\temp2\0123-rand16_030.jpg Destination: C:\Users\
xxxx\Documents\xx xx\temp2\rand16_030.jpg".
But removing the whatif gives me errors of this type:
Rename-Item : The input to the script block for parameter 'NewName' failed. Exception calling "Substring" with "1" argument(s): "startIndex cannot be
larger than length of string.
followed by a whole bunch of:
Rename-Item : Cannot create a file when that file already exists.
The files themselves are renamed with random number of characters removed rather than 5 as was intended. So they have ended up like:
01.jpg
01.jpg
.
.
.
d14_001.jpg
etc.
I have used this command to rename such files in the past with success. The fact that I'm getting such random results is making me pull my hair out.

tl;dr
Make sure you only process the files of interest:
(Get-ChildItem -File [0-9][0-9][0-9][0-9]-*.jpg) |
Rename-Item -NewName {$_.name.Substring(5)} -WhatIf
The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.
In PowerShell [Core] 6+, placing (...) around Get-ChildItem is no longer technically necessary, but advisable.[1]
That way:
You rule out unrelated files up front.
Even if something goes wrong, you can correct the problem and run the command again to reprocess only the failed files, without affecting the previously renamed files.
The most likely reason for something going wrong is more than 1 input file resulting in the same filename after removing the 5 first char.
It sounds like you've mistakenly run the command repeatedly, so you've cut off 5 chars. multiple times:
0001-rand01_01.jpg -> rand01_01.jpg -> _01.jpg
Once a filename has fewer than 5 chars., you'll get the the startIndex-related error, because the [string] class's .Substring() method doesn't accept an index beyond the length of the string (try 'ab'.Substring(3)).
That said, since you're running Get-ChildItem without a filter and therefore return all (non-hidden) child items, you may be processing unrelated files ore even directories whose names are too short.
The Cannot create a file when that file already exists. errors are just follow-on errors that result from the script block that normally returns the new name effectively returning the empty string, so Rename-Item is somewhat obscurely complaining that you can't rename a file to its current name.
That said, you can even get Cannot create a file when that file already exists errors during the first run, namely if more than 1 input file with its first 5 chars. chopped off results in the same filename.
E.g., 0001-rand01_012.jpg and 0002-rand01_012.jpg would both be renamed to rand01_012.jpg, which fails once the first one has been renamed.
That is, for your command to work as intended, all filenames that result from dropping the first 5 chars. must be unique.
Here's an MCVE (Minimal, Complete, and Verifiable Example):
Setup:
# Create and change to temp dir.
Set-Location (mkdir temp)
# Create sample input files named 0001-rand01_01.jpg, 0002-rand01_02.jpg, ...
# Note how the suffixes after the first 5 char. must be *unique*.
1..9 | %{ $null > "000${_}-rand01_0${_}.jpg" }
1st run:
# No errors
> (Get-ChildItem -File) | Rename-Item -NewName { $_.Name.Substring(5) }
# Show new names
> Get-ChildItem | Select Name
Name
----
rand01_01.jpg
rand01_02.jpg
rand01_03.jpg
rand01_04.jpg
rand01_05.jpg
rand01_06.jpg
rand01_07.jpg
rand01_08.jpg
rand01_09.jpg
A 2nd run yields:
Name
----
1_01.jpg
1_02.jpg
1_03.jpg
1_04.jpg
1_05.jpg
1_06.jpg
1_07.jpg
1_08.jpg
1_09.jpg
At the time of the 3rd run, all the names are too short, and all you'll get is Rename-Item : Cannot create a file when that file already exists. errors.
[1] Enclosing Get-ChildItem in (...) ensures that the matching files are collected in an array, up front, before Rename-Item is invoked.
This explicitly prevents already-renamed files from getting re-enumerated by Get-ChildItem and thus interfering with the iteration. Explicit use of (...) is technically no longer necessary in PowerShell [Core] 6+ (it is necessary in Windows PowerShell (5.1-)), because Get-ChildItem is implemented in a way that always internally collects info about all files up front, across platforms, because it sorts them by name, which is inherently only possible after all names have been collected.
In light of that, whether you use (...) or not should functionally amount to the same, although using (...) is advisable, because it doesn't rely on what amounts to an implementation detail (the documentation doesn't mention how the outputs are ordered).

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 - How to replace dots on files exept the extension

I have a question about the replacement of dots on multiple files, i have the next code:
Dir | Rename-item -NewName{ $_.basename.replace(".","-") + $_.extension }
This code works, but i have folders with dots and the problem is when i run the code, the folders repeat the words after the point like a "file extension":
like this:
How can i resolve this problem, i need just replace the dot on folder name with another word or space or everything i like and the files on the folder just replace before the extension.
thanks!
Unfortunately, the .BaseName ETS (Extended Type System) property that PowerShell adds to System.IO.DirectoryInfo instances, i.e. directories, by - unfortunate - design, unconditionally reports the directory name as-is.
It is only for System.IO.FileInfo instances, i.e. files, that .BaseName strips the extension, i.e., the last .-separated component.[1]
You can work around the problem by calling the System.IO.Path.GetFileNameWithoutExtension .NET method, which does not make this distinction (similarly, the type-native .Extension property doesn't make this distinction either, so it can be used as-is).
Get-ChildItem | Rename-Item -NewName {
[IO.Path]::GetFileNameWithoutExtension($_.Name).Replace('.', '-') + $_.Extension
}
[1] You can verify this as follows:
(Get-TypeData System.IO.DirectoryInfo).Members.BaseName vs.
(Get-TypeData System.IO.FileInfo).Members.BaseName

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.$_"}}'

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.

Bulk renaming of files in PowerShell with sequential numeric suffixes

I have 14,000 pictures sorted into files by year and month but taken with multiple cameras and I want the file name to reflect the date taken. For example October 16, 1998 pictures are in a folder called 1998\10 October\19981016.
I want the all the pictures to be named 19981016_0001 19981016_0002 etc. I tried multiple suggestions but it hasn't worked.
I can get to the point where it lists the folder I want to change but I'm unable to actually change it. All of my pictures are .jpg.
Attempts
I created a temp file of copies in case I messed it up. I started by typing cd "C:\Documents and Settings\Brooke LastName\Desktop\Temp" then after successfully getting my file to load I used a formula I found on this forum.
ls *jpg | Foreach {$i=1} {Rename-Item _ -NewName ("$($.19981016){0:00000000#} .jpg" -f $i++) -whatif}
The error I got said
Unexpected token ' .19981016' in expression or statement.
At line:1 char:12 + $.19981016 <<<<
The error repeated several times.
I found several formulas on the web but most created files that would number with parentheses for example vacation (1).jpg. I want a four digit counter after an underscore at the end of my date, e.g. 19981016_0001
The syntax is way off. A few issues:
I'm not sure what $($.19981016) was intended to produce, but $( ) evaluates the expression in the parentheses and interpolates the result into the string, and you're getting the error because $.19981016is not a valid expression. The error would be repeated for each .jpg file in the directory.
{0:00000000#} in a formatted string will create a zero-padded number of 9 digits, but a shorter way to do that is {0:D9}. However, I thought you wanted the zero-padded number to have 4 digits, so that should be {0:0000#} or {0:D4}.
I'm not sure what Foreach {$i=1} { [...] is supposed to do. The keyword foreach can mean a foreach loop, or it can be shorthand for Foreach-Object. In this context, receiving input from a pipeline, it's necessarily the latter, but this syntax is incorrect either way.
This will do what you want, if I understand the description correctly:
$i = 1
Get-ChildItem *.jpg | %{Rename-Item $_ -NewName ('19981016_{0:D4}.jpg' -f $i++)}
The filenames will be 19981016_0001.jpg, 19981016_0002.jpg, 19981016_0003.jpg, etc.
A few notes:
You said that you want filenames like 19981016_0001, but I'm assuming you want to keep the .jpg extension.
You need to initialize $i, otherwise it will start from 0 if it's not yet defined, or worse yet, from some other number if $i was used previously in the same PowerShell session. For example, if you have 1,432 .jpg files in the directory, and you run the command first with -WhatIf, and then run it for real, the numbers will start from 1432.
$i++ increments $i by 1. However, if you're using it as a value, it increments after the value is read; that's why if $i is undefined, it will start from 0.
Get-ChildItem is the same as ls. I used the native PowerShell name, but they're interchangeable (ls, dir, and gci are all aliases for Get-ChildItem).
%{ } is shorthand for Foreach-Object
This comes close to the original question. You can actually pass a script block array to foreach-object -process, as documented (barely). I only know this after Bruce Payette's book. The containing folder name is "19981016". The source filenames may not go in the order you expect.
ls *jpg |
Foreach {$i=1} {Rename-Item $_ -NewName ("$($_.directory.name)_{0:000#}.jpg" -f
$i++) -whatif}
What if: Performing the operation "Rename File" on target "Item: C:\users\js\19981016\file1.jpg Destination: C:\users\js\19981016\19981016_0001.jpg".
What if: Performing the operation "Rename File" on target "Item: C:\users\js\19981016\file10.jpg Destination: C:\users\js\19981016\19981016_0002.jpg".
What if: Performing the operation "Rename File" on target "Item: C:\users\js\19981016\file2.jpg Destination: C:\users\js\19981016\19981016_0003.jpg".
$path="C:\..."
$files=Get-ChildItem $path -Filter *.gif
Foreach($obj in $files){
$pathWithFilename=$path + $obj.Name;
$newFilename= "file_"+$obj.Name;
Rename-Item -Path $pathWithFilename -NewName $name
}
You can use this code blocks in powershell ISE. Example output like this :
Old files :
1232.gif
asd.gif
After run code :
file_1232.gif
file_asd.gif