├── .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 | --------------------------------------------------------------------------------