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:
- It's most reliable across different systems
- Provides fine-grained control over what gets deleted
- 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