1

I've been trying to achieve a more consistent file naming scheme and chose PowerShell to help with the task. However, for some reason I can't get the script to replace multiple patterns with underscores.

$regex = [regex] '^\d{4}\d{2}\d{2}_' # $special = [regex] '\. - ' # prepend with date get-childitem -attributes !directory+!system | where-object { !($_.name -match $regex) } | rename-item -newname { (get-date $_.creationtime -format "yyyyMMdd_") + $_.basename + $_.extension } # make all letters lowercase get-childitem -attributes !directory+!system | %{ $newFilename = ($_.basename.tolower())+($_.extension.tolower()); move-item $_ $newFilename } # replace special chars w underscores get-childitem -attributes !directory+!system | where-object { $_.basename.contains(" ") -or $_.basename.contains("-") -or $_.basename.contains(" ") -or $_.basename.contains(".") } | rename-item -newname { $_.basename -replace "\s+-\s+", "_" -replace "-+", "_" -replace "\.+", "_" -replace "\s+", "_" + $_.extension } 

The first two blocks work fine, but the -replace statements get ignored, and nothing gets replaced with underscores. I've searched around here and on Google, but I couldn't find a fix that works.

I've also tried with this variation:

$_.basename.replace(".", "_") + $_.extension; $_.basename.replace(" ", "_") + $_.extension ... 

instead of regex, but that messes up file extensions when it gets to the "." part. It also only works on the first pattern ignoring the rest.

5
  • You just need a pair of well-placed parentheses. See my answer below. Commented Jan 27, 2020 at 4:39
  • 1
    Why are you testing for " " twice? Also, the series of $_basename.contains... can be replaced with Where-Object{ $_.basename -match ' |-|\.' } Commented Jan 27, 2020 at 4:50
  • I'm testing twice because I need to pay more attention. And yes, one of the other answers has already suggested the regex replacement. Good point. Commented Jan 27, 2020 at 5:24
  • We haven't heard from you.. Did any of the given answers solve your problem? If so, please consider accepting it by clicking ✓ on the left. This will help others with a similar question finding it more easily. Commented Feb 2, 2020 at 15:31
  • Sorry for that. I was actually contemplating whether to accept your answer or Keith's since you were both so helpful (and had somewhat similar solutions) and then I ended up forgetting to vote altogether :) Commented Feb 6, 2020 at 7:25

3 Answers 3

1

For the third block, you could do

# replace special chars w underscores Get-ChildItem -Attributes !directory+!system | Where-Object { $_.BaseName -match '[-+._\s]' } | ForEach-Object { $newName = '{0}{1}' -f ($_.BaseName -replace '[-+._\s]+', '_') , $_.Extension $_ | Rename-Item -NewName $newName -Force -WhatIf } 

Remove the -WhatIf switch if the console output shows the correct new file names.

Example:

20190126_this is a file - name that contains + - spaces and dots.pdf 

Becomes:

20190126_this_is_a_file_name_that_contains_spaces_and_dots.pdf 

Regex details:

[-+._\s] Match a single character present in the list below One of the characters “-+._” A whitespace character (spaces, tabs, line breaks, etc.) + Between one and unlimited times, as many times as possible, giving back as needed (greedy) 


Edit

While the above only shows the use of the regex -replace operator, the code could be simplified by combining your original three loops into just one:

Get-ChildItem -Attributes !directory+!system | ForEach-Object { # get the file's BaseName and if needed prefix the CreationTime to it $baseName = if ($_.BaseName -notmatch '^\d{8}_') { '{0:yyyyMMdd}_{1}' -f $_.CreationTime, $_.BaseName } else { $_.BaseName } # do a regex -replace on the base name and combine it with the file's extension $newName = '{0}{1}' -f ($baseName -replace '[-+._\s]+', '_') , $_.Extension # do the rename using the lowercase $newName $_ | Rename-Item -NewName $newName.ToLower() -Force -WhatIf } 

Edit 2

Inspired by Keith Miller's comment, using a scriptblock for the -NewName parameter, you can dispose of the ForEach-Object in the code above:

Get-ChildItem -Attributes !directory+!system | Rename-Item -NewName { # get the file's BaseName and if needed prefix the CreationTime to it $baseName = if ($_.BaseName -notmatch '^\d{8}_') { '{0:yyyyMMdd}_{1}' -f $_.CreationTime, $_.BaseName } else { $_.BaseName } # do a regex -replace on the base name and combine it with the file's extension $newName = '{0}{1}' -f ($baseName -replace '[-+._\s]+', '_') , $_.Extension # output the new name in lower case $newName.ToLower() } 
4
  • ah so I can get by with a single regex, nice! didn't think about that. side question: why does it still work if I get rid of the ForEach-Obect part? i.e., Get-ChildItem -Attributes !directory+!system | Where-Object { $_.BaseName -match '[-+._\s]' } | Rename-Item -NewName { '{0}{1}' -f ($_.BaseName -replace '[-+._\s]+', '_') , $_.Extension } Commented Jan 27, 2020 at 3:51
  • 1
  • Does this make the ForEach-Obect part redundant then, or is it still good practice to include it? Does Theo's version have a nested loop with the inner loop always looping once? Commented Jan 27, 2020 at 5:20
  • 1
    Not the way his code is written, because he's using a string rather than a scriptblock for the NewName parameter. For large data sets, avoiding ForEach and using a scriptblock for NewName should be faster. Interesting info here. You could probably put all your renaming logic in a defined scriptblock variable and then use something like gci -file -recurse | ren -NewName $MyScriptBlock Commented Jan 27, 2020 at 20:44
2

You could pipe get-childitem over to a foreach-object loop to get each file object and its property values. Stack some replace operators with a regex multi-match character expression, and use that to strip out and replace the unnecessary characters within the base file name part.

Essentially this will replace the dots, hyphens, and one or more white space with underscores, and then the stacked replace on top of that will replace two or more hyphens with just one hyphen.

$regex = [regex] '^\d{8}_'; Get-ChildItem -attributes !directory+!system | ?{!($_.name -match $regex)} | %{ Rename-Item -path $_.FullName -newname ((get-date $_.creationtime -format "yyyyMMdd_") + $_.basename + $_.extension).ToLower() }; Get-ChildItem -attributes !directory+!system | %{ If(($_.basename + $_.extension) -cmatch "[A-Z]"){Move-Item $_ ($_.basename + $_.extension).ToLower()} }; Get-ChildItem -attributes !directory+!system | %{ Rename-Item -Path $_.FullName -NewName "$(Split-Path $_.FullName)\$(($_.Basename -Replace "[.]|-|\s`+","_") -Replace "_{2,}", "_")$($_.extension)" }; 

Supporting Resources

1
  • Can I do this with -replace or some other way that uses regex? I want to be able to replace multiple spaces, dashes, fullstops and " - " (with 1 or more spaces but a single dash) with a single underscore. I was never intending to replace the plus sign, but I guess that's a good idea too. Commented Jan 26, 2020 at 10:29
1

EDIT: Simplified COde ---

$DateFilter = '^\d{8}_' $CharFilter = '[\.\s-]+' # 'dot'or one or more whitespace or '-' $Standarize_Name = { $Prefix = '' If ($_.name -notmatch $DateFilter) { $Prefix = (get-date $_.creationtime -format "yyyyMMdd_") } '{0}{1}{2}' -f $Prefix, ($_.BaseName.ToLower() -replace $CharFilter, '_'), $_.Extension } Get-ChildItem -Attributes !directory+!system | Rename-Item -NewName $Standarize_Name 

Everything is fine in your original code except you need a pair of parentheses. I took your Rename ScriptBlock and tested in a simple ForEach:

PS C:\...\keith>gi 'a b - c.d.txt' | %{$_.basename -replace "\s+-\s+", "_" -replace "-+", "_" -replace "\.+", "_" -replace "\s+", "_" + $_.extension} The -ireplace operator allows only two elements to follow it, not 3. At line:1 char:99 + ... \s+-\s+", "_" -replace "-+", "_" -replace "\.+", "_" -replace "\s+", ... + ~~~~~~~~ + CategoryInfo : InvalidOperation: (System.Object[]:Object[]) [], RuntimeException + FullyQualifiedErrorId : BadReplaceArgument 

It errored on the last -replace becasue it's reading + $_.extension as a "3rd element".

So the fix is to enclose the -replace chain in parentheses and then concatenate with $_.extension:

# replace special chars w underscores get-childitem -attributes !directory+!system | where-object { $_.basename -match '\s|-|\.') } | rename-item -newname { ($_.basename -replace "\s+-\s+", "_" -replace "-+", "_" -replace "\.+", "_" -replace "\s+", "_") + $_.extension } 

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.