WindowsのRENAME(REN)コマンドはワイルドカードをどのように解釈しますか?
組み込みのHELP機能は役に立ちません - ワイルドカードを扱うことはまったくありません。
Microsoftテクニカル_ XPオンラインヘルプ はそれほど良くありません。これがワイルドカードに関して言えることすべてです。
"どちらのファイル名パラメータでもワイルドカード(
*
と?
)を使用できます。filename2でワイルドカードを使用すると、ワイルドカードで表される文字はfilename1の対応する文字と同じになります。"
それほど助けにはならない - ステートメントを解釈することができる多くの方法がある。
私はfilename2パラメータでワイルドカードをうまく使うことができましたが、いつも試行錯誤されてきました。何がうまくいくのか、何がうまくいかないのか、私は予想できませんでした。多くの場合、必要に応じて新しい名前をそれぞれ作成できるように、各名前を解析するFORループを含む小さなバッチスクリプトを書くことに頼らなければなりませんでした。あまり便利ではありません。
ワイルドカードの処理方法に関する規則を知っていれば、それほど頻繁にバッチ処理を行わなくてもRENAMEコマンドをより効果的に使用できると思います。もちろん、ルールを知っていることもバッチ開発に役立ちます。
(はい、これは私が一対の質問と回答を投稿しているケースです。私はルールを知らないことにうんざりして自分で実験することにしました。
これらのルールは、Vistaマシンでの広範なテストの後に発見されました。ファイル名にUnicodeを使用したテストは行われませんでした。
RENAMEには、sourceMaskとそれに続くtargetMaskという2つのパラメーターが必要です。 sourceMaskとtargetMaskの両方に、*
や?
ワイルドカードを含めることができます。ワイルドカードの動作は、ソースマスクとターゲットマスクの間でわずかに変化します。
注意-RENはフォルダの名前を変更するために使用できますが、ワイルドカードはnotフォルダーの名前を変更する場合、sourceMaskまたはtargetMaskで許可されます。 sourceMaskが少なくとも1つのファイルと一致する場合、ファイルの名前が変更され、フォルダーは無視されます。 sourceMaskがファイルではなくフォルダーのみに一致する場合、ワイルドカードがソースまたはターゲットに表示されると構文エラーが生成されます。 sourceMaskが何にも一致しない場合、「ファイルが見つかりません」というエラーが発生します。
また、ファイル名を変更する場合、ワイルドカードはsourceMaskのファイル名部分でのみ許可されます。ファイル名までのパスにワイルドカードは使用できません。
SourceMaskは、名前を変更するファイルを決定するフィルターとして機能します。ワイルドカードは、ファイル名をフィルタリングする他のコマンドと同じように機能します。
?
-0または1文字と一致except.
このワイルドカードは貪欲であり、そうでない場合は常に次の文字を消費しますa .
ただし、名前の末尾にある場合、または次の文字が.
である場合、失敗することなく何にも一致しません
*
-0個以上の任意の文字に一致します含む.
(以下の例外を1つ)。このワイルドカードは貪欲ではありません。後続の文字を一致させるために必要なだけ、または必要に応じて一致します。
すべての非ワイルドカード文字は、いくつかの特殊なケースの例外を除き、自分自身と一致する必要があります。
.
-それ自体と一致するか、文字が残っていない場合は名前の最後(何もない)と一致します。 (注-有効なWindows名は.
で終わることはできません)
{space}
-それ自体と一致するか、文字が残っていない場合は名前の末尾と一致する(何もない) (注-有効なWindows名は{space}
で終わることはできません)
*.
-0以上の任意の文字と一致除く.
終了する.
は、実際には.
と{space}
の任意の組み合わせマスクの最後の文字が.
である限り、これは*
がどの文字セットとも単純に一致しない唯一の例外です。
上記の規則はそれほど複雑ではありません。しかし、状況を混乱させるもう1つの非常に重要なルールがあります。sourceMaskは、長い名前と短い8.3名(存在する場合)の両方と比較されます。この最後のルールにより、マスクが短い名前で一致する場合は常に明らかではないため、結果の解釈が非常に難しくなります。
RegEditを使用して、NTFSボリューム上の短い8.3形式の名前の生成を無効にすることができます。この時点で、ファイルマスクの結果の解釈ははるかに簡単です。ショートネームを無効にする前に生成されたショートネームは残ります。
注-厳密なテストは行っていませんが、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は構文エラーを返します。
空のテストフォルダーで開始:
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を作成できるかもしれないと心配していましたが、誘導することはできませんでした。
バグを引き起こすには、以下のすべてが真実でなければならないと思います。私が見たバグのあるケースにはすべて以下の条件がありましたが、以下の条件を満たすすべてのケースがバグだったわけではありません。
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));
}
たぶん誰かがこれが役に立つと思うかもしれません。この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)
}
私はワイルドカードのファイル名を隠すために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