Нормализовать регистр имен файлов на основе шаблонов подстановочных знаков

Как я могу нормализовать регистр имен файлов на основе литеральных компонентов соответствующих шаблонов подстановочных знаков?

Рассмотрим следующие имена файлов:

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 за то, что вдохновил этот вопрос.

1 ответ

Примечание по терминологии: поскольку шаблон может использоваться для ссылки как на подстановочные выражения, так и на регулярные выражения, термин glob используется как однозначное сокращение для подстановочных выражений.


Простое, но ограниченное решение, основанное на разбиении строк

В частности, решение ниже ограничено шаблонами подстановки, которые содержат один * как единственный подстановочный метасимвол.

# 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   # ?
                                     '\\(\[.+?\])', '($1)') + # [...]
  '$'
}

# 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    
    }
  }
}
Другие вопросы по тегам