Comment la commande Windows RENAME (REN) interprète-t-elle les caractères génériques?
La fonction d'aide intégrée n'est d'aucune aide - elle ne traite pas du tout les caractères génériques.
Le Microsoft technet XP aide en ligne n’est pas bien meilleur. Voici tout ce qu'il a à dire à propos des jokers:
"Vous pouvez utiliser des caractères génériques (
*
et?
) dans l'un ou l'autre des paramètres de nom de fichier. Si vous utilisez des caractères génériques dans nomfichier2, les caractères représentés par les caractères génériques seront identiques aux caractères correspondants dans nomfichier1."
Pas beaucoup d’aide - cette déclaration peut être interprétée de nombreuses manières.
J'ai parfois réussi à utiliser des caractères génériques dans le paramètre nomfichier2, mais cela a toujours été un essai et une erreur. Je n'ai pas été en mesure d'anticiper ce qui fonctionne et ce qui ne fonctionne pas. Souvent, j'ai dû écrire un petit script batch avec une boucle FOR qui analyse chaque nom afin de pouvoir créer chaque nouveau nom selon les besoins. Pas très pratique.
Si je connaissais les règles de traitement des caractères génériques, je pensais pouvoir utiliser la commande RENAME de manière plus efficace sans devoir recourir au traitement par lots aussi souvent. Bien sûr, connaître les règles profiterait également au développement par lots.
(Oui - c’est un cas où je poste une question et une réponse jumelées. Je suis fatigué de ne pas connaître les règles et j’ai décidé d’expérimenter moi-même. J’imagine que beaucoup d’autres pourraient être intéressées par ce que j’ai découvert.} _
Ces règles ont été découvertes après des tests approfondis sur une machine Vista. Aucun test n'a été effectué avec unicode dans les noms de fichiers.
RENAME nécessite 2 paramètres - un masque source, suivi d'un masque cible. SourceMask et targetMask peuvent contenir des caractères génériques *
et/ou ?
. Le comportement des caractères génériques change légèrement entre les masques source et cible.
Note- REN peut être utilisé pour renommer un dossier, mais les caractères génériques sont not autorisés dans le masque source ou le masque cible lors du changement de nom. Si le sourceMask correspond à au moins un fichier, puis le ou les fichiers sont renommés et les dossiers sont ignorés. Si le masque source correspond uniquement à des dossiers et non à des fichiers, une erreur de syntaxe est générée si des caractères génériques apparaissent dans la source ou la cible. ne correspond à rien, puis une erreur "fichier non trouvé" est générée.
De plus, lors du changement de nom de fichiers, les caractères génériques ne sont autorisés que dans la partie nom de fichier du masque de source. Les caractères génériques ne sont pas autorisés dans le chemin qui mène au nom de fichier.
SourceMask fonctionne comme un filtre pour déterminer quels fichiers sont renommés. Les caractères génériques fonctionnent ici de la même manière que toute autre commande filtrant les noms de fichiers.
?
- Correspond à n'importe quel caractère 0 ou 1 sauf.
Ce caractère générique est glouton - il consomme toujours le caractère suivant s'il ne s'agit pas d'un .
Cependant, il ne correspondra à rien sans échec s'il se trouve à la fin du nom ou si le caractère suivant est un .
*
- Correspond à tous les caractères 0 ou plus y compris.
(avec une exception ci-dessous). Ce joker n'est pas gourmand. La correspondance sera aussi faible ou aussi grande que nécessaire pour permettre la correspondance des caractères suivants.
Tous les caractères non génériques doivent correspondre, à quelques exceptions près.
.
- Correspond lui-même ou peut correspondre à la fin du nom (rien) s'il ne reste plus de caractères. (Remarque - un nom Windows valide ne peut pas se terminer par .
)
{space}
- Correspond lui-même ou peut correspondre à la fin du nom (rien) s'il ne reste plus de caractères. (Remarque - un nom Windows valide ne peut pas se terminer par {space}
)
*.
à la fin - Correspond à tous les caractères 0 ou plus sauf.
Le .
final peut en réalité être une combinaison quelconque de .
et de {space}
tant que le dernier caractère du masque est .
. est la seule et unique exception où *
ne correspond simplement à aucun jeu de caractères.
Les règles ci-dessus ne sont pas si complexes. Mais il existe une autre règle très importante qui rend la situation confuse: le masque source est comparé à la fois au nom long et au nom court 8.3 (s'il existe). Cette dernière règle peut rendre l'interprétation des résultats très délicate, car ce n'est pas toujours évident lorsque le masque correspond via le nom abrégé.
Il est possible d’utiliser RegEdit pour désactiver la génération de noms courts 8.3 sur des volumes NTFS, l’interprétation des résultats du masque de fichier étant alors beaucoup plus simple. Tous les noms abrégés générés avant la désactivation des noms abrégés resteront.
Remarque - Je n'ai pas effectué de test rigoureux, mais il semble que ces mêmes règles fonctionnent également pour le nom cible de la commande COPY.
TargetMask spécifie le nouveau nom. Il est toujours appliqué au nom complet complet. Le masque cible n'est jamais appliqué au nom abrégé 8.3, même si le masque source correspond au nom abrégé 8.3.
La présence ou l'absence de caractères génériques dans le masque source n'a aucun impact sur la façon dont les caractères génériques sont traités dans le masque cible.
Dans la discussion suivante - c
représente tout caractère autre que *
, ?
ou .
Le targetMask est traité par rapport au nom de source strictement de gauche à droite sans suivi en arrière.
c
- Avance la position dans le nom de la source tant que le caractère suivant n'est pas .
et ajoute c
au nom de la cible. (Remplace le caractère qui était dans la source par c
, mais ne remplace jamais .
)
?
- Correspond au caractère suivant du nom long de la source et l'ajoute au nom de la cible tant que le caractère suivant n'est pas .
Si le caractère suivant est .
ou si, à la fin du nom de la source, non Le caractère est ajouté au résultat et la position actuelle dans le nom de la source reste inchangée.
*
à la fin de targetMask - Ajoute tous les caractères restants de la source à la cible. Si déjà à la fin de la source, alors ne fait rien.
*c
- Correspond à tous les caractères source de la position actuelle jusqu'à la dernière occurrence de c
(correspondance avide sensible à la casse) et ajoute l'ensemble de caractères correspondant au nom de la cible. Si c
n'est pas trouvé, tous les caractères restants de la source sont ajoutés, suivis de c
. C'est la seule situation à ma connaissance où la correspondance de modèle de fichier Windows est sensible à la casse.
*.
- Correspond à tous les caractères source de la position actuelle via l'occurrence de dernier _ de .
(correspondance avide) et ajoute l'ensemble de caractères correspondant au nom de la cible. Si .
n'est pas trouvé, tous les caractères restants de la source sont ajoutés, suivis de .
*?
- Ajoute tous les caractères restants de la source à la cible. Si déjà à la fin de la source alors ne fait rien.
.
sans*
devant - fait avancer la position dans la source de l'occurrence première de .
sans copier de caractères et ajoute .
au nom de la cible. Si .
est introuvable dans la source, avance à la fin de la source et ajoute .
au nom de la cible.
Une fois le targetMask épuisé, les .
et {space}
finaux sont supprimés à la fin du nom de cible résultant, car les noms de fichier Windows ne peuvent pas se terminer par .
ou {space}
.
Remplacez un personnage aux 1ère et 3ème positions avant toute extension (ajoute un 2ème ou 3ème caractère s'il n'existe pas encore)
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
Changer l'extension (finale) de chaque fichier
ren * *.txt
a -> a.txt
b.dat -> b.txt
c.x.y -> c.x.txt
Ajouter une extension à chaque fichier
ren * *?.bak
a -> a.bak
b.dat -> b.dat.bak
c.x.y -> c.x.y.bak
Supprimer toute extension supplémentaire après l'extension initiale. Notez que ?
doit être utilisé pour conserver le nom complet complet et l’extension initiale.
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)
Comme ci-dessus, mais filtrez les fichiers dont le nom initial et/ou l'extension est supérieur à 5 caractères afin qu'ils ne soient pas tronqués. (Évidemment, pourrait ajouter un ?
supplémentaire à l'une des extrémités de targetMask pour conserver les noms et les extensions jusqu'à 6 caractères)
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)
Changez les caractères après le dernier _
dans le nom et tentez de conserver l’extension. (Ne fonctionne pas correctement si _
apparaît en 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)
Tout nom peut être divisé en composants délimités par .
Les caractères ne peuvent être ajoutés ou supprimés qu'à la fin de chaque composant. Les caractères ne peuvent pas être supprimés ou ajoutés au début ou au milieu d'un composant tout en préservant le reste avec des caractères génériques. Les substitutions sont autorisées n'importe où.
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
Si les noms abrégés sont activés, un masque source comportant au moins 8 ?
pour le nom et au moins 3 ?
pour l'extension correspondra à tous les fichiers, car il correspondra toujours au nom abrégé 8.3.
ren ????????.??? ?x.????999.*rForTheCourse
part1.part2.part3.part4 -> px.part999.part3.parForTheCourse
Cet article de SuperUser décrit comment un ensemble de barres obliques (/
) peut être utilisé pour supprimer les caractères de tête d'un nom de fichier. Une barre oblique est requise pour chaque caractère à supprimer. J'ai confirmé le comportement sur une machine Windows 10.
ren "abc-*.txt" "////*.txt"
abc-123.txt --> 123.txt
abc-HelloWorld.txt --> HelloWorld.txt
Cette technique ne fonctionne que si les masques source et cible sont placés entre guillemets. Tous les formulaires suivants sans les guillemets requis échouent avec cette erreur: 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
Le /
ne peut pas être utilisé pour supprimer des caractères au milieu ou à la fin d'un nom de fichier. Il ne peut supprimer que les caractères de préfixe.
Techniquement, le /
ne fonctionne pas comme un caractère générique. Au lieu de cela, il effectue une substitution de caractère simple, mais après la substitution, la commande REN reconnaît que /
n'est pas valide dans un nom de fichier et supprime les barres /
du premier nom. REN donne une erreur de syntaxe s'il détecte /
au milieu d'un nom de cible.
Commencer dans un dossier de test vide:
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
Je crois que le sourceMask *1*
correspond en premier au nom de fichier long, et le fichier est renommé avec le résultat attendu de 223456789.123.x
. RENAME continue ensuite à rechercher plus de fichiers à traiter et recherche le fichier nouvellement nommé via le nouveau nom abrégé 223456~1.X
. Le fichier est ensuite renommé, donnant le résultat final de 223456789.123.xx
.
Si je désactive la génération de noms 8.3, le RENAME donne le résultat attendu.
Je n'ai pas complètement défini toutes les conditions de déclenchement qui doivent exister pour induire ce comportement étrange. Je craignais qu'il ne soit possible de créer un RENAME récursif sans fin, mais je n'ai jamais réussi à le provoquer.
Je crois que tout ce qui suit doit être vrai pour induire le bogue. Tous les incidents que j'ai vus comportaient les conditions suivantes, mais tous les incidents répondant aux conditions suivantes ne l'ont pas été.
Semblable à exebook, voici une implémentation en C # pour obtenir le nom de fichier cible à partir d’un fichier source.
J'ai trouvé une petite erreur dans les exemples de dbenham:
ren *_* *_NEW.*
abc_newt_1.dat -> abc_newt_NEW.txt (should be: abd_newt_NEW.dat)
Voici le code:
/// <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('.', ' ');
}
Et voici une méthode de test NUnit pour tester les exemples:
[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));
}
Peut-être que quelqu'un peut trouver cela utile. Ce code JavaScript est basé sur la réponse de dbenham ci-dessus.
Je n'ai pas beaucoup testé sourceMask
, mais targetMask
correspond à tous les exemples donnés par 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)
}
J'ai réussi à écrire ce code en BASIC pour masquer les noms de fichiers génériques:
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