WindowsのRENAMEコマンドはどのようにワイルドカードを解釈するのですか?

batch-file cmd.exe command-line rename windows

Windows RENAME (REN) コマンドはどのようにワイルドカードを解釈するのですか?

HELP機能が組み込まれていても何の役にも立ちません – ワイルドカードには全く対応していません

Microsoft technet XPのオンラインヘルプはあまり良くありません。ここでは、ワイルドカードに関するすべてのことが書かれています

“どちらのファイル名パラメータにもワイルドカード(*?)を使用することができます。filename2 でワイルドカードを使用した場合、ワイルドカードで表される文字は filename1 の対応する文字と同じになります。”

あまり助けにはなりませんが、その文を解釈するには多くの方法があります

何度か filename2 パラメータでワイルドカードを使用することに成功したことがありますが、それはいつも試行錯誤でした。何がうまくいき、何がうまくいかないのかを予測することができませんでした。必要に応じて新しい名前を構築できるように、それぞれの名前を解析する FOR ループを持つ小さなバッチスクリプトを書かなければならないことがよくありました。あまり便利ではありません

ワイルドカードがどのように処理されるかのルールを知っていれば、バッチ処理に頼らずにRENAMEコマンドをより効果的に使えるようになると思います。もちろん、ルールを知っていればバッチ開発にも役立ちます

(はい-これはペアの質問と回答を投稿している場合です。ルールを知らないことに飽きてしまったので、自分で実験してみることにしました。私は他の多くの人が私が発見したことに興味を持っているかもしれないと考えています)

  83  dbenham  2012-09-16


ベストアンサー

これらのルールは、Vistaマシンでの広範なテストの後に発見されました。ファイル名にユニコードを使用したテストは行われていません

RENAMEには2つのパラメータ、sourceMaskとtargetMaskが必要です。sourceMaskとtargetMaskの両方には、*および/または?のワイルドカードを含めることができます。ワイルドカードの動作は、ソースマスクとターゲットマスクの間でわずかに変化します

注意 – REN はフォルダの名前を変更するために使用できますが、フォルダの名前を変更する際に sourceMask または targetMask のどちらにもワイルドカードを使用することはできません。sourceMask が少なくとも 1 つのファイルに一致する場合、そのファイルはリネームされ、フォルダは無視されます。sourceMask がファイルではなくフォルダのみに一致する場合、source または target にワイルドカードが出現すると構文エラーが発生します。sourceMask が何もマッチしない場合は、”file not found” エラーが発生します

また、ファイル名を変更する際には、ワイルドカードは sourceMask のファイル名部分でのみ許可されます。ファイル名につながるパスにはワイルドカードは使用できません

sourceMask

sourceMask は、どのファイルがリネームされるかを決定するためのフィルタとして機能します。ワイルドカードは、ファイル名をフィルタリングする他のコマンドと同じように動作します

  • ?.以外の0または1文字にマッチします。このワイルドカードは貪欲です – .でない場合は常に次の文字を消費しますが、名前の最後にある場合や次の文字が.の場合は失敗することなく何もマッチしません

  • *.を含む0文字以上の文字にマッチします(下記の1つの例外を除く)。このワイルドカードは貪欲ではありません。後続の文字がマッチするようにするために必要な数だけマッチします

ワイルドカード以外のすべての文字は、いくつかの特殊なケースの例外を除いて、自分自身にマッチしなければなりません

  • . – それ自身にマッチするか、それ以上の文字が残っていない場合は名前の最後にマッチします (何もありません)。(注意 – 有効な Windows の名前は . で終わることはできません)

  • {space} – それ自身にマッチするか、それ以上の文字が残っていない場合は名前の最後にマッチします (何もありません)。(注意 – 有効な Windows の名前は {space} で終わることはできません)

  • *.終了時 – .以外の0文字以上の文字にマッチします。 .終了時の.は、マスクの最後の文字が.である限り、実際には.{space}の任意の組み合わせにすることができます

上記のルールはそれほど複雑なものではありません。しかし、状況を混乱させる非常に重要なルールがもう一つあります。それは、sourceMaskは長い名前と短い8.3の名前(存在する場合)の両方と比較されるということです。この最後のルールは結果の解釈を非常にトリッキーなものにします

RegEdit を使って NTFS ボリューム上で 8.3 のショートネームの生成を無効にすることができます。ショートネームを無効にする前に生成されたショートネームは残ります

targetMask

注意 – 私は厳密なテストを行っていませんが、これらの同じルールはCOPYコマンドのターゲット名に対しても動作するようです

targetMask は新しい名前を指定します。targetMaskは常にフルロングの名前に適用されます

sourceMaskにワイルドカードがあるかどうかは、targetMaskでのワイルドカードの処理方法には影響しません

以下の議論では – c*, ?, . 以外の文字を表します

targetMaskはソース名に対して左から右へ厳密に処理され、バックトラッキングは行われません

  • c – ソース文字が . でない場合にのみソース名内の位置を進め、常にターゲット名に c を追加します。(ソースにあった文字を c に置き換えますが、決して . に置き換えることはありません)

  • ? – ソース文字が . でない限り、ソース長名の次の文字にマッチし、ターゲット名に追加します。次の文字が . の場合、またはソース名の最後にある場合、結果には何も追加されず、ソース名内の現在の位置は変更されません

  • * at end of targetMask – ソースからターゲットに残っているすべての文字を追加します。すでにソースの最後にある場合は何もしません

  • *c – 現在の位置からcが最後に出現するまでのすべてのソース文字をマッチさせ、マッチした文字セットをターゲット名に追加します (大文字小文字を区別する貪欲なマッチ)。c が見つからない場合、ソースからの残りの文字がすべて追加され、その後に c が追加されます。 これは、Windows ファイルのパターンマッチングが大文字小文字を区別する唯一の状況です

  • *. – 現在の位置から.が最後に出現するまでのすべてのソース文字をマッチさせ(貪欲なマッチ)、マッチした文字セットをターゲット名に追加します。. が見つからない場合は、ソースからの残りの文字をすべて追加し、その後に . を追加します

  • *? – ソースの残りの文字をすべてターゲットに追加します。すでにソースの最後にある場合は何もしません

  • . の前に * を置かない – ソース内の位置を . の最初の文字をコピーせずに進め、ターゲット名に . を追加します。ソース中に . が見つからない場合は、ソースの最後まで進み、ターゲット名に . を追加します

Windows のファイル名は .{space} で終わることができないため、targetMask を使い切った後、最後の .{space} は、結果として得られるターゲット名の末尾から削除されます

いくつかの実践例

いずれかの延長前に1位と3位のキャラを代用する(まだ存在しない場合は2位か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
c.x.y -> c.x.txt

すべてのファイルに拡張子を追加します

ren  *  *?.bak
a     -> a.bak
b.dat -> b.dat.bak
c.x.y -> c.x.y.bak

最初の拡張子の後の余分な拡張子を削除します。既存の完全な名前と最初の拡張子を保持するためには、適切な ? を使用しなければならないことに注意してください

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)

上記と同じですが、最初の名前や拡張子が5文字以上のファイルをフィルタリングして、切り捨てられないようにします。(明らかに、6文字までの名前と拡張子を保存するために、targetMaskの両端に?を追加することができます)

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)

名前の最後の_以降の文字を変更し、拡張子を保持しようとする。(拡張子に _ が含まれている場合は正しく動作しません)

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)

どんな名前でも、.で区切られたコンポーネントに分割することができます。文字は、ワイルドカードで残りの部分を保存したまま、コンポーネントの最初や途中から削除したり、追加したりすることはできません。置換はどこでも可能です

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

短い名前が有効になっている場合、名前に少なくとも8 ?、拡張子に少なくとも3 ?を持つsourceMaskは、常に短い8.3の名前と一致するため、すべてのファイルにマッチします

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


名前の接頭辞を削除するのに便利な気まぐれ/バグ?

この SuperUser 投稿では、ファイル名から先頭の文字 (. を除く) を削除するために、スラッシュ (/) のセットを使用する方法を説明しています。削除する文字には1つのスラッシュが必要です。Windows10マシンで動作を確認してみました

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

残念ながら、/を先頭にしても名前の中の.を削除することはできません。そのため、.を含むプレフィックスを削除することはできません。例えば、以下のようになります

ren "abc.xyz.*.txt" "////////*.txt"
abc.xyz.123.txt        --> .xyz.123.txt
abc.xyz.HelloWorld.txt --> .xyz.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

/は、ファイル名の途中や末尾にある文字を削除するためには使えません。削除できるのは、先頭(プレフィックス)の文字のみです。また、この方法はフォルダ名には使えないことに注意してください

技術的には、/はワイルドカードとして機能していません。むしろ、cターゲットマスクルールに従った単純な文字置換を行っています。しかし、置換の後、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 は元のロングネームと一致する必要があります
  • 最初のリネームは、sourceMask にもマッチする短い名前を生成する必要があります
  • 名前を変更した最初のショートネームは、元のショートネームよりも後にソートしなければなりません(存在する場合は?

128  dbenham  2012-09-16


exebookと同様に、ソースファイルからターゲットのファイル名を取得するC#の実装を紹介します

dbenhamの例の中に1つの小さな誤りを見つけました

 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("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));

}

4  amrunning  2014-12-16


ワイルドカードのファイル名をマスクするためのコードを 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

1  eoredson  2016-10-13


多分誰かがこれを役に立つと思うでしょう。この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)
}

1  exebook  2014-04-09


タイトルとURLをコピーしました