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. 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 ' |-|\.' } 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. 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.
    – Theo
    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 :) 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
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. 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
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.