Forked from mrwellmann/Unity3DEmptyFolderTool.cs
Last active
February 6, 2026 23:57
-
-
Save MatthewMaker/cd5c1dc9afb4e622dec5edd50ba214af to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // MIT License | |
| // Modified from https://gist.github.com/mrwellmann/c9c6bc416143a58d734077ffe57179a3 | |
| using System; | |
| using System.IO; | |
| using System.Linq; | |
| using UnityEditor; | |
| using UnityEngine; | |
| /// <summary> | |
| /// This tool helps to identify and remove empty folders from your Unity 3D project. | |
| /// | |
| /// /// Why do I need this: | |
| /// Empty folders are not committed by git but the connected meta files are. | |
| /// So there will be a creation - deletion cycle between persons with and without such a folder. | |
| /// | |
| /// /// Usage: | |
| /// The tool adds a new menu Tools->Empty Folder Tool. | |
| /// 1. If you "Toggle Auto Delete", every time you remove or move something in your project | |
| /// it will remove empty folders connected to the specific operation path. It will put in a Debug.Log for each removed folder. | |
| /// 2. "Show Empty Folder" will put a Debug.Log for each empty folder in your project. | |
| /// 3. "Delete Empty Folder" will delete all empty folders in your project and put a Debug.Log for each removed folder. | |
| /// | |
| /// /// Acknowledgment: | |
| /// The base code is partly from http://ideaplusplus.com/emptydirectoriesremover-cs/, https://gist.github.com/liortal53/780075ddb17f9306ae32 | |
| /// </summary> | |
| [InitializeOnLoad] | |
| // ReSharper disable once CheckNamespace | |
| public class EmptyFolderTool : AssetPostprocessor | |
| { | |
| private static bool autoDelete; | |
| private const string MENU_NAME = "Assets/Empty Folder Tool/"; | |
| private const string MENU_NAME_AUTO_DELETE = MENU_NAME + "Toggle Auto Delete"; | |
| private const string ASSET_STRING = "Assets"; | |
| /// Called on load thanks to the InitializeOnLoad attribute | |
| static EmptyFolderTool() | |
| { | |
| autoDelete = EditorPrefs.GetBool(MENU_NAME_AUTO_DELETE, false); | |
| // Delaying until first editor tick so that the menu | |
| // will be populated before setting check state, and | |
| // re-apply correct action | |
| EditorApplication.delayCall += () => | |
| { | |
| PerformAction(autoDelete); | |
| }; | |
| } | |
| [MenuItem(MENU_NAME_AUTO_DELETE)] | |
| private static void ToggleAction() | |
| { | |
| // Toggling action | |
| PerformAction(!autoDelete); | |
| } | |
| private static void PerformAction(bool enabled) | |
| { | |
| // Set checkmark on menu item | |
| Menu.SetChecked(MENU_NAME_AUTO_DELETE, enabled); | |
| // Saving editor state | |
| EditorPrefs.SetBool(MENU_NAME_AUTO_DELETE, enabled); | |
| autoDelete = enabled; | |
| } | |
| // Remove Folders Automatically if auto delete is active | |
| private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) | |
| { | |
| if (autoDelete) | |
| { | |
| DeleteEmptyDirectories(deletedAssets); | |
| DeleteEmptyDirectories(movedFromAssetPaths); | |
| } | |
| } | |
| private static void DeleteEmptyDirectories(string[] paths) | |
| { | |
| foreach (var path in paths) | |
| DeleteUpmostEmptyDirectory(path); | |
| } | |
| private static void DeleteUpmostEmptyDirectory(string assetPath) | |
| { | |
| Debug.Log($"DeleteUpmostEmptyDirectory {assetPath}"); | |
| try | |
| { | |
| var assetDir = Path.GetDirectoryName(assetPath); | |
| if (string.IsNullOrEmpty(assetDir) || assetDir == ASSET_STRING) | |
| return; | |
| var absoluteDir = AssetPathToAbsolutePath(assetDir); | |
| var files = Directory.GetFiles(absoluteDir, "*.*", SearchOption.AllDirectories); | |
| if (files.Length == 0) | |
| { | |
| AssetDatabase.DeleteAsset(assetDir); | |
| Debug.Log("Deleting : " + assetDir); | |
| DeleteUpmostEmptyDirectory(assetDir); | |
| } | |
| } | |
| catch (DirectoryNotFoundException) | |
| { | |
| // Folder already gone, no action needed | |
| } | |
| catch (UnauthorizedAccessException e) | |
| { | |
| Debug.LogWarning($"[EmptyFolderTool] Permission denied: {e.Message}"); | |
| } | |
| catch (IOException e) | |
| { | |
| Debug.LogWarning($"[EmptyFolderTool] File in use or I/O error: {e.Message}"); | |
| } | |
| catch (Exception e) | |
| { | |
| // Catch-all for unexpected errors (ArgumentException, PathTooLongException, etc.) | |
| Debug.LogException(e); | |
| } | |
| } | |
| private static string AssetPathToAbsolutePath(string assetPath) | |
| { | |
| return assetPath == ASSET_STRING ? Application.dataPath : Path.Combine(Application.dataPath, assetPath.Substring(ASSET_STRING.Length + 1)); | |
| } | |
| [MenuItem(MENU_NAME + "Print empty folders")] | |
| private static void PrintEmptyFoldersMenuItem() | |
| { | |
| RemoveEmptyFoldersFunc(true); | |
| Debug.Log("Print empty folders is done."); | |
| } | |
| [MenuItem(MENU_NAME + "Remove empty folders")] | |
| private static void RemoveEmptyFoldersMenuItem() | |
| { | |
| RemoveEmptyFoldersFunc(false); | |
| Debug.Log("Remove empty folders is done."); | |
| } | |
| private static void RemoveEmptyFoldersFunc(bool dryRun) | |
| { | |
| var projectSubfolders = Directory.GetDirectories(Application.dataPath, "*", SearchOption.AllDirectories); | |
| //Debug.Log($"[Subfolders] ({projectSubfolders.Length})\n{string.Join("\n", projectSubfolders.Select(f => f.Replace(Application.dataPath, "Assets")))}"); | |
| // Create a list of all the empty subfolders under Assets. | |
| var emptyFolders = projectSubfolders.Where(IsEmptyRecursive).ToArray(); | |
| var index = Application.dataPath.IndexOf(ASSET_STRING, System.StringComparison.CurrentCulture); | |
| foreach (var folder in emptyFolders) | |
| { | |
| // Verify that the folder exists (may have been already removed). | |
| if (Directory.Exists(folder)) | |
| { | |
| if (dryRun) | |
| { | |
| Debug.Log($"Found Empty Folder : {folder}"); | |
| } | |
| else | |
| { | |
| Debug.Log($"Deleting : {folder}"); | |
| // Remove dir (recursively) | |
| Directory.Delete(folder, true); | |
| // Sync AssetDatabase with the delete operation. | |
| AssetDatabase.DeleteAsset(folder.Substring(index + 1)); | |
| } | |
| } | |
| } | |
| // Refresh the asset database once we're done. | |
| AssetDatabase.Refresh(); | |
| } | |
| // A helper method for determining if a folder is empty or not. | |
| private static bool IsEmptyRecursive(string path) | |
| { | |
| //Debug.LogWarning($"IsEmptyRecursive {path}"); | |
| // A folder is empty if all files are .meta files AND all subdirectories are also empty. | |
| return Directory.GetFiles(path).All(file => file.EndsWith(".meta", StringComparison.Ordinal)) && Directory.GetDirectories(path, "*", SearchOption.TopDirectoryOnly).All(IsEmptyRecursive); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment