基于通配符模式的大小写规范化文件名

Case-normalize filenames based on wildcard patterns

如何根据匹配通配符模式的文字部分对文件名进行大小写规范化?

考虑以下文件名:

ABC_1232.txt
abC_4321.Txt
qwerty_1232.cSv
QwErTY_4321.CsV

它们都匹配以下通配符模式:

QWERTY_*.csv
abc_*.TXT

请注意模式的文字组成部分(例如,QUERTY_.csv在大小写 中与上面列表中的匹配文件有何不同(例如,QwErTY.CsV).

我想重命名匹配的文件,以便 模式 的文字部分在文件名中使用 大小写 ;因此,结果名称应该是:

abc_1232.TXT
abc_4321.TXT
QWERTY_1232.csv
QWERTY_4321.csv

感谢 Vladimir Semashkin 启发了这个问题。

术语说明:由于 pattern 可用于指代通配符表达式和正则表达式,术语 glob 用作明确的 shorthand对于通配符表达式。


基于字符串拆分的简单但有限的解决方案

具体来说,下面的解决方案仅限于包含一个 * 作为唯一通配符元字符的通配符模式

# Sample input objects that emulate file-info objects
# as output by Get-ChildItem
$files =
    @{ Name = 'ABC_1232.txt' },
    @{ Name = 'abC_4321.TxT' },
    @{ Name = 'qwerty_1232.cSv' },
    @{ Name = 'QwErTY_4321.CsV' },
    @{ Name = 'Unrelated.CsV' }

# The wildcard patterns to match against.
$globs = 'QWERTY_*.csv', 'abc_*.TXT'

# Loop over all files.
# IRL, use Get-ChildItem in lieu of $files.
$files | ForEach-Object {    
  # Loop over all wildcard patterns
  foreach ($glob in $globs) {    
    if ($_.Name -like $glob) { # matching filename    
      # Split the glob into the prefix (the part before '*') and
      # the extension (suffix), (the part after '*').
      $prefix, $extension = $glob -split '\*'

      # Extract the specific middle part of the filename; the part that 
      # matched '*'
      $middle = $_.Name.Substring($prefix.Length, $_.Name.Length - $prefix.Length - $extension.Length)

      # This is where your Rename-Item call would go.
      #   $_ | Rename-Item -WhatIf -NewName ($prefix + $middle + $extension)
      # Note that if the filename already happens to be case-exact, 
      # Rename-Item is a quiet no-op.
      # For this demo, we simply output the new name.
      $prefix + $middle + $extension    
    }
  }
}

通用但更复杂的正则表达式解决方案

此解决方案要复杂得多,但应该适用于所有通配符表达式(只要不支持 `-转义)。

# Sample input objects that emulate file-info objects
# as output by Get-ChildItem
$files =
    @{ Name = 'ABC_1232.txt' },
    @{ Name = 'abC_4321.TxT' },
    @{ Name = 'qwerty_1232.cSv' },
    @{ Name = 'QwErTY_4321.CsV' },
    @{ Name = 'Unrelated.CsV' }

# The globs (wildcard patterns) to match against.
$globs = 'QWERTY_*.csv', 'abc_*.TXT'

# Translate the globs into regexes, with the non-literal parts enclosed in
# capture groups; note the addition of anchors ^ and $, given that globs
# match the entire input string.
# E.g., 'QWERTY_*.csv' -> '^QWERTY_(.*)\.csv$'
$regexes = foreach($glob in $globs) {
  '^' +
    ([regex]::Escape($glob) -replace '\\*', '(.*)' -replace  # *
                                     '\\?', '(.)' -replace   # ?
                                     '\(\[.+?\])', '()') + # [...]
  '$'
}

# Construct string templates from the globs that can be used with the -f
# operator to fill in the variable parts from each filename match.
# Each variable part is replaced with a {<n>} placeholder, starting with 0.
# E.g., 'QWERTY_*.csv' -> 'QWERTY_{0}.csv'
$templates = foreach($glob in $globs) {
  $iRef = [ref] 0
  [regex]::Replace(
    ($glob -replace '[{}]', '$&$&'), # escape literal '{' and '}' as '{{' and '}}' first
    '\*|\?|\[.+?\]', # wildcard metachars. / constructs
    { param($match) '{' + ($iRef.Value++) + '}' } # replace with {<n>} placeholders
  )
}

# Loop over all files.
# IRL, use Get-ChildItem in lieu of $files.
$files | ForEach-Object {

  # Loop over all wildcard patterns
  $i = -1
  foreach ($regex in $regexes) {
    ++$i
    # See if the filename matches
    if (($matchInfo = [regex]::Match($_.Name, $regex, 'IgnoreCase')).Success) {
      # Instantiate the template string with the capture-group values.
      # E.g., 'QWERTY_{0}.csv' -f '4321' 
      $newName = $templates[$i] -f ($matchInfo.Groups.Value | Select-Object -Skip 1)

      # This is where your Rename-Item call would go.
      #   $_ | Rename-Item -WhatIf -NewName $newName
      # Note that if the filename already happens to be case-exact, 
      # Rename-Item is a quiet no-op.
      # For this demo, we simply output the new name.
      $newName    
    }
  }
}