Effective Ways to Delete Hidden Files and Directories in Bash While Avoiding . and .. Directories


1 views

When working with hidden files in Bash, the naive approach of using rm -rf .* creates issues because it attempts to remove the current (.) and parent (..) directories. This results in error messages and a non-zero exit status:

$ rm -rf .*
rm: cannot remove directory '.': Device or resource busy
rm: cannot remove directory '..': Invalid argument
$ echo $?
1

The pattern .??* partially solves the problem by matching only hidden files with names longer than 3 characters, but it misses single-character hidden files like .a or .z:

$ touch .a .abc
$ rm -f .??*
$ ls -la | grep -E '\.a|\.abc'
.a

Here are several effective methods to handle hidden files deletion properly:

Method 1: Using find with -mindepth

find . -mindepth 1 -name ".*" -delete

Method 2: Combining glob patterns

rm -rf .[^.] .??*

Method 3: Explicit exclusion (Bash 4+)

shopt -s dotglob
GLOBIGNORE=".:.."
rm -rf *
shopt -u dotglob

The find approach is most reliable as it:

  • Excludes . with -mindepth 1
  • Specifically targets hidden files with -name ".*"
  • Uses -delete for safe removal

The glob pattern method .[^.] .??* works by:

  • .[^.] matches hidden files with exactly two characters (excluding ..)
  • .??* catches longer hidden names

For directories with thousands of files, the find method is generally fastest. Benchmark examples:

$ time find . -mindepth 1 -name ".*" -delete
real    0m0.023s

$ time rm -rf .[^.] .??*
real    0m0.157s

When attempting to delete hidden files in Bash using rm -rf .*, you'll encounter these common issues:

$ rm -rf .*
rm: cannot remove directory .'
rm: cannot remove directory ..'
$ echo $?
1

This occurs because the pattern .* matches both the current (.) and parent (..) directories, causing rm to fail and return exit code 1.

A common workaround uses this pattern:

$ rm -f .??*

This works because:

  • .?? matches hidden files with at least 2 characters after the dot
  • The * allows for any additional characters

However, this approach has limitations:

  • Misses single-character hidden files (like .a)
  • Only works for files, not directories

A more robust solution using find:

$ find . -maxdepth 1 -name ".*" ! -name "." ! -name ".." -exec rm -rf {} +

Breakdown:

  • -maxdepth 1: Only searches current directory
  • -name ".*": Finds hidden files/directories
  • ! -name "." ! -name "..": Excludes current/parent directories
  • -exec rm -rf {} +: Deletes matches efficiently

For Bash 4.0+ with extglob enabled:

$ shopt -s extglob
$ rm -rf .[!.]* .??*

This combines two patterns:

  • .[!.]*: Matches hidden files not starting with ..
  • .??*: Catches longer hidden names

Another approach using Bash's GLOBIGNORE:

$ GLOBIGNORE=".:.."
$ rm -rf .*
$ unset GLOBIGNORE

For production scripts, I recommend the find solution because:

  1. It's most reliable across different systems
  2. Provides fine-grained control over what gets deleted
  3. Can be easily modified for different matching criteria

Example safe deletion with confirmation:

#!/bin/bash
echo "These files will be deleted:"
find . -maxdepth 1 -name ".*" ! -name "." ! -name ".." -print
read -p "Continue? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]
then
    find . -maxdepth 1 -name ".*" ! -name "." ! -name ".." -exec rm -rf {} +
fi