Как команда Windows RENAME интерпретирует подстановочные знаки?

Как команда Windows RENAME (REN) интерпретирует подстановочные знаки?

Встроенное средство HELP не помогает – оно не относится к подстановочным знакам вообще.

Электронная помощь Microsoft technet XP не намного лучше. Здесь все, что нужно сказать о подстановочных знаках:

«Вы можете использовать подстановочные знаки ( * и ? ) В параметре имени файла. Если вы используете подстановочные знаки в filename2, символы, представленные подстановочными знаками, будут идентичны соответствующим символам в имени файла1».

Не очень помогает – есть много способов интерпретировать этот оператор.

Мне удалось успешно использовать подстановочные знаки в параметре filename2 в некоторых случаях, но это всегда было проб и ошибок. Я не мог предвидеть, что работает, а что нет. Часто мне приходилось прибегать к написанию небольшого пакетного скрипта с циклом FOR, который анализирует каждое имя, чтобы я мог создавать каждое новое имя по мере необходимости. Не очень удобно.

Если бы я знал правила для обработки шаблонов, я полагал, что могу использовать команду RENAME более эффективно, не прибегая к пакету так часто. Разумеется, знание правил также принесло бы пользу развитию партии.

(Да – это случай, когда я отправляю парный вопрос и ответ. Я устал от незнания правил и решил экспериментировать сам по себе. Я полагаю, что многие другие могут быть заинтересованы в том, что я обнаружил)

  • Отображение и сохранение выходного файла Windows Batch
  • Запуск файла bat с помощью планировщика Windows
  • 4 Solutions collect form web for “Как команда Windows RENAME интерпретирует подстановочные знаки?”

    Эти правила были обнаружены после тщательного тестирования на машине Vista. Никаких тестов с юникодом в именах файлов не проводилось.

    RENAME требует 2 параметра – sourceMask, за которыми следует targetMask. И sourceMask, и targetMask могут содержать * и / или ? подстановочные знаки. Поведение подстановочных знаков слегка изменяется между масками источника и цели.

    Примечание. REN можно использовать для переименования папки, но при переименовании папки подстановочные знаки не допускаются ни в sourceMask, ни в targetMask. Если исходная маска соответствует хотя бы одному файлу, то файл (ы) будет переименован и папки будут проигнорированы. Если исходная маска соответствует только папкам, а не файлам, тогда возникает синтаксическая ошибка, если в исходной или целевой среде появляются подстановочные знаки. Если sourceMask ничего не соответствует, возникает ошибка «файл не найден».

    Кроме того, при переименовании файлов подстановочные знаки разрешены только в части имени файла sourceMask. Подстановочные знаки не допускаются на пути, ведущем к имени файла.

    sourceMask

    SourceMask работает как фильтр, чтобы определить, какие файлы переименованы. Подстановочные знаки работают здесь так же, как и с любой другой командой, которая фильтрует имена файлов.

    • ? – Соответствует любому 0 или 1 символу кроме . Этот шаблон является жадным – он всегда потребляет следующий символ, если он не является . Однако он не будет соответствовать ничему без сбоя, если по окончании имени или если следующий символ равен .

    • * – Соответствует любым 0 или более символам, включая . (С одним исключением ниже). Этот шаблон не жадный. Он будет соответствовать как можно меньше или меньше, чтобы обеспечить соответствие последующих символов.

    Все символы без подстановочных знаков должны соответствовать самим себе, с несколькими исключениями особых случаев.

    • . – Соответствует самому или может соответствовать концу имени (ничего), если осталось больше символов. (Примечание. Действительное имя Windows не может закончиться . )

    • {space} – Соответствует самому или может соответствовать концу имени (ничего), если больше символов осталось. (Примечание. Действительное имя Windows не может быть завершено {space} ).

    • *. В конце – Соответствует любым 0 или более символам кроме . Прекращение . Может быть любой комбинацией . И {space} до тех пор, пока последний символ в маске . Это единственное исключение, где * не просто соответствует любому набору символов.

    Вышеприведенные правила не так сложны. Но есть еще одно очень важное правило, которое делает ситуацию запутанной: SourceMask сравнивается как с длинным именем, так и с коротким именем 8.3 (если оно существует). Это последнее правило может сделать интерпретацию результатов очень сложной, потому что это не всегда очевидно, когда маска соответствует короткому имени.

    Можно использовать RegEdit для отключения генерации коротких имен 8.3 на томах NTFS, после чего интерпретация результатов маски файлов намного более прямолинейна. Любые короткие имена, которые были сгенерированы до отключения коротких имен, останутся.

    targetMask

    Примечание. Я не провел тщательного тестирования, но, похоже, эти же правила также работают для целевого имени комманды COPY

    TargetMask указывает новое имя. Он всегда применяется к полному длинному имени; TargetMask никогда не применяется к короткому имени 8.3, даже если sourceMask соответствует короткому имени 8.3.

    Присутствие или отсутствие подстановочных знаков в исходной маске не влияет на то, как шаблоны шаблонов обрабатываются в targetMask.

    В следующем обсуждении – c представляет любой символ, который не является * ? , Или .

    TargetMask обрабатывается с именем источника строго слева направо без обратного отслеживания.

    • c – продвигает позицию в пределах имени источника, если следующий символ не указан . И добавляет c к целевому имени. (Заменяет символ, который был в источнике с c , но никогда не заменяет . )

    • ? – Соответствует следующему символу из длинного имени источника и добавляет его к целевому имени до тех пор, пока следующий символ не будет . Если следующий символ . Или если в конце имени источника не добавляется символ, и текущая позиция в имени источника не изменяется.

    • * конце targetMask – добавляет все оставшиеся символы из источника в цель. Если уже в конце источника, то ничего не делает.

    • *c – Соответствует всем исходным символам из текущей позиции через последнее время c (чувствительное к регистру жадное соответствие) и добавляет согласованный набор символов к целевому имени. Если c не найден, добавляются все остальные символы из источника, за которым следует c Это единственная ситуация, о которой я знаю, где сопоставление шаблонов файлов Windows зависит от регистра.

    • *. – Соответствует всем исходным символам текущей позиции через последнее время . (Жадное совпадение) и добавляет согласованный набор символов к целевому имени. Если . Не найден, затем добавляются все остальные символы из источника, а затем .

    • *? – Добавляет все оставшиеся символы из источника в цель. Если уже в конце источника ничего не делает.

    • . Без * впереди – продвигает позицию в исходном состоянии через первое появление . Без копирования каких-либо символов и добавления . К целевому имени. Если . Не найден в источнике, затем продвигается к концу источника и добавляется . К целевому имени.

    После того, как targetMask будет исчерпан, любой трейлинг . И {space} обрезаны с конца конечного имени цели, потому что имена файлов Windows не могут закончиться . Или {space}

    Некоторые практические примеры

    Заменяйте символ в 1-й и 3-й позиции перед любым расширением (добавляет второй или третий символ, если он еще не существует)

     ren * A?Z* 1 -> AZ 12 -> A2Z 1.txt -> AZ.txt 12.txt -> A2Z.txt 123 -> A2Z 123.txt -> A2Z.txt 1234 -> A2Z4 1234.txt -> A2Z4.txt 

    Изменить (окончательное) расширение каждого файла

     ren * *.txt a -> a.txt b.dat -> b.txt cxy -> cxtxt 

    Добавить расширение для каждого файла

     ren * *?.bak a -> a.bak b.dat -> b.dat.bak cxy -> cxybak 

    Удалите любое дополнительное расширение после первоначального расширения. Обратите внимание, что это адекватно ? Необходимо использовать для сохранения полного имени и начального расширения.

     ren * ?????.????? a -> a ab -> ab abc -> ab part1.part2.part3 -> part1.part2 123456.123456.123456 -> 12345.12345 (note truncated name and extension because not enough `?` were used) 

    То же, что и выше, но отфильтровывайте файлы с начальным именем и / или расширением длиной более 5 символов, чтобы они не были усечены. (Очевидно, можно добавить дополнительный ? На обоих концах targetMask для сохранения имен и расширений длиной до 6 символов)

     ren ?????.?????.* ?????.????? a -> a ab -> ab abc -> ab part1.part2.part3 -> part1.part2 123456.123456.123456 (Not renamed because doesn't match sourceMask) 

    Измените символы после последнего _ в имени и попытайтесь сохранить расширение. (Не работает должным образом, если _ появляется в расширении)

     ren *_* *_NEW.* abcd_12345.txt -> abcd_NEW.txt abc_newt_1.dat -> abc_newt_NEW.dat abcdef.jpg (Not renamed because doesn't match sourceMask) abcd_123.a_b -> abcd_123.a_NEW (not desired, but no simple RENAME form will work in this case) 

    Любое имя может быть разбито на компоненты, которые ограничены . Символы могут быть добавлены или удалены только с конца каждого компонента. Символы не могут быть удалены или добавлены в начало или середину компонента, сохраняя остаток с помощью подстановочных знаков. Замены допускаются в любом месте.
    EDIT – этот пример исправлен. В более ранней версии была неверная информация о несогласованных исходных масках

     ren ??????.??????.?????? ?x.????999.*rForTheCourse part1.part2 -> px.part999.rForTheCourse part1.part2.part3 -> px.part999.parForTheCourse part1.part2.part3.part4 (Not renamed because doesn't match sourceMask) abc -> ax.b999.crForTheCourse abCarPart3BEER -> ax.b999.CarParForTheCourse 

    Если короткие имена включены, то sourceMask с по меньшей мере 8 ? Для имени и не менее 3 ? Для расширения будет соответствовать все файлы, потому что он всегда будет соответствовать короткому имени 8.3.

     ren ????????.??? ?x.????999.*rForTheCourse part1.part2.part3.part4 -> px.part999.part3.parForTheCourse 

    Полезная причуда / ошибка? Для удаления префиксов имен

    В этой записи SuperUser описано, как можно использовать набор косых черт ( / ) для удаления ведущих символов из имени файла. Для каждого символа, который нужно удалить, требуется одна косая черта. Я подтвердил поведение на компьютере с Windows 10.

     ren "abc-*.txt" "////*.txt" abc-123.txt --> 123.txt abc-HelloWorld.txt --> HelloWorld.txt 

    Этот метод работает только в том случае, если исходная и целевая маски заключены в двойные кавычки. Все следующие формы без необходимых кавычек терпят неудачу с этой ошибкой: The syntax of the command is incorrect

     REM - All of these forms fail with a syntax error. ren abc-*.txt "////*.txt" ren "abc-*.txt" ////*.txt ren abc-*.txt ////*.txt 

    Невозможно использовать / для удаления любых символов в середине или в конце имени файла. Он может удалять только лидирующие (префиксные) символы.

    Технически / не работает в качестве шаблона. Скорее, он выполняет простую замену символов, но затем после подстановки команда REN распознает, что / недействительна в имени файла, и лишает имя / косую черту с именем. REN дает синтаксическую ошибку, если она обнаруживает / в середине целевого имени.

    Возможная ошибка RENAME – одна команда может дважды переименовать один и тот же файл!

    Начиная с пустой тестовой папки:

     C:\test>copy nul 123456789.123 1 file(s) copied. C:\test>dir /x Volume in drive C is OS Volume Serial Number is EE2C-5A11 Directory of C:\test 09/15/2012 07:42 PM <DIR> . 09/15/2012 07:42 PM <DIR> .. 09/15/2012 07:42 PM 0 123456~1.123 123456789.123 1 File(s) 0 bytes 2 Dir(s) 327,237,562,368 bytes free C:\test>ren *1* 2*3.?x C:\test>dir /x Volume in drive C is OS Volume Serial Number is EE2C-5A11 Directory of C:\test 09/15/2012 07:42 PM <DIR> . 09/15/2012 07:42 PM <DIR> .. 09/15/2012 07:42 PM 0 223456~1.XX 223456789.123.xx 1 File(s) 0 bytes 2 Dir(s) 327,237,562,368 bytes free REM Expected result = 223456789.123.x 

    Я считаю, что sourceMask *1* сначала соответствует длинному имени файла, и файл переименовывается в ожидаемый результат 223456789.123.x . Затем RENAME продолжает искать больше файлов для обработки и поиска вновь названного файла через новое короткое имя 223456~1.X Затем файл снова переименовывается, давая окончательный результат 223456789.123.xx .

    Если я отключу генерацию имени 8.3, RENAME дает ожидаемый результат.

    Я не полностью разработал все условия запуска, которые должны существовать, чтобы вызвать это странное поведение. Я был обеспокоен тем, что возможно создать бесконечное рекурсивное RENAME, но я никогда не смог его вызвать.

    Я считаю, что все это должно быть правдой, чтобы вызвать ошибку. Каждый случай, который я видел, имел следующие условия, но не все случаи, которые удовлетворяли следующим условиям, были прослушиваемы.

    • Должны быть включены короткие имена 8.3
    • SourceMask должен соответствовать оригинальному длинному имени.
    • Первоначальное переименование должно генерировать короткое имя, которое также соответствует исходной маске
    • Первоначальное переименованное короткое имя должно сортироваться позже исходного короткого имени (если оно существует?)

    Подобно exebook, вот реализация C #, чтобы получить имя целевого файла из исходного файла.

    Я нашел 1 небольшую ошибку в примерах dbenham:

      ren *_* *_NEW.* abc_newt_1.dat -> abc_newt_NEW.txt (should be: abd_newt_NEW.dat) 

    Вот код:

      /// <summary> /// Returns a filename based on the sourcefile and the targetMask, as used in the second argument in rename/copy operations. /// targetMask may contain wildcards (* and ?). /// /// This follows the rules of: http://superuser.com/questions/475874/how-does-the-windows-rename-command-interpret-wildcards /// </summary> /// <param name="sourcefile">filename to change to target without wildcards</param> /// <param name="targetMask">mask with wildcards</param> /// <returns>a valid target filename given sourcefile and targetMask</returns> public static string GetTargetFileName(string sourcefile, string targetMask) { if (string.IsNullOrEmpty(sourcefile)) throw new ArgumentNullException("sourcefile"); if (string.IsNullOrEmpty(targetMask)) throw new ArgumentNullException("targetMask"); if (sourcefile.Contains('*') || sourcefile.Contains('?')) throw new ArgumentException("sourcefile cannot contain wildcards"); // no wildcards: return complete mask as file if (!targetMask.Contains('*') && !targetMask.Contains('?')) return targetMask; var maskReader = new StringReader(targetMask); var sourceReader = new StringReader(sourcefile); var targetBuilder = new StringBuilder(); while (maskReader.Peek() != -1) { int current = maskReader.Read(); int sourcePeek = sourceReader.Peek(); switch (current) { case '*': int next = maskReader.Read(); switch (next) { case -1: case '?': // Append all remaining characters from sourcefile targetBuilder.Append(sourceReader.ReadToEnd()); break; default: // Read source until the last occurrance of 'next'. // We cannot seek in the StringReader, so we will create a new StringReader if needed string sourceTail = sourceReader.ReadToEnd(); int lastIndexOf = sourceTail.LastIndexOf((char) next); // If not found, append everything and the 'next' char if (lastIndexOf == -1) { targetBuilder.Append(sourceTail); targetBuilder.Append((char) next); } else { string toAppend = sourceTail.Substring(0, lastIndexOf + 1); string rest = sourceTail.Substring(lastIndexOf + 1); sourceReader.Dispose(); // go on with the rest... sourceReader = new StringReader(rest); targetBuilder.Append(toAppend); } break; } break; case '?': if (sourcePeek != -1 && sourcePeek != '.') { targetBuilder.Append((char)sourceReader.Read()); } break; case '.': // eat all characters until the dot is found while (sourcePeek != -1 && sourcePeek != '.') { sourceReader.Read(); sourcePeek = sourceReader.Peek(); } targetBuilder.Append('.'); // need to eat the . when we peeked it if (sourcePeek == '.') sourceReader.Read(); break; default: if (sourcePeek != '.') sourceReader.Read(); // also consume the source's char if not . targetBuilder.Append((char)current); break; } } sourceReader.Dispose(); maskReader.Dispose(); return targetBuilder.ToString().TrimEnd('.', ' '); } 

    И вот тестовый метод NUnit для тестирования примеров:

      [Test] public void TestGetTargetFileName() { string targetMask = "?????.?????"; Assert.AreEqual("a", FileUtil.GetTargetFileName("a", targetMask)); Assert.AreEqual("ab", FileUtil.GetTargetFileName("ab", targetMask)); Assert.AreEqual("ab", FileUtil.GetTargetFileName("abc", targetMask)); Assert.AreEqual("part1.part2", FileUtil.GetTargetFileName("part1.part2.part3", targetMask)); Assert.AreEqual("12345.12345", FileUtil.GetTargetFileName("123456.123456.123456", targetMask)); targetMask = "A?Z*"; Assert.AreEqual("AZ", FileUtil.GetTargetFileName("1", targetMask)); Assert.AreEqual("A2Z", FileUtil.GetTargetFileName("12", targetMask)); Assert.AreEqual("AZ.txt", FileUtil.GetTargetFileName("1.txt", targetMask)); Assert.AreEqual("A2Z.txt", FileUtil.GetTargetFileName("12.txt", targetMask)); Assert.AreEqual("A2Z", FileUtil.GetTargetFileName("123", targetMask)); Assert.AreEqual("A2Z.txt", FileUtil.GetTargetFileName("123.txt", targetMask)); Assert.AreEqual("A2Z4", FileUtil.GetTargetFileName("1234", targetMask)); Assert.AreEqual("A2Z4.txt", FileUtil.GetTargetFileName("1234.txt", targetMask)); targetMask = "*.txt"; Assert.AreEqual("a.txt", FileUtil.GetTargetFileName("a", targetMask)); Assert.AreEqual("b.txt", FileUtil.GetTargetFileName("b.dat", targetMask)); Assert.AreEqual("cxtxt", FileUtil.GetTargetFileName("cxy", targetMask)); targetMask = "*?.bak"; Assert.AreEqual("a.bak", FileUtil.GetTargetFileName("a", targetMask)); Assert.AreEqual("b.dat.bak", FileUtil.GetTargetFileName("b.dat", targetMask)); Assert.AreEqual("cxybak", FileUtil.GetTargetFileName("cxy", targetMask)); targetMask = "*_NEW.*"; Assert.AreEqual("abcd_NEW.txt", FileUtil.GetTargetFileName("abcd_12345.txt", targetMask)); Assert.AreEqual("abc_newt_NEW.dat", FileUtil.GetTargetFileName("abc_newt_1.dat", targetMask)); Assert.AreEqual("abcd_123.a_NEW", FileUtil.GetTargetFileName("abcd_123.a_b", targetMask)); targetMask = "?x.????999.*rForTheCourse"; Assert.AreEqual("px.part999.rForTheCourse", FileUtil.GetTargetFileName("part1.part2", targetMask)); Assert.AreEqual("px.part999.parForTheCourse", FileUtil.GetTargetFileName("part1.part2.part3", targetMask)); Assert.AreEqual("ax.b999.crForTheCourse", FileUtil.GetTargetFileName("abc", targetMask)); Assert.AreEqual("ax.b999.CarParForTheCourse", FileUtil.GetTargetFileName("abCarPart3BEER", targetMask)); } 

    Может быть, кто-то может найти это полезным. Этот код JavaScript основан на ответе dbenham выше. Я не тестировал sourceMask очень много, но targetMask соответствует всем примерам, данным dbenham.

     function maskMatch(path, mask) { mask = mask.replace(/\./g, '\\.') mask = mask.replace(/\?/g, '.') mask = mask.replace(/\*/g, '.+?') var r = new RegExp('^'+mask+'$', '') return path.match(r) } function maskNewName(path, mask) { if (path == '') return var x = 0, R = '' for (var m = 0; m < mask.length; m++) { var ch = mask[m], q = path[x], z = mask[m + 1] if (ch != '.' && ch != '*' && ch != '?') { if (q && q != '.') x++ R += ch } else if (ch == '?') { if (q && q != '.') R += q, x++ } else if (ch == '*' && m == mask.length - 1) { while (x < path.length) R += path[x++] } else if (ch == '*') { if (z == '.') { for (var i = path.length - 1; i >= 0; i--) if (path[i] == '.') break if (i < 0) { R += path.substr(x, path.length) + '.' i = path.length } else R += path.substr(x, i - x + 1) x = i + 1, m++ } else if (z == '?') { R += path.substr(x, path.length), m++, x = path.length } else { for (var i = path.length - 1; i >= 0; i--) if (path[i] == z) break if (i < 0) R += path.substr(x, path.length) + z, x = path.length, m++ else R += path.substr(x, i - x), x = i + 1 } } else if (ch == '.') { while (x < path.length) if (path[x++] == '.') break R += '.' } } while (R[R.length - 1] == '.') R = R.substr(0, R.length - 1) } 

    Мне удалось написать этот код в BASIC для маскировки имен файлов подстановочных знаков:

     REM inputs a filename and matches wildcards returning masked output filename. FUNCTION maskNewName$ (path$, mask$) IF path$ = "" THEN EXIT FUNCTION IF INSTR(path$, "?") OR INSTR(path$, "*") THEN EXIT FUNCTION x = 0 R$ = "" FOR m = 0 TO LEN(mask$) - 1 ch$ = MID$(mask$, m + 1, 1) q$ = MID$(path$, x + 1, 1) z$ = MID$(mask$, m + 2, 1) IF ch$ <> "." AND ch$ <> "*" AND ch$ <> "?" THEN IF LEN(q$) AND q$ <> "." THEN x = x + 1 R$ = R$ + ch$ ELSE IF ch$ = "?" THEN IF LEN(q$) AND q$ <> "." THEN R$ = R$ + q$: x = x + 1 ELSE IF ch$ = "*" AND m = LEN(mask$) - 1 THEN WHILE x < LEN(path$) R$ = R$ + MID$(path$, x + 1, 1) x = x + 1 WEND ELSE IF ch$ = "*" THEN IF z$ = "." THEN FOR i = LEN(path$) - 1 TO 0 STEP -1 IF MID$(path$, i + 1, 1) = "." THEN EXIT FOR NEXT IF i < 0 THEN R$ = R$ + MID$(path$, x + 1) + "." i = LEN(path$) ELSE R$ = R$ + MID$(path$, x + 1, i - x + 1) END IF x = i + 1 m = m + 1 ELSE IF z$ = "?" THEN R$ = R$ + MID$(path$, x + 1, LEN(path$)) m = m + 1 x = LEN(path$) ELSE FOR i = LEN(path$) - 1 TO 0 STEP -1 'IF MID$(path$, i + 1, 1) = z$ THEN EXIT FOR IF UCASE$(MID$(path$, i + 1, 1)) = UCASE$(z$) THEN EXIT FOR NEXT IF i < 0 THEN R$ = R$ + MID$(path$, x + 1, LEN(path$)) + z$ x = LEN(path$) m = m + 1 ELSE R$ = R$ + MID$(path$, x + 1, i - x) x = i + 1 END IF END IF END IF ELSE IF ch$ = "." THEN DO WHILE x < LEN(path$) IF MID$(path$, x + 1, 1) = "." THEN x = x + 1 EXIT DO END IF x = x + 1 LOOP R$ = R$ + "." END IF END IF END IF END IF END IF NEXT DO WHILE RIGHT$(R$, 1) = "." R$ = LEFT$(R$, LEN(R$) - 1) LOOP R$ = RTRIM$(R$) maskNewName$ = R$ END FUNCTION 
    Давайте будем гением компьютера.