web-dev-qa-db-ja.com

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

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

組み込みのHELP機能は役に立ちません - ワイルドカードを扱うことはまったくありません。

Microsoftテクニカル_ XPオンラインヘルプ はそれほど良くありません。これがワイルドカードに関して言えることすべてです。

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

それほど助けにはならない - ステートメントを解釈することができる多くの方法がある。

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

ワイルドカードの処理方法に関する規則を知っていれば、それほど頻繁にバッチ処理を行わなくてもRENAMEコマンドをより効果的に使用できると思います。もちろん、ルールを知っていることもバッチ開発に役立ちます。

(はい、これは私が一対の質問と回答を投稿しているケースです。私はルールを知らないことにうんざりして自分で実験することにしました。

75
dbenham

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

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

注意-RENはフォルダの名前を変更するために使用できますが、ワイルドカードはnotフォルダーの名前を変更する場合、sourceMaskまたはtargetMaskで許可されます。 sourceMaskが少なくとも1つのファイルと一致する場合、ファイルの名前が変更され、フォルダーは無視されます。 sourceMaskがファイルではなくフォルダーのみに一致する場合、ワイルドカードがソースまたはターゲットに表示されると構文エラーが生成されます。 sourceMaskが何にも一致しない場合、「ファイルが見つかりません」というエラーが発生します。

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

sourceMask

SourceMaskは、名前を変更するファイルを決定するフィルターとして機能します。ワイルドカードは、ファイル名をフィルタリングする他のコマンドと同じように機能します。

  • ?-0または1文字と一致except.このワイルドカードは貪欲であり、そうでない場合は常に次の文字を消費しますa .ただし、名前の末尾にある場合、または次の文字が.である場合、失敗することなく何にも一致しません

  • *-0個以上の任意の文字に一致します含む.(以下の例外を1つ)。このワイルドカードは貪欲ではありません。後続の文字を一致させるために必要なだけ、または必要に応じて一致します。

すべての非ワイルドカード文字は、いくつかの特殊なケースの例外を除き、自分自身と一致する必要があります。

  • .-それ自体と一致するか、文字が残っていない場合は名前の最後(何もない)と一致します。 (注-有効なWindows名は.で終わることはできません)

  • {space}-それ自体と一致するか、文字が残っていない場合は名前の末尾と一致する(何もない) (注-有効なWindows名は{space}で終わることはできません)

  • *.-0以上の任意の文字と一致除く.終了する.は、実際には.{space}の任意の組み合わせマスクの最後の文字が.である限り、これは*がどの文字セットとも単純に一致しない唯一の例外です。

上記の規則はそれほど複雑ではありません。しかし、状況を混乱させるもう1つの非常に重要なルールがあります。sourceMaskは、長い名前と短い8.3名(存在する場合)の両方と比較されます。この最後のルールにより、マスクが短い名前で一致する場合は常に明らかではないため、結果の解釈が非常に難しくなります。

RegEditを使用して、NTFSボリューム上の短い8.3形式の名前の生成を無効にすることができます。この時点で、ファイルマスクの結果の解釈ははるかに簡単です。ショートネームを無効にする前に生成されたショートネームは残ります。

targetMask

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

TargetMaskは新しい名前を指定します。これは常に完全な長い名前に適用されます。 sourceMaskが短い8.3名に一致した場合でも、targetMaskは短い8.3名に適用されません。

SourceMaskでのワイルドカードの有無は、targetMaskでのワイルドカードの処理方法に影響を与えません。

以下の説明では、cは、*?、または.以外の文字を表します

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

  • c-次の文字が.でない限り、ソース名内の位置を進め、ターゲット名にcを追加します。 (ソースにあった文字をcで置き換えますが、.を決して置き換えません)

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

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

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

  • *.-現在の位置からlast.の出現(貪欲な一致)までのすべてのソース文字と一致し、一致したセットを追加しますターゲット名に文字。 .が見つからない場合、ソースからの残りのすべての文字が追加され、その後に.が続きます

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

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

TargetMaskが使い果たされた後、Windowsファイル名は.または{space}で終わることができないため、後続の.および{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文字の名前と拡張子を保持できます)

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


役に立つ癖/バグ?名前のプレフィックスを削除する

このスーパーユーザーの投稿 は、一連のスラッシュ(/)を使用してファイル名から先頭の文字を削除する方法を説明しています。削除する文字ごとに1つのスラッシュが必要です。 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のバグの可能性-1つのコマンドで同じファイルの名前を2回変更することがあります!

空のテストフォルダーで開始:

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にも一致する短い名前を生成する必要があります
  • 名前が変更された最初の短縮名は、元の短縮名よりも後でソートする必要があります(存在する場合)。
116
dbenham

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

たぶん誰かがこれが役に立つと思うかもしれません。この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

私はワイルドカードのファイル名を隠すために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
0
eoredson