PowerShell - Rename files with number prefix [duplicate] - windows

How can I batch rename files in powershell using the following code:
$nr=1;Get-ChildItem -Filter *.jpg |
Rename-Item -Newname {"PPPPPPP_{0:d3}.jpg" -f $global:nr++}
where PPPPPPP is the name of parent folder containing these files.
Expected Output :
PPPPPPP_001.jpg
PPPPPPP_002.jpg
PPPPPPP_003.jpg
Files are located in C:\USER\MAIN\BLABLABLA\PPPPPPP folder.

Get the parent directory's name via $_.Directory.Name inside the script block.
Use Get-Variable to obtain a reference to the $nr sequence-number variable in the caller's scope, so you can modify its value directly (via .Value), which is preferable to using scope modifier $global: (-Scope 1 could be added to explicitly target the parent scope, but it isn't strictly necessary and omitted for brevity):
$nr = 1
Get-ChildItem -Filter *.jpg | Rename-Item -Newname {
'{0}_{1:d3}.jpg' -f $_.Directory.Name, (Get-Variable nr).Value++
} -WhatIf
-WhatIf previews the renaming operation; remove it, once you're confident that the command will perform as intended.
A more concise and efficient - but more obscure - alternative is to cast the $nr variable to [ref] so that you can modify its value directly in the caller's scope (via .Value).
$nr = 1
Get-ChildItem -Filter *.jpg | Rename-Item -Newname {
'{0}_{1:d3}.jpg' -f $_.Directory.Name, ([ref] $nr).Value++
} -WhatIf
Finally, another alternative is to use an aux. hashtable
$nr = #{ Value = 1 }
Get-ChildItem -Filter *.jpg | Rename-Item -Newname {
'{0}_{1:d3}.jpg' -f $_.Directory.Name, $nr.Value++
} -WhatIf
The following section explains these techniques.
Optional reading: Modifying the caller's variables in a delay-bind script block or calculated property:
The reason you couldn't just use $nr++ in your script block in order to increment the sequence number directly is:
Delay-bind script blocks (such as the one passed to Rename-Item -NewName) and script blocks in calculated properties run in a child scope.
Contrast this with script blocks passed to Where-Object and ForEach-Object, which run directly in the caller's scope.
It is unclear whether that difference in behavior is intentional.
Therefore, attempting to modify the caller's variables instead creates a block-local variable that goes out of scope in every iteration, so that the next iteration again sees the original value:
As an aside: A proposed future enhancement would obviate the need to maintain sequence numbers manually, via the introduction of an automatic $PSIndex variable that reflects the sequence number of the current pipeline object: see GitHub issue #13772.
Using a calculated property as an example:
PS> $nr = 1; 1..2 | Select-Object { '#' + $nr++ }
'#' + $nr++
-------------
#1
#1 # !! the *caller's* $nr was NOT incremented
While you can use a scope modifier such as $global: or $script: to explicitly reference a variable in a parent scope, these are absolute scope references that may not work as intended: Case in point: if you move your code into a script, $global:nr no longer refers to the variable created with $nr = 1.
Quick aside: Creating global variables should generally be avoided, given that they linger in the current session, even after a script exits.
The robust approach is to use a Get-Variable -Scope 1 call to robustly refer to the immediate parent scope:
PS> $nr = 1; 1..2 | Select-Object { '#' + (Get-Variable -Scope 1 nr).Value++ }
'#' + (Get-Variable -Scope 1 nr).Value++
------------------------------------------
#1
#2 # OK - $nr in the caller's scope was incremented
While this technique is robust, the cmdlet call introduces overhead, and it is a bit verbose, but:
you may omit the -Scope argument for brevity.
alternatively, you can improve the efficiency as follows:
$nr = 1; $nrVar = Get-Variable nr
1..2 | Select-Object { '#' + $nrVar.Value++ }
Using the [ref] type offers a more concise alternative, though the solution is a bit obscure:
PS> $nr = 1; 1..2 | Select-Object { '#' + ([ref] $nr).Value++ }
'#' + ([ref] $nr).Value++
---------------------------
#1
#2 # OK - $nr in the caller's scope was incremented
Casting a variable to [ref] returns an object whose .Value property can access - and modify - that variable's value. Note that since $nr isn't being assigned to at that point, it is indeed the caller's $nr variable that is referenced.
If you don't mind using an aux. hashtable, you can take advantage of the fact that a hashtable is a .NET reference type, which means that the child scope in which the delay-bind script block runs sees the very same object as the caller's scope, and modifying a property (entry) of this object therefore persists across calls:
PS> $nr = #{ Value = 1 }; 1..2 | Select-Object { '#' + $nr.Value++ }
'#' + $nr.Value++
---------------------------
#1
#2 # OK - $nr in the caller's scope was incremented

EDIT: modified due to mklement0s hint.
To get the parent name, use .Directory.Name as another parameter for the format operator
$nr=1
Get-ChildItem *.jpg -file|
Rename-Item -Newname {"{0}_{1:d3}.jpg" -f $_.Directory.Name,([ref]$nr).Value++} -whatIf
If the output looks OK remove the -WhatIf
This will only work while there are no overlapping ranges doing the rename, in that case you should probaply use a temporary extension.
Sample output on my German locale Windows
WhatIf: Ausführen des Vorgangs "Datei umbenennen" für das Ziel "Element: A:\test\150.jpg Ziel: A:\test\test_007.jpg".
WhatIf: Ausführen des Vorgangs "Datei umbenennen" für das Ziel "Element: A:\test\151.jpg Ziel: A:\test\test_008.jpg".
WhatIf: Ausführen des Vorgangs "Datei umbenennen" für das Ziel "Element: A:\test\152.jpg Ziel: A:\test\test_009.jpg".
WhatIf: Ausführen des Vorgangs "Datei umbenennen" für das Ziel "Element: A:\test\153.jpg Ziel: A:\test\test_010.jpg".
WhatIf: Ausführen des Vorgangs "Datei umbenennen" für das Ziel "Element: A:\test\154.jpg Ziel: A:\test\test_011.jpg".
WhatIf: Ausführen des Vorgangs "Datei umbenennen" für das Ziel "Element: A:\test\155.jpg Ziel: A:\test\test_012.jpg".

Another, slightly different way:
$nr=1;Get-ChildItem -Filter *.jpg |
Rename-Item -Newname {"$(split-path -leaf $_.Directory)_{0:d3}.jpg" -f
$global:nr++}

Related

How to rename/rearrange file names in Windows 11?

I had multiple photos saved in my computer with file names in First name_Middle Name_Last Name format. I would like to change it to Last Name, First Name, Middle Name.
For example:
From: One_Two_Three
To: Three, One, Two
I did a little research but I only found a replace-like method, but not rearranging the words.
Use the -replace regex operator:
PS ~> 'One_Two_Three' -replace '([^_]+)_([^_]+)_([^_]+)', '$3, $1, $2'
Three, One, Two
The pattern ([^_]+) will capture any sequence of non-_, and we can then "rearrange" each capture in the substitution pattern $3, $1, $2 ($1 refers to the group that captures One before the first _, etc.).
For renaming files, you'll want to reference the BaseName property on each file object:
Get-ChildItem path\to\folder\with\pictures -File -Filter *_*_*.* |Rename-Item -NewName {($_.BaseName -replace '([^_]+)_([^_]+)_([^_]+)', '$3, $1, $2') + $_.Extension}
Another method:
PS /tmp> Get-ChildItem -File -Filter *_*_*.* |
Rename-Item -WhatIf -NewName {
$w = $_.BaseName -split '_'; (($w[2], $w[0], $w[1]) -join ', ') + $_.Extension }
Output:
What if: Performing the operation "Rename File" on target "Item: /tmp/One_Two_Three.jpg Destination: /tmp/Three, One, Two.jpg".
Remove -WhatIf to do real renaming

Rename multiple files in a folder, add a line counts as prefix (Powershell)

I'd like to batch rename files in a folder, prefixing the each filename line counts and "_" into the new names.
example
a.txt
b.txt
c.txt
d.txt
to
1000_a.txt
32_b.txt
199_c.txt
8_d.txt
Bonus: pad the line counts with leading zeroes
to
1000_a.txt
0032_b.txt
0199_c.txt
0008_d.txt
what i tried:
from command that add suffix
Dir | Rename-Item -NewName { $_.basename + "_" + (Get-Content $_).length + "_" + $_.extension}
to
Dir | Rename-Item -NewName { (Get-Content $_).length + "_" + $_.basename + $_.extension}
but it give error
Rename-Item : The input to the script block for parameter 'NewName' failed. Cannot convert value "a" to type "System.Int32". Error: "Input string was not in a correct format."
Thanks
What you are doing is almost fine, the error comes from trying to concatenate an int with a string, PowerShell attempts type conversion of all elements to the type of the leftmost object in the operation:
The operation that PowerShell performs is determined by the Microsoft .NET type of the leftmost object in the operation. PowerShell tries to convert all the objects in the operation to the .NET type of the first object. If it succeeds in converting the objects, it performs the operation appropriate to the .NET type of the first object. If it fails to convert any of the objects, the operation fails.
Since the leftmost object in this case (the .Length property) is of the type int PowerShell attempts to convert the rest to int and it fails, for example:
PS /> 1 + 'A'
InvalidArgument: Cannot convert value "A" to type "System.Int32". Error: "Input string was not in a correct format."
This would be easily fixed by type casting the returned value to sring:
-NewName { [string](Get-Content $_).Length + "_" + $_.BaseName + $_.Extension }
Or for example using string formatting or the -f format operator:
-NewName { '{0}_{1}{2}' -f (Get-Content $_).Length, $_.BaseName, $_.Extension }
As for "the bonus", with string formatting see How do I control the number of integral digits?
In this case you can use {0:0000}, as an example:
(0..1000).ForEach({'{0:0000}' -f $_})
On the other hand, if you have many lengthy files, [System.IO.File]::ReadAllLines(...) is likely to be faster than Get-Content:
-NewName { '{0:0000}_{1}' -f [System.IO.File]::ReadAllLines($_).Length, $_.Name }
# OR
$io = [System.IO.File]
-NewName { '{0:0000}_{1}' -f $io::ReadAllLines($_).Length, $_.Name }

Powershell. Difference between piping commands and using Foreach-Object

Sorry if this question is already been answered, I could find similar questions but not the exact one I need to ask.
Let's take two examples:
1. Get-Process -name msedge,putty | Stop-Process
2. Get-Process -name msedge,putty | Foreach-Object {Stop-Process $_}
Both are doing the same operation. What about the methods used in each one? Are they the same in the sense that the first example just omits the Foreach-Object construction for the sake of code readability/aesthetics?
The first example requires the Cmdlet to support binding of the relevant parameters via the pipeline. In your case Stop-Process will bind the Process object from the pipeline to it's -InputObject parameter.
You can check that using get-help stop-process -Parameter * and see which parameters have "Accept pipeline input?" set to true.
In case a Cmdlet does not support the binding of the relevant parameters values you can wrap ForEach-Object around it, like you did in the second example. This way you use the automatic variable $_ to bind the current pipeline object (or information that you derive from it) "manually" to the corresponding parameter.
What approach should you use if a Cmdlet supports the binding of parameter values from the pipeline? That unfortunately depends. It is possible to write a Cmdlet that behaves differently, depending on how the parameter values are bound. Let me illustrate this point:
function Test-BindingFoo {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline)]
[string[]]
$InputParameter
)
begin {
Write-Host "[BEGIN]"
}
process {
foreach ($value in $InputParameter) {
Write-Host "The current value is: $value"
}
}
end {
Write-Host "[END]"
}
}
If you execute this Cmdlet using the pipeline binding the Begin block of the function is executed exactly once:
❯ "foo1", "foo2" | Test-BindingFoo
[BEGIN]
The current value is: foo1
The current value is: foo2
[END]
If you use ForEach-Object the Begin block is executed every time an object passes through the pipeline:
❯ "foo1", "foo2" | ForEach-Object { Test-BindingFoo $_ }
[BEGIN]
The current value is: foo1
[END]
[BEGIN]
The current value is: foo2
[END]
In well implemented Cmdlets the difference here should not matter. But I found it useful to be aware of what happens inside a Cmdlet when parameteres are passed in in the ways that we have discussed here.
You can do it this way too and use the kill method on the process object (operation statement):
Get-Process msedge,putty | Foreach-Object kill
# or
Get-Process msedge,putty | Foreach-Object -membername kill

How to rename files in Powershell with three digits and odd numbers?

Pretty novice to using powershell.
Trying to rename several .wav files that need to be in an odd sequential order.
I've found from another post from here how to rename multiple files in odd increments but can't figure out how to format them as "001_title.wav", 003_title.wav, 005_title.wav etc.
Stumped with trying to implement '{0:d3}'.
Should I be using that?
Any help would be appreciated!
Thanks.
$path = "\path\to\files"
$oddNumbersArray = 0..500 | % {$_ *2 +1}
$i = 0
Get-ChildItem $path -Filter *.wav | % {Rename-Item $_ -NewName ("$($oddNumbersArray[$i])_title.wav") '{0:d3}' -f $($oddNumbersArray[$i]) ;$i++}
Your -NewName expression should look like this:
... |Rename-Item -NewName { '{0:d3}_title.wav' -f $oddNumbersArray[$script:i++] }
The {} brackets ensure the pipeline binder re-calculates the value for each input item, and the script: scope modifier is required because we're writing to a variable in a parent scope.
FWIW you don't have to "pre-calculate" your odd numbers - you can increment your counter variable by 2 instead:
$path = "\path\to\files"
$oddCounter = 1
Get-ChildItem $path -Filter *.wav |Rename-Item -NewName { '{0:d3}_title.wav' -f $oddNumbersArray[($script:i += 2)] }

Breaking foreach loop in PowerShell v2

I'm having trouble terminating a foreach-object loop in PowerShell v2. For a rough idea of the task I'm trying to accomplish, here's the pseudo-code:
Read lists of host machines from a text file
For each host in the text file get Win32_Product (filtered against an exclusion list),
convert output to html and save.
The reason for the script is that I've amassed a text file listing all applications included on standard client images, and would like to periodically scan hosts from another text file to see if there are any unauthorized, sketchy or otherwise unnecessary applications on the host machines.
The code does work in a rough sense, but the main issue I'm having is that the script will not terminate without manual intervention. I guess the component I'm missing here is to run the loop until some condition exists (ie. first line in the host file is encountered for the second time), then terminates the script. Although this is the method I've envisioned, I am always open to other logic, especially if its more efficient.
Here's the actual code:
Get-Content c:\path\to\testhostlist.txt | Foreach-Object {
Get-WmiObject Win32_Product |
Where-Object { $_.Name -f "'C:\path\to\testauthapplist.txt'" |
ConvertTo-Html name,vendor,version -title $name -body "<H2>Unauthorized Applications.</H2>"}} |
Set-Content c:\path\to\unauthapplisttest.html
I don't see how the first line of the host file (I infer you mean testhostlist.tx) would ever be encountered a second time, since you're only listing it once. This doesn't even seem to be an infinite loop that would need an exit condition. Foreach-Object doesn't repeat indefinitely.
It seems to me that the problem is not that the loop doesn't exit without a condition, it's that the syntax is invalid.
Where-Object filters the pipeline by passing only objects that meet a certain condition, but the scriptblock that follows doesn't perform a boolean test.
In fact, the content of the scriptblock doesn't appear valid in and of itself. -f is the format operator, and takes a format string as the left operand, but $_.Name is not a format string.
I'm going to take a guess here, based on your description, that the idea is to filter the results of Get-WmiObject Win32_Product for objects whose Name property isn't listed in testauthapplist.txt (I take it that's the "exclusion list" you're referring to). If so, this is the correct syntax:
Get-Content c:\path\to\testhostlist.txt | %{
Get-WmiObject Win32_Product | ?{
(Get-Content 'C:\path\to\testauthapplist.txt') -notcontains $_.Name
} | ConvertTo-Html name,vendor,version -title $name -body "<H2>Unauthorized Applications.</H2>"
} | Set-Content c:\path\to\unauthapplisttest.html
(Note that %{} and ?{} are just abbreviations for Foreach-Object and Where-Object, respectively.)
If i understood you correctly you are trying to stop your Script completely? If so did you try Break?
If you only want to skip a loop use continue
$hostlist = Get-Content c:\path\to\testhostlist.txt
$a = #()
Foreach($item in $hostlist)
{
$a += "<style>"
$a += "BODY{background-color:gray;}"
$a += "TABLE{margin: auto;border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}"
$a += "TH{border-width: 1px;padding: 4px;border-style: solid;border-color: black;background-color:yellow}"
$a += "TD{border-width: 1px;padding: 4px;border-style: solid;border-color: black;background-color:white}"
$a += "h2{color:#fff;}"
$a += "</style>"
Get-WmiObject Win32_Product | select name,vendor,version | sort name | ConvertTo-Html -head $a -body "<Center><H2>Unauthorized Applications.</H2></Center>" | Out-File c:\path\to\$item"-applist.html"
}

Resources