2012-09-16 13:59:11 +0000 2012-09-16 13:59:11 +0000
81
81

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

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

Microsoft technet XP オンラインヘルプ はあまり良くありません。ワイルドカードについては以下のように書かれています。

_“ファイル名のパラメータにワイルドカード(*?)を使用することができます。

_"どちらのファイル名パラメータにもワイルドカード (0x6& と 0x6&) を使用することができます。ファイル名2 でワイルドカードを使用した場合、ワイルドカードで表される文字はファイル名1 の対応する文字と同じになります。

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

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

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

回答 (4)

120
120
120
2012-09-16 14:00:21 +0000

これらのルールは、Vistaマシンでの広範なテストの後に発見されました。

RENAMEでは、2つのパラメータ、sourceMaskとtargetMaskの後に続く2つのパラメータが必要です。sourceMaskとtargetMaskの両方に*および/または?のワイルドカードを含めることができます。ワイルドカードの動作は、ソースマスクとターゲットマスクで若干異なります。

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

_また、ファイルの名前を変更する際に、ワイルドカードは sourceMask のファイル名部分でのみ許可されます。

sourceMask

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

  • ? - 0x6以外の0文字または1文字にマッチします。このワイルドカードは貪欲です。このワイルドカードは貪欲ではありません。後続の文字がマッチするようにするために必要な分だけマッチします。

ワイルドカード以外のすべての文字は、いくつかの特殊なケースの例外を除いて、それ自身と一致しなければなりません。

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

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

  • . at the end - 0個以上の文字にマッチします _ただし、.を除く) .で終わる{space}は、マスクの最後の文字が{space}である限り、実際には*..の任意の組み合わせになります。

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

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

targetMask

注 - 厳密なテストはしていませんが、COPYコマンドのターゲット名にも同じルールが適用されるようです

targetMaskは新しい名前を指定します。targetMaskは常に完全なロングネームに適用されます。

sourceMask にワイルドカードがあってもなくても、targetMask でのワイルドカードの処理方法には影響しません。

以下の議論では - ..{space}.* 以外の文字を表します。

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

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

  • . - ソースのロングネームから次の文字をマッチさせ、ソースの文字が c でない限り、それをターゲット名に追加します。

  • c at the end of targetMask - ソースの残りの文字をすべてターゲットに追加します。すでにソースの最後にある場合は何もしません。

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

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

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

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

targetMask を使い切った後。Windows のファイル名は * または .

いくつかの実用的な例

拡張子の前の 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)

どんな名前でも、0x6で区切られたコンポーネントに分割することができます& 文字は、各コンポーネントの末尾にのみ追加または削除することができます。ワイルドカードを使用して残りの部分を保存しながら、コンポーネントの先頭または中間に文字を削除したり、追加したりすることはできません。置換はどこでも可能です。

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 {space} を指定した sourceMask は、常に 8.3 の短縮名と一致するため、すべてのファイルにマッチします。

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

名前の接頭辞を削除するための便利な癖/バグ?

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

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

この手法は、ソースマスクとターゲットマスクの両方が二重引用符で囲まれている場合にのみ動作します。必要な引用符がない以下のフォームはすべてこのエラーで失敗します。{space}

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

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

厳密には、? はワイルドカードとして機能していません。むしろ、単純な文字置換を行っていますが、置換後、RENコマンドは_がファイル名では無効であることを認識し、先頭の_スラッシュをファイル名から削除します。REN は、対象の名前の途中に . があることを検出すると、構文エラーを与えます。

RENAMEのバグの可能性 - 1つのコマンドで同じファイルの名前を2回変更することがあります!

空のテストフォルダで起動。

0x1&

sourceMask ? が最初に長いファイル名にマッチし、ファイルは期待される ? の結果にリネームされると思います。その後、RENAMEは処理するファイルを探し続け、新しいショートネーム/を介して新たに命名されたファイルを見つけます。その後、ファイルは再びリネームされ、最終的な結果はThe syntax of the command is incorrectとなります。

8.3の名前生成を無効にすると、RENAMEでは期待通りの結果が得られます。

この奇妙な動作を引き起こすために存在しなければならないトリガー条件の全てを完全に理解したわけではありません。終わらない再帰的なRENAMEを作成できるのではないかと心配していましたが、それを誘発することはできませんでした。

私は、このバグを誘発するためには、以下のすべてが真でなければならないと考えています。私が見たバグったケースはすべて以下の条件を満たしていましたが、以下の条件を満たしているケースがすべてバグったわけではありませんでした。

  • 8.3のショートネームが有効になっている必要がある
  • sourceMaskが元のロングネームと一致している必要がある。
  • 最初のリネームは、ソースマスクにもマッチするショートネームを生成しなければならない
  • 最初にリネームされたショートネームは、元のショートネームよりも後にソートされなければならない (存在する場合は?)
4
4
4
2014-12-16 10:13:11 +0000

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

    }
``` 0x1&
1
1
1
2014-04-09 17:07:37 +0000

多分誰かがこれを役に立つと思うでしょう。この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
1
1
2016-10-13 01:27:15 +0000

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