├── .gitignore
├── LICENSE
├── README.md
├── bin
└── RR.exe
└── source
├── CodeAnalysis.ruleset
├── Program.cs
├── Properties
└── AssemblyInfo.cs
├── RenameRegex.csproj
├── RenameRegex.sln
├── RenameRegex.snk
└── Settings.SourceAnalysis
/.gitignore:
--------------------------------------------------------------------------------
1 | source/bin
2 | source/obj
3 | source/StyleCop.Cache
4 | *.suo
5 | .vs/
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022 Nic Jansma, http://nicj.net
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022 Nic Jansma
2 | [http://nicj.net](http://nicj.net)
3 |
4 | # Introduction
5 |
6 | RenameRegex (RR) is a Windows command-line bulk file and directory renamer, using regular expressions. You can use it as a simple
7 | file renamer or with a complex regular expression for matching and replacement. See the Examples section for details.
8 |
9 | # Usage
10 |
11 | ```
12 | RR.exe file-match search replace [/p] [/r] [/f] [/e] [/files] [/dirs]
13 | /p: pretend (show what will be renamed)
14 | /r: recursive
15 | /c: case insensitive
16 | /f: force overwrite if the file already exists
17 | /e: preserve file extensions
18 | /files: include only files
19 | /dirs: include only directories
20 | (default is to include files only, to include both use /files /dirs)
21 | /fr: use regex for file name matching instead of Windows glob matching
22 | ```
23 |
24 | You can use [.NET regular expressions](http://msdn.microsoft.com/en-us/library/hs600312.aspx) for the search and
25 | replacement strings, including [substitutions](http://msdn.microsoft.com/en-us/library/ewy2t5e0.aspx) (for example,
26 | "$1" is the 1st capture group in the search term).
27 |
28 | # Examples
29 |
30 | Simple rename without a regular expression:
31 |
32 | RR.exe * .ext1 .ext2
33 |
34 | Renaming with a replacement of all "-" characters to "_":
35 |
36 | RR.exe * "-" "_"
37 |
38 | Remove all numbers from the file names:
39 |
40 | RR.exe * "[0-9]+" ""
41 |
42 | Rename files in the pattern of "`124_xyz.txt`" to "`xyz_123.txt`":
43 |
44 | RR.exe *.txt "([0-9]+)_([a-z]+)" "$2_$1"
45 |
46 | Rename directories (only):
47 |
48 | RR * "-" "_" /dirs
49 |
50 | Rename files and directories:
51 |
52 | RR * "-" "_" /files /dirs
53 |
54 | Apply a regular expression to the glob pattern files and directories:
55 |
56 | RR a_\d.txt "a_" "a_0" /fr
57 |
58 | # Version History
59 |
60 | * v1.0 - 2012-01-30: Initial release
61 | * v1.1 - 2012-12-15: Added `/r` option
62 | * v1.2 - 2013-05-11: Allow `/p` and `/r` options before or after main arguments
63 | * v1.3 - 2013-10-23: Added `/f` option
64 | * v1.4 - 2018-04-06: Added `/e` option (via Marcel Peeters)
65 | * v1.5 - 2020-07-02: Added support for directories, added length-check (via Alec S. @Synetech)
66 | * v1.6 - 2021-05-22: Added `/c` support for case insensitivity (via Alec S. @Synetech)
67 | * v1.6.1 - 2021-06-12: Fix `/r` for sub-dirs
68 | * v1.7- 2022-02-01: Added `/fr` option to apply a regex to file matches (instead of Windows glob pattern)
69 |
70 | # Credits
71 |
72 | * Nic Jansma (http://nicj.net)
73 | * Marcel Peeters
74 | * Alec S. (http://synetech.freehostia.com/)
75 |
--------------------------------------------------------------------------------
/bin/RR.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nicjansma/rename-regex/e843a63f211073f3db8b00f0e280cf065298b632/bin/RR.exe
--------------------------------------------------------------------------------
/source/CodeAnalysis.ruleset:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/source/Program.cs:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Nic Jansma 2021 All Right Reserved
3 | //
4 | // Nic Jansma
5 | // nic@nicj.net
6 | namespace RenameRegex
7 | {
8 | using System;
9 | using System.Collections.Generic;
10 | using System.Diagnostics;
11 | using System.IO;
12 | using System.Reflection;
13 | using System.Text.RegularExpressions;
14 |
15 | ///
16 | /// RenameRegex command line program
17 | ///
18 | public static class Program
19 | {
20 | ///
21 | /// Maximum Windows / DOS path length
22 | ///
23 | public const int MaxPath = 260;
24 |
25 | ///
26 | /// Include files
27 | ///
28 | public const int IncludeFiles = 1;
29 |
30 | ///
31 | /// Include directories
32 | ///
33 | public const int IncludeDirs = 2;
34 |
35 | ///
36 | /// Main command line
37 | ///
38 | /// Command line arguments
39 | /// 0 on success
40 | public static int Main(string[] args)
41 | {
42 | // get command-line arguments
43 | string nameSearch;
44 | string nameReplace;
45 | string fileMatch;
46 | bool recursive;
47 | bool caseInsensitive;
48 | bool pretend;
49 | bool force;
50 | bool preserveExt;
51 | int includeMask;
52 | bool fileMatchRegEx;
53 |
54 | if (!GetArguments(
55 | args,
56 | out fileMatch,
57 | out nameSearch,
58 | out nameReplace,
59 | out pretend,
60 | out recursive,
61 | out caseInsensitive,
62 | out force,
63 | out preserveExt,
64 | out includeMask,
65 | out fileMatchRegEx))
66 | {
67 | Usage();
68 |
69 | return 1;
70 | }
71 |
72 | // enumerate all files and directories
73 | List allItems = new List();
74 |
75 | // include all files by default
76 | if ((includeMask == 0) || ((includeMask & IncludeFiles) != 0))
77 | {
78 | string[] files = Directory.GetFiles(
79 | Environment.CurrentDirectory,
80 | fileMatchRegEx ? "*" : fileMatch,
81 | recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
82 |
83 | if (fileMatchRegEx)
84 | {
85 | files = ApplyFileRegex(files, fileMatch);
86 | }
87 |
88 | allItems.AddRange(files);
89 | }
90 |
91 | // include all directories if requested
92 | if ((includeMask & IncludeDirs) != 0)
93 | {
94 | string[] dirs = Directory.GetDirectories(
95 | Environment.CurrentDirectory,
96 | fileMatchRegEx ? "*" : fileMatch,
97 | recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
98 |
99 | if (fileMatchRegEx)
100 | {
101 | dirs = ApplyFileRegex(dirs, fileMatch);
102 | }
103 |
104 | allItems.AddRange(dirs);
105 | }
106 |
107 | if (allItems.Count == 0)
108 | {
109 | Console.WriteLine(@"No files or directories match!");
110 |
111 | return 1;
112 | }
113 |
114 | string pretendModeNotification = pretend ? " (pretend)" : String.Empty;
115 |
116 | //
117 | // loop through each file, renaming via a regex
118 | //
119 | foreach (string fullFile in allItems)
120 | {
121 | if (fullFile.Length > MaxPath || Path.GetDirectoryName(fullFile).Length > MaxPath - 12)
122 | {
123 | Console.WriteLine(@"""{0}"" cannot be accessed; too long.", fullFile);
124 | continue;
125 | }
126 |
127 | // split into filename, extension and path
128 | string fileName = Path.GetFileNameWithoutExtension(fullFile);
129 | string fileExt = Path.GetExtension(fullFile);
130 | string fileDir = Path.GetDirectoryName(fullFile);
131 |
132 | if (!preserveExt)
133 | {
134 | // if file extension should NOT be preserverd
135 | // append extension to filename BEFORE renaming
136 | fileName += fileExt;
137 | }
138 |
139 | // rename via a regex
140 | string fileNameAfter;
141 | if (caseInsensitive)
142 | {
143 | fileNameAfter = Regex.Replace(fileName, nameSearch, nameReplace, RegexOptions.IgnoreCase);
144 | }
145 | else
146 | {
147 | fileNameAfter = Regex.Replace(fileName, nameSearch, nameReplace);
148 | }
149 |
150 | if (preserveExt)
151 | {
152 | // if file extension SHOULD be preserved
153 | // append extension to filenames AFTER renaming
154 | fileName += fileExt;
155 | fileNameAfter += fileExt;
156 | }
157 |
158 | bool newFileAlreadyExists = File.Exists(fileDir + @"\" + fileNameAfter);
159 |
160 | // write what we changed (or would have)
161 | if (fileName != fileNameAfter)
162 | {
163 | // show the relative file path if not the current directory
164 | string fileNameToShow = (System.Environment.CurrentDirectory == fileDir) ?
165 | fileName :
166 | (fileDir + @"\" + fileName).Replace(System.Environment.CurrentDirectory + @"\", String.Empty);
167 |
168 | Console.WriteLine(
169 | @"{0} -> {1}{2}{3}",
170 | fileNameToShow,
171 | fileNameAfter,
172 | pretendModeNotification,
173 | newFileAlreadyExists ? @" (already exists)" : String.Empty);
174 | }
175 |
176 | // move file
177 | if (!pretend && fileName != fileNameAfter)
178 | {
179 | try
180 | {
181 | if (newFileAlreadyExists && force)
182 | {
183 | // remove old file on force overwrite
184 | File.Delete(fileNameAfter);
185 | }
186 |
187 | if (File.Exists(fileDir + @"\" + fileName))
188 | {
189 | File.Move(fileDir + @"\" + fileName, fileDir + @"\" + fileNameAfter);
190 | }
191 | else if (Directory.Exists(fileDir + @"\" + fileName))
192 | {
193 | Directory.Move(fileDir + @"\" + fileName, fileDir + @"\" + fileNameAfter);
194 | }
195 | else
196 | {
197 | Console.WriteLine(@"Could not rename {0}", fileName);
198 | }
199 | }
200 | catch (IOException)
201 | {
202 | Console.WriteLine(@"WARNING: Could not move {0} to {1}", fileName, fileNameAfter);
203 | }
204 | }
205 | }
206 |
207 | return 0;
208 | }
209 |
210 | ///
211 | /// Matches a list of files/directories to a regular expression
212 | ///
213 | /// List of files or directories
214 | /// Regular expression to match
215 | /// List of files or directories that matched
216 | private static string[] ApplyFileRegex(string[] list, string fileMatch)
217 | {
218 | List matching = new List();
219 |
220 | Regex regex = new Regex(fileMatch);
221 |
222 | for (int i = 0; i < list.Length; i++)
223 | {
224 | if (regex.IsMatch(list[i]))
225 | {
226 | matching.Add(list[i]);
227 | }
228 | }
229 |
230 | return matching.ToArray();
231 | }
232 |
233 | ///
234 | /// Gets the program arguments
235 | ///
236 | /// Command-line arguments
237 | /// File matching pattern
238 | /// Search expression
239 | /// Replace expression
240 | /// Whether or not to only show what would happen
241 | /// Whether or not to recursively look in directories
242 | /// Whether or not to force overwrites
243 | /// Whether or not to preserve file extensions
244 | /// Whether to include directories, files or both
245 | /// Whether to use a RegEx for the file match. If false, Windows glob patterns are used.
246 | /// True if argument parsing was successful
247 | private static bool GetArguments(
248 | string[] args,
249 | out string fileMatch,
250 | out string nameSearch,
251 | out string nameReplace,
252 | out bool pretend,
253 | out bool recursive,
254 | out bool caseInsensitive,
255 | out bool force,
256 | out bool preserveExt,
257 | out int includeMask,
258 | out bool fileMatchRegEx)
259 | {
260 | // defaults
261 | fileMatch = String.Empty;
262 | nameSearch = String.Empty;
263 | nameReplace = String.Empty;
264 |
265 | bool foundNameReplace = false;
266 |
267 | pretend = false;
268 | recursive = false;
269 | force = false;
270 | caseInsensitive = false;
271 | preserveExt = false;
272 | includeMask = 0;
273 | fileMatchRegEx = false;
274 |
275 | // check for all arguments
276 | if (args == null || args.Length < 3)
277 | {
278 | return false;
279 | }
280 |
281 | //
282 | // Loop through all of the command line arguments.
283 | //
284 | // Look for options first:
285 | // /p: pretend (show what will be renamed)
286 | // /r: recursive
287 | //
288 | // If not an option, assume it's one of the three main arguments (filename, search, replace)
289 | //
290 | for (int i = 0; i < args.Length; i++)
291 | {
292 | if (args[i].Equals("/p", StringComparison.OrdinalIgnoreCase))
293 | {
294 | pretend = true;
295 | }
296 | else if (args[i].Equals("/r", StringComparison.OrdinalIgnoreCase))
297 | {
298 | recursive = true;
299 | }
300 | else if (args[i].Equals("/c", StringComparison.OrdinalIgnoreCase))
301 | {
302 | caseInsensitive = true;
303 | }
304 | else if (args[i].Equals("/f", StringComparison.OrdinalIgnoreCase))
305 | {
306 | force = true;
307 | }
308 | else if (args[i].Equals("/e", StringComparison.OrdinalIgnoreCase))
309 | {
310 | preserveExt = true;
311 | }
312 | else if (args[i].Equals("/files", StringComparison.OrdinalIgnoreCase))
313 | {
314 | includeMask |= IncludeFiles;
315 | }
316 | else if (args[i].Equals("/dirs", StringComparison.OrdinalIgnoreCase))
317 | {
318 | includeMask |= IncludeDirs;
319 | }
320 | else if (args[i].Equals("/fr", StringComparison.OrdinalIgnoreCase))
321 | {
322 | fileMatchRegEx = true;
323 | }
324 | else
325 | {
326 | // if not an option, the rest of the arguments are filename, search, replace
327 | if (String.IsNullOrEmpty(fileMatch))
328 | {
329 | fileMatch = args[i];
330 | }
331 | else if (String.IsNullOrEmpty(nameSearch))
332 | {
333 | nameSearch = args[i];
334 | }
335 | else if (String.IsNullOrEmpty(nameReplace))
336 | {
337 | nameReplace = args[i];
338 | foundNameReplace = true;
339 | }
340 | }
341 | }
342 |
343 | if (fileMatchRegEx)
344 | {
345 | try
346 | {
347 | Regex regex = new Regex(fileMatch);
348 | }
349 | catch (ArgumentException e)
350 | {
351 | Console.WriteLine("ERROR: File match is not a regular expression!\n");
352 | return false;
353 | }
354 | }
355 |
356 | return !String.IsNullOrEmpty(fileMatch)
357 | && !String.IsNullOrEmpty(nameSearch)
358 | && foundNameReplace;
359 | }
360 |
361 | ///
362 | /// Program usage
363 | ///
364 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly")]
365 | private static void Usage()
366 | {
367 | // get the assembly version
368 | Assembly assembly = Assembly.GetExecutingAssembly();
369 | FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(assembly.Location);
370 | string version = fvi.ProductVersion;
371 |
372 | Console.WriteLine(@"Rename Regex (RR) v{0} by Nic Jansma, http://nicj.net", version);
373 | Console.WriteLine();
374 | Console.WriteLine(@"Usage: RR.exe file-match search replace [/p] [/r] [/c] [/f] [/e] [/files] [/dirs]");
375 | Console.WriteLine(@" /p: pretend (show what will be renamed)");
376 | Console.WriteLine(@" /r: recursive");
377 | Console.WriteLine(@" /c: case insensitive");
378 | Console.WriteLine(@" /f: force overwrite if the file already exists");
379 | Console.WriteLine(@" /e: preserve file extensions");
380 | Console.WriteLine(@" /files: include files (default)");
381 | Console.WriteLine(@" /dirs: include directories");
382 | Console.WriteLine(@" (default is to include files only, to include both use /files /dirs)");
383 | Console.WriteLine(@" /fr: use regex for file name matching instead of Windows glob matching");
384 | return;
385 | }
386 | }
387 | }
388 |
--------------------------------------------------------------------------------
/source/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Nic Jansma 2021 All Right Reserved
3 | //
4 | // Nic Jansma
5 | // nic@nicj.net
6 | using System;
7 | using System.Reflection;
8 | using System.Runtime.CompilerServices;
9 | using System.Runtime.InteropServices;
10 |
11 | // General Information about an assembly is controlled through the following
12 | // set of attributes. Change these attribute values to modify the information
13 | // associated with an assembly.
14 | [assembly: AssemblyTitle("RenameRegex")]
15 | [assembly: AssemblyDescription("Use regular-expressions to rename files and directories from the command-line")]
16 | [assembly: AssemblyConfiguration("")]
17 | [assembly: AssemblyCompany("")]
18 | [assembly: AssemblyProduct("RenameRegex")]
19 | [assembly: AssemblyCopyright("Copyright © Nic Jansma 2022")]
20 | [assembly: AssemblyTrademark("")]
21 | [assembly: AssemblyCulture("")]
22 |
23 | // Setting ComVisible to false makes the types in this assembly not visible
24 | // to COM components. If you need to access a type in this assembly from
25 | // COM, set the ComVisible attribute to true on that type.
26 | [assembly: ComVisible(false)]
27 |
28 | // The following GUID is for the ID of the typelib if this project is exposed to COM
29 | [assembly: Guid("2804025c-e5ef-4e79-aa4b-1f48feccea9d")]
30 |
31 | // Version information for an assembly consists of the following four values:
32 | //
33 | // Major Version
34 | // Minor Version
35 | // Build Number
36 | // Revision
37 | //
38 | [assembly: AssemblyVersion("1.7.0.0")]
39 | [assembly: AssemblyFileVersion("1.7.0.0")]
40 |
41 | [assembly: CLSCompliant(true)]
42 |
--------------------------------------------------------------------------------
/source/RenameRegex.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | 8.0.50727
7 | 2.0
8 | {16A6043E-7F40-476D-B063-F3F9A2351C36}
9 | Exe
10 | Properties
11 | RenameRegex
12 | RR
13 |
14 |
15 |
16 |
17 | 3.5
18 | v2.0
19 | publish\
20 | true
21 | Disk
22 | false
23 | Foreground
24 | 7
25 | Days
26 | false
27 | false
28 | true
29 | 0
30 | 1.0.0.%2a
31 | false
32 | false
33 | true
34 |
35 |
36 | true
37 | full
38 | false
39 | bin\Debug\
40 | DEBUG;TRACE
41 | prompt
42 | 4
43 | ExtendedDesignGuidelineRules.ruleset
44 |
45 |
46 | pdbonly
47 | true
48 | bin\Release\
49 | TRACE
50 | prompt
51 | 4
52 | CodeAnalysis.ruleset
53 |
54 |
55 | true
56 |
57 |
58 | RenameRegex.snk
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | False
75 | .NET Framework 3.5 SP1 Client Profile
76 | false
77 |
78 |
79 | False
80 | .NET Framework 3.5 SP1
81 | true
82 |
83 |
84 | False
85 | Windows Installer 3.1
86 | true
87 |
88 |
89 |
90 |
97 |
--------------------------------------------------------------------------------
/source/RenameRegex.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.28307.1209
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RenameRegex", "RenameRegex.csproj", "{16A6043E-7F40-476D-B063-F3F9A2351C36}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {16A6043E-7F40-476D-B063-F3F9A2351C36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {16A6043E-7F40-476D-B063-F3F9A2351C36}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {16A6043E-7F40-476D-B063-F3F9A2351C36}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {16A6043E-7F40-476D-B063-F3F9A2351C36}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {35B6CC6D-E147-4D40-B2D7-7D18C57A0B25}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/source/RenameRegex.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nicjansma/rename-regex/e843a63f211073f3db8b00f0e280cf065298b632/source/RenameRegex.snk
--------------------------------------------------------------------------------
/source/Settings.SourceAnalysis:
--------------------------------------------------------------------------------
1 |
2 |
3 | NoMerge
4 |
5 |
6 |
7 |
8 |
9 |
10 | False
11 |
12 |
13 |
14 |
15 | False
16 |
17 |
18 |
19 |
20 | False
21 |
22 |
23 |
24 |
25 | False
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | False
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | False
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | True
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | False
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------