How to recursively append to file name in powershell? - powershell

I have multiple .txt files in folders/their sub-folders.
I want to append _old to their file names.
I tried:
Get-ChildItem -Recurse | Rename-Item -NewName {$_.name -replace '.txt','_old.txt' }
This results in:
Some files get updated correctly
Some files get updated incorrectly - they get _old twice - example: .._old_old.txt
There are few errors: Rename-Item : Source and destination path must be different.

To prevent already renamed files from accidentally reentering the file enumeration and therefore getting renamed multiple times, enclose your Get-ChildItem call in (), the grouping operator, which ensures that all output is collected first[1], before sending the results through the pipeline:
(Get-ChildItem -Recurse) |
Rename-Item -NewName { $_.name -replace '\.txt$', '_old.txt' }
Note that I've used \.txt$ as the regex[2], so as to ensure that only a literal . (\.) followed by string txt at the end ($) of the file name is matched, so as to prevent false positives (e.g., a file named Atxt.csv or even a directory named AtxtB would accidentally match your original regex).
Note: The need to collect all Get-ChildItem output first arises from how the PowerShell pipeline fundamentally works: objects are (by default) sent to the pipeline one by one, and processed by a receiving command as they're being received. This means that, without (...) around Get-ChildItem, Rename-Item starts renaming files before Get-ChildItem has finished enumerating files, which causes problems. See this answer for more information about how the PowerShell pipeline works.
Tip of the hat to Matthew for suggesting inclusion of this information.
However, I suggest optimizing your command as follows:
(Get-ChildItem -Recurse -File -Filter *.txt) |
Rename-Item -NewName { $_.BaseName + '_old' + $_.Extension }
-File limits the the output to files (doesn't also return directories).
-Filter is the fastest way to limit results to a given wildcard pattern.
$_.BaseName + '_old' + $_.Extension uses simple string concatenation via the sub-components of a file name.
An alternative is to stick with -replace:
$_.Name -replace '\.[^.]+$', '_old$&'
Note that if you wanted to run this repeatedly and needed to exclude files renamed in a previous run, add -Exclude *_old.txt to the Get-ChildItem call.
[1] Due to a change in how Get-ChildItem is implemented in PowerShell [Core] 6+ (it now internally sorts the results, which invariably requires collecting them all first), the (...) enclosure is no longer strictly necessary, but this could be considered an implementation detail, so for conceptual clarity it's better to continue to use (...).
[2] PowerShell's -replace operator operates on regexes (regular expressions); it doesn't perform literal substring searches the way that the [string] type's .Replace() method does.

The below command will return ALL files from the current folder and sub-folders within the current directory the command is executed from.
Get-ChildItem -Recurse
Because of this you are also re-turning all the files you have already updated to have the _old suffix.
What you need to do is use the -Include -Exclude paramters of the Get-Childitem Cmdlet in order to ignore files that already have the _old suffix, and meet your include criteria, for example.
Get-ChildItem -Recure -Include "*.txt" -Exclude "*_old"
Then pipe the results into your re-name item command
Get-ChildItem cmdlet explanation can be found here.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-childitem?view=powershell-7

Related

Find and Copy files with wildcard in filename [closed]

Closed. This question is not reproducible or was caused by typos. It is not currently accepting answers.
This question was caused by a typo or a problem that can no longer be reproduced. While similar questions may be on-topic here, this one was resolved in a way less likely to help future readers.
Closed 2 months ago.
Improve this question
I have a big folder of files and I would like to find and copy specific files. There are no subfolders - all files are in the same path.
The problem is - a part of the file is always different and it is impossible to guess, so using the wildcard is required.
Here's a script:
dir K:\ALL_FILES /s/b|for /f %%i in ('find "First File To Copy.*.jpg"') do copy "%%i" k:\COPIED_FILES
dir K:\ALL_FILES /s/b|for /f %%i in ('find "Second File to be copied.*.jpg"') do copy "%%i" k:\COPIED_FILES
The above script works if I replace * with the real part of the file name.
I would like to do it in a batch file, but may be it would be easier to do with Powershell?
The fact that all your files are in a single folder makes this task easier, since most PowerShell cmdlets that take a -Path parameter accept string arrays for the parameter. So if you execute the cmdlet from the directory containing your files, the -Path array can contain your wildcard match patterns. This allows all the filtering to be done by the FileSystem Provider, which is faster than using -Include or -Exclude parameters or piping files to Where-Object.
$SourceDir = 'k:\ALLFILES`
$DestDir = 'k:\COPIED_FILES'
$MatchPatterns = #('First File To Copy.*.jpg','Second File To Copy.*.jpg')
Set-Location $SourceDir
Copy-Item -Path $MatchPattterns -Destination $DestDir
If you're just working interactively at the console, you can reduce the typing burden using aliases, positional parameters, and literal parameter values:
$MatchPatterns = #('First File To Copy.*.jpg','Second File To Copy.*.jpg')
sl k:\ALLFILES
copy $MatchPattterns k:\COPIED_FILES
And if you have a larger set of match patterns, a multiline Here-String makes for easier creation/editing:
$MatchPatterns = #'
First File To Copy.*.jpg
Second File To Copy.*.jpg
...
Tenth File to Copy.*.jpg
' -split "`n"
Documentation:
Copy-Item
Files in Subfilders: Recursive copy vs. Recursive file selection
While Copy-Item has the option of a -Recurse parameter, it doesn't recurse the file selection criteria through subfolders, instead, when the target of the copy command is a directory, it controls whether or not the contents of the directory is copied along with the folder itself.
If you want to select files from a folder and its subfolders, you want to select the files using Get-ChildItem with the -Recurse parameter, and then pipe the selected files for whatever further processing you choose, be it copy, move, or whatever. A simple example, selecting all text files in a folder and its subfolders, would look like this:
Get-ChildItem -Path 'C:\FolderPath' -Filter *.txt -Recurse
And while the options available to Get-ChildItem are sufficient for some scenarios, multiple criteria or matching more precise than wildcards require more advanced filtering. For those situations, you pipe the output of Get-ChildItem to Where-Object -- the "Swiss Army Knife" of object filtering in PowerShell. It can use any of the comparison operators available in PowerShell to test the properties of an object, or evaluate any script block that returns a boolean.
So, if your example contained subfolders with files you wanted to copy, you could use: (filename match patterns simplified for readability)
$SourceDir = 'k:\ALLFILES`
$DestDir = 'k:\COPIED_FILES'
Get-ChildItem -Path $SourceDir -Filter *.jpg -Recurse |
Where-Object { ($_.Name -like 'ABC.*.jpg') -or ($_.Name -like 'DEF.*.jpg')} |
Copy-Item -Destination $DestDir
And while the above Where-Object scriptblock does the trick for matching two different patterns, it could quickly become cumbersome for more than a few patterns. Using a regular expression for matching is more concise and more efficient. A regex comparison version of the above would be:
Get-ChildItem -Path $SourceDir -Filter *.jpg -Recurse |
Where-Object Name -match '(ABC|DEF)\..*\.jpg' |
Copy-Item -Destination $DestDir
You can see that the regex could be easily expanded to include more literal prefixes before becoming unwieldy. And of course, an even lengthier regex could be assigned to a string variable prior to the operation and the variable used in the operation itself.

Find and Rename large quantities of files

I am using PowerShell to find, move, and rename a large amount of audit files. These files are in a shared folder with hundreds of gigabytes of extra junk. Manually clicking and dragging would take hours or even days as they are in many nested folders.
All files are currently named the same (audit.log, or audit1.log if there is a second log in the same folder). I need to find those files, copy them to a central location and rename them so they don't overwrite one another (not necessarily in that order).
I am not a programmer by any standard. This is what I have tried so far based on this website:
cd "H:\Flights\SCP\Log Analysis\1st Quarter"
Get-ChildItem -Filter "audit*.log" -Recurse `
| Rename-Item -NewName {$_.Name -replace 'audit', "$_.Fullname"} -WhatIf `
| Move-Item -Destination "H:\Flights\SCP\Log Analysis\Audit logs" -WhatIf
I use -WhatIf to make sure I do not make a mistake since I cannot overwrite the files. My original line of thought was to simply replace the word audit with the file path, but any reasonable method to rename the files in a way which will not overwrite will be helpful.
Theo and Mathias R. Jessen have provided all the crucial pointers in comments:
Rename-Item only accepts a mere name as a -NewName argument.
Move-Item can perform both moving and renaming in a single operation.
Delay-bind script blocks ({ ... }) can be passed to both Rename-Item's -NewName and Move-Item's -Destination parameters, which enable deriving the target name / path dynamically, for each input object ($_)
To put it all together:
Get-ChildItem -Filter audit*.log -Recurse |
Move-Item -Destination {
"H:\Flights\SCP\Log Analysis\Audit logs\$($_.FullName -replace '[:\\/]', '_')"
} -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 target directory of the move operation must already exist (-Force does not create it for you, it would only allow you to replace an existing file).
$_.FullName -replace '[:\\/]', '_' transforms the full path of the original file into something that can be used as a file name, by replacing :, \ (and /) characters with _.
The caveat is that with long paths you may run into the 256-characters-per-name limit
An alternative is to use an abstract, unique identifier of fixed length, which you can generate with the New-Guid cmdlet, as Mathias suggests.

Rename-Item : Source and destination path must be different error in Powershell

I am using Powershell and trying to return the child item of a directory, which happens to be a subdirectory, and then use the Rename-Item cmdlet to rename the subdirectory name to something else.
I feel like the following code should work:
Get-ChildItem "C:\Users\Admin\Desktop\mydirectory\subdirectory" | Rename-Item -NewName {$_.Name -replace 'test'}
But I am getting this error:
Rename-Item : Source and destination path must be different.
What am I missing here?
Thanks in advance!
Since you're using Get-ChildItem without limiting the result to files (via the -File switch), both files and directories can be among the output items.
While Rename-Item results in a quiet no-op if a file is being renamed to the same name that it currently has, trying the same on a directory results in the error you saw.
This surprising discrepancy is the subject of GitHub issue #14903.
This applies to all items whose name does not contain substring 'test', in which case the
-replace operation passes the input string through as-is.
If your intent is to rename files only, the solution is to simply add the -File switch:
Get-ChildItem -File "C:\Users\Admin\Desktop\mydirectory\subdirectory" |
Rename-Item -NewName { $_.Name -replace 'test' }
If directories are (also) targeted, as in your case, you need to explicitly filter out input items for which no actual renaming would occur:
Get-ChildItem -Directory -Filter *test* "C:\Users\Admin\Desktop\mydirectory\subdirectory" |
Rename-Item -NewName { $_.Name -replace 'test' }
-Filter *test* ensures that only subdirectories that contain the word 'test' are output, which guarantees that actual renaming occur (though note that the command would fail if a subdirectory's entire name were 'test', as that would make the script block return the empty string).
If you simply want to rename a single subdirectory to a fixed new name, you don't need a delay-bind script block at all:
# NOTE: Works only if only a SINGLE subdirectory is returned.
Get-ChildItem -Directory "C:\Users\Admin\Desktop\mydirectory\subdirectory" |
Rename-Item -NewName 'test'
If you have multiple subdirectories and you want incorporate a sequence number into the new names, you do again need a delay-bind script block:
$num = 0
Get-ChildItem -Directory "C:\Users\Admin\Desktop\mydirectory\subdirectory" |
Rename-Item -NewName { 'test' + ++(Get-Variable -Scope 1 num).Value } -WhatIf
This renames the subdirectories to test1, test2, ...
For an explanation of this technique (the need for a Get-Variable call), see this answer.
If you want to preview the renaming operations that would take place, you can add the -WhatIf common parameter to the Rename-Item call, which will show for each input file what it would be renamed to.
However, you have to infer from the output the cases when no actual renaming takes place, due to the delay-bind script block passed to -NewName returning the same name as before.
E.g., an input file named foo would not be renamed, because 'foo' -replace 'test' returns 'foo' unmodified, which with -WhatIf would show as follows (line breaks added for readability) - note how the target and the destination path are the same:
What if: Performing the operation "Rename File" on target "
Item: C:\path\to\foo
Destination: C:\path\to\foo
"

rename multiple folder to Uppercase not files using powershell

I am working on powershell where I have multiple folders i need to change all folders names into Uppercase not the files only folder.
I have tried the below code
Get-ChildItem -Path "C:\Users\Xyz\Desktop\sample" -Recurse | % {
if ($_.Name -cne $_.Name.ToUpper()) { ren $_.FullName $_.Name.ToUpper() }
}
But with this code it was changing only file name but I want to change only dir
for example
foldername (lowerCase)
abc
cab
dab
like this (UPPERCASE)
ABC
CAB
DAB
Thanks in advance
These tricks may not be obvious. How's this? Hmm, that didn't actually work. You can't rename folders to the same thing in upper case in powershell.
# doesn't work!
get-childitem -recurse -directory -path "C:\Users\Xyz\Desktop\sample" |
Rename-Item -NewName { $_.name.toupper() } -whatif
Sometimes calling cmd from powershell just works better. Try this first as a "whatif" to see if it does what you want. And if I really understand the question. All this does is echo strings. This command is just "pretend".
get-childitem -recurse -directory -path "C:\Users\Xyz\Desktop\sample" |
foreach { write-host cmd /c ren $_.fullname $_.name.toupper() }
And if that looks good, this actually does the rename. But maybe make a backup in case something goes wrong. Be able to undo the action.
get-childitem -recurse -directory -path "C:\Users\Xyz\Desktop\sample" |
foreach { cmd /c ren $_.fullname $_.name.toupper() }
To limit Get-ChildItem's output to directories only, use the -Directory switch (PSv3+; in PSv2, pipe to Where-Object { $_.PSIsContainer }
Apart from that, your solution should work, but doesn't due to a conceptual flaw in .NET's System.IO.DirectoryInfo.MoveTo() method (and also System.IO.Directory.Move() method), which PowerShell's Rename-Item cmdlet builds on, as of .NET Core 3.0 preview6 / .NET 4.8:
The method doesn't recognize case variations of directory name as a different name, and fails with Source and destination path must be different.
Curiously, files are not affected, as you've experienced.[1]
Of course, while NTFS is case-insensitive, it is also case-preserving, so it should be possible to rename foo to FOO, for instance.
The problem has been reported in this GitHub issue.
Workaround (PSv4+):
Note: js2010's helpful answer offers another, workaround, based on calling cmd.exe for each input folder to use its ren command. While it is conceptually simpler, the caveat is that this approach of creating a child process for every folder processed is inefficient and slow. That said, for occasional renaming operations that probably won't matter.
$path = 'C:\Users\Xyz\Desktop\sample'
Get-ChildItem -LiteralPath $path -Recurse -Directory |
Where-Object { $_.Name -cne $_.Name.ToUpper() } -PipelineVariable dir |
Rename-Item -PassThru -NewName { [IO.Path]::GetRandomFileName() } |
Rename-Item -NewName { $dir.Name.ToUpper() }
The workaround temporarily renames matching folders to a transient, randomly generated name ([IO.Path]::GetRandomFileName()) and then applies the all-uppercase version of the original name.
The input folder's original state and name are captured via variable $dir, which is "stashed" away for later use via the common -PipelineVariable parameter.
[1] Caveat: When you use Get-ChildItem or Get-Item to report a specific directory or file by its literal name, whatever case variation you specify is reported back, even though the true case variation as stored in the filesystem may differ (File Explorer, by contrast, always shows you the true case). To see the true case, useGet-ChildItem <parentDir> -Filter <name>or, as a quick workaround that may show additional items, however, append * to the name/path. In both cases, the name is treated as a wildcard pattern, and the result of the matching process reflects the true case.
How to double quotes folder names ? Some folders has spaces ...
write-host cmd /c ren $.fullname $.name.toupper()

How to use wildcards in Windows Powershell V2.0

I'm trying to do a simple copy with rename from the Powershell V2.0 command line as follows:
Copy-Item my_file.* my_file_old.*
I expect this to function the same as the DOS equivalent
copy my_file.* my_file_old.*
However, it's failing with the complaint "illegal characters in path". If I replace the wildcard * with a specific file number, e.g. 1, it works fine.
I guess I'm basically trying to replicate that DOS version but in Powershell, where I'd like to use the -Exclude parameter once I get this basic version sorted out.
What am I doing wrong? Some posts on another forum I found suggested wildcards were not supported in V2.0, but I can't believe that. That's like Microsoft saying "We just released a new version of Windows, but forgot to support keyboards and mice".
If you look at the help on Copy-Item it will tell you which parameters support wildcards and which do not. -Destination does not support wildcards:
-Destination
Specifies the path to the new location. To rename a copied item, include the new name in the value.
Required? false
Position? 2
Default value
Accept pipeline input? true (ByPropertyName)
Accept wildcard characters? false
You can do what you want it with a tiny bit of script:
Get-ChildItem my_file.* | Copy-Item -Dest {$_.basename + "_old" + $_.extension} -WhatIf
Remove the -WhatIf parameter when you are happy it will copy the files correctly.
Wildcards are definitely supported, assuming you use them properly. A wildcard in your destination is strange and in most situations not what you want. And copying a file (or group of files) onto itself (which is what you're doing here - when you don't specify parameter names for Copy-Item the first is the source and the second is the destination) is almost certainly not what you want.
copy-item myFile.* -destination DESTPATH
or
get-childitem -filter myFile.* | Copy-Item -destination DESTPATH
Here is an example of renaming a bunch of .jpg files to a sequential list of numbers [001.jpg, 002.jpg, etc]
get-childitem *.jpg | foreach-object -begin {$count=1} -process {rename-item -path $_.FullName -newname ('{0:000}.jpg' -f $count++)}
You could adapt this to meet your needs by adjusting the *.jpg and '{0:000}.jpg' parts