Skip to content

Instantly share code, notes, and snippets.

@MatthewMaker
Forked from mrwellmann/Unity3DEmptyFolderTool.cs
Last active February 6, 2026 23:57
Show Gist options
  • Select an option

  • Save MatthewMaker/cd5c1dc9afb4e622dec5edd50ba214af to your computer and use it in GitHub Desktop.

Select an option

Save MatthewMaker/cd5c1dc9afb4e622dec5edd50ba214af to your computer and use it in GitHub Desktop.
// 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