I'm trying to write a shell script that deletes all empty directories
as well as any directory that contains only the .DS_Store
file that Mac generates.
I can do the former pretty easily with
find -depth -type d -empty
but I can't figure how to find directories that contain only .DS_Store
.
Is there an easy way of doing this without writing my own recursive search function?
4 Answers 4
POSIX sh + find
Here's a solution that relies only on POSIX find and POSIX sh. List all directories, then filter those that only contain an entry called .DS_Store
.
find . -type d -exec sh -c '
cd "0ドル" &&
for x in * .[!.]* ..?*; do
if [ "$x" = ".DS_Store" ]; then continue; fi;
if [ -e "$x" ] || [ -L "$x" ]; then exit 1; fi;
done' {} \; -print
- I use
find
to enumerate all directories recursively. - On each directory, I call
sh
to run some shell code. - The
for
loop enumerates all the files in the directory. - The body of the loop skips
.DS_Store
. - Each of the three patterns is left unchanged if it doesn't match any file.
[ -e "$x" ] || [ -L "$x" ]
captures any file including broken symbolic links; the only way they don't match is if a pattern was left unchanged. - Therefore the shell snippet runs
exit 1
if there is a file other than.DS_Store
, and returns 0 for success otherwise. - Change
-print
to-exec ...
if you want to do something other than printing the names.
Zsh
Here's a solution in zsh. Change echo
to whatever command you want to run.
setopt extended_glob
echo **/*(/DNe\''a=($REPLY/^.DS_Store(DNY1)); ((!#a))'\')
**/*
enumerates all files recursively.- With the glob qualifier
/
,**/*(/)
enumerates all directories recursively. - The glob qualifier
N
ensures that you get an empty list if there are no matches (by default zsh signals an error). - The glob qualifier
D
causes dot files to be included. - The glob qualifier
e\''CODE'\'
runsCODE
on each matching file name and limits the matches to those for whichCODE
succeeds.CODE
can use the variable$REPLY
to refer to the file name. ^.DS_Store
matches files that are not called.DS_Store
.- Thus the CODE limits the matches to those for which the number of files other than
.DS_Store
is zero. - The glob qualifier
Y1
limits the matches to one (it's only an efficiency improvement).
Python
Here's a solution in Python (it works in both 2 and 3). The structure is rather clearer despite this being compressed into a one-liner.
python -c 'import os; print("\n".join([path for path, dirs, files in os.walk(".") if dirs == [] and files in ([], [".DS_Store"])]))'
os.walk
returns a list of directories recursively under its argument. For each directory, it produces a triple containingpath
(the path to the directory),dirs
(the list of subdirectories) andfiles
(the list of files in the directory that aren't themselves directories).[... for ... in os.walk(...) if ...]
filters the result ofos.walk
.- The
if
clause keeps an element only if it has no subdirectories and no files other than.DS_Store
. - The script prints the accepted elements, joined with a newline in between and with a final newline.
Easy solution: first, delete all such files:
find <path> -type f -name "*.DS_Store" -delete
then delete empty directories.
Update based on comment: In order to delete only directories that have only such files, you will need something like (caution: not tested at all, I will not be surprised if it needs to be a bit more involved):
find <path> -type d | while read dir; do
if ! ls --ignore=*.DS_Store $dir; then
rm -rf $dir
fi
done
Explanation of complicated part:
ls --ignore=*.DS_Store $dir
should print files in $dir that do not end in .DS_Store. If there are none, the expression is False and the if-block is executed.
-
I though of this but I don't want to delete all .DS_Store files, only the ones in directories that are otherwise empty.David– David2018年12月17日 20:04:06 +00:00Commented Dec 17, 2018 at 20:04
-
1Sorry to spoil the fun, but the standard
ls
on macOS doesn't know about--ignore
...nohillside– nohillside2018年12月17日 20:27:44 +00:00Commented Dec 17, 2018 at 20:27 -
2in that case, --ignore can be emulated with suitable 'grep -v'WerKater– WerKater2018年12月17日 20:33:10 +00:00Commented Dec 17, 2018 at 20:33
-
(1a) Quoting Stéphane Chazelas’s comment elsewhere on this page: "See Understanding "IFS= read -r line" and Why is looping over find's output bad practice? (1b) If you’re going to use this approach, you need a lot more quotes. (2) Why are you checking for
*.DS_Store
? Yes, I see that you say you’re looking for files whose names end in.DS_Store
; but why? ... (Cont’d)G-Man Says 'Reinstate Monica'– G-Man Says 'Reinstate Monica'2022年05月27日 18:19:53 +00:00Commented May 27, 2022 at 18:19 -
(Cont’d) ... (3) Thank you for admitting that you didn’t test this, because I believe that it cannot possibly work, because
ls
doesn’t exit with an error just because it didn’t list any files.G-Man Says 'Reinstate Monica'– G-Man Says 'Reinstate Monica'2022年05月27日 18:19:57 +00:00Commented May 27, 2022 at 18:19
For Mac:
find . -type d | while read dir; do
# Found empty dir if it only has:
# ., ..
# ., .., .DS_Store
if [ "$(ls -am "$dir" | sed -E 's/^., ..$|^., .., .DS_Store$//')" == "" ]; then
echo rm -R "\"$dir\""
fi
done
-
1See Understanding "IFS= read -r line" and Why is looping over find's output bad practice?Stéphane Chazelas– Stéphane Chazelas2022年05月27日 12:34:34 +00:00Commented May 27, 2022 at 12:34
-
(1) I believe that Gilles’s answer will work on Mac; you didn’t really need to add another one. (2) Fun fact: your answer will report a directory that contains (only) a file called
zDS_Store
. (3) You should explain how to switch your answer from debug mode to operational mode, since you have thrown a monkey wrench into it.G-Man Says 'Reinstate Monica'– G-Man Says 'Reinstate Monica'2022年05月27日 17:44:45 +00:00Commented May 27, 2022 at 17:44
Another POSIX variant with a bit more safeguards:
find . -type d -exec sh -c '
for dir do
contents=$(ls -AFq "$dir") &&
case $contents in
("" | .[dD][sS]_[sS][tT][oO][rR][eE]) printf "%s\n" "$dir"
esac
done' sh {} +
Here:
- we bail out if
ls
can't list the directory contents (or can't find out the type of the files within). - by using
ls -F
, we make sure we don't list a directory that contains a.DS_Store
that is not a regular file (as-F
adds suffixes for other types of files). - With
-q
, we avoid false positives with files names$'.DS_Store\n\n'
for instance. - We match case insensitively as I believe filenames are case insensitive in macos.
A zsh
equivalent could be:
empty() {
set -o localoptions -o extendedglob
ERRNO=0
() ((!ERRNO && !$#)) $REPLY/(#i){^.ds_store(NDY1),.ds_store(N^.)}
}
print -rC1 -- **/*(ND/+empty)
Now, if the point is to remove those directories, there's the case where dirA
contains only dirB
and dirA/dirB
is empty or only contains a .DS_Store
file. Then, we wouldn't be reporting dirA
even though it would become empty after dirA/dirB
is removed.
To address that, that's where you would use -depth
, but you wouldn't be able to use -exec ... {} +
as it's important the empty directories are deleted as soon as they're found, so we'd use -exec ... {} ';'
instead:
find . -depth -type d -exec sh -c '
contents=$(ls -AFq "1ドル") &&
case $contents in
("" | .[dD][sS]_[sS][tT][oO][rR][eE]) exec rm -rf "1ドル"
esac' sh {} ';'