Find all empty directories AND directories with a single type of file












1














I'm trying to write a bash script that delete all empty directories as well as any directory that only container 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 only contain .DS_Store.



Is there an easy way of doing this without writing my own recursive search function?










share|improve this question





























    1














    I'm trying to write a bash script that delete all empty directories as well as any directory that only container 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 only contain .DS_Store.



    Is there an easy way of doing this without writing my own recursive search function?










    share|improve this question



























      1












      1








      1







      I'm trying to write a bash script that delete all empty directories as well as any directory that only container 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 only contain .DS_Store.



      Is there an easy way of doing this without writing my own recursive search function?










      share|improve this question















      I'm trying to write a bash script that delete all empty directories as well as any directory that only container 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 only contain .DS_Store.



      Is there an easy way of doing this without writing my own recursive search function?







      bash command-line find






      share|improve this question















      share|improve this question













      share|improve this question




      share|improve this question








      edited Dec 17 at 20:05









      nohillside

      2,292819




      2,292819










      asked Dec 17 at 19:54









      David

      1084




      1084






















          2 Answers
          2






          active

          oldest

          votes


















          2














          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'' runs CODE on each matching file name and limits the matches to those for which CODE 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 containing path (the path to the directory), dirs (the list of subdirectories) and files (the list of files in the directory that aren't themselves directories).


          • [… for … in os.walk(…) if …] filters the result of os.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.






          share|improve this answer





























            0














            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.






            share|improve this answer























            • 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
              Dec 17 at 20:04






            • 1




              Sorry to spoil the fun, but the standard ls on macOS doesn't know about --ignore...
              – nohillside
              Dec 17 at 20:27






            • 2




              in that case, --ignore can be emulated with suitable 'grep -v'
              – WerKater
              Dec 17 at 20:33











            Your Answer








            StackExchange.ready(function() {
            var channelOptions = {
            tags: "".split(" "),
            id: "106"
            };
            initTagRenderer("".split(" "), "".split(" "), channelOptions);

            StackExchange.using("externalEditor", function() {
            // Have to fire editor after snippets, if snippets enabled
            if (StackExchange.settings.snippets.snippetsEnabled) {
            StackExchange.using("snippets", function() {
            createEditor();
            });
            }
            else {
            createEditor();
            }
            });

            function createEditor() {
            StackExchange.prepareEditor({
            heartbeatType: 'answer',
            autoActivateHeartbeat: false,
            convertImagesToLinks: false,
            noModals: true,
            showLowRepImageUploadWarning: true,
            reputationToPostImages: null,
            bindNavPrevention: true,
            postfix: "",
            imageUploader: {
            brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
            contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
            allowUrls: true
            },
            onDemand: true,
            discardSelector: ".discard-answer"
            ,immediatelyShowMarkdownHelp:true
            });


            }
            });














            draft saved

            draft discarded


















            StackExchange.ready(
            function () {
            StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2funix.stackexchange.com%2fquestions%2f489556%2ffind-all-empty-directories-and-directories-with-a-single-type-of-file%23new-answer', 'question_page');
            }
            );

            Post as a guest















            Required, but never shown

























            2 Answers
            2






            active

            oldest

            votes








            2 Answers
            2






            active

            oldest

            votes









            active

            oldest

            votes






            active

            oldest

            votes









            2














            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'' runs CODE on each matching file name and limits the matches to those for which CODE 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 containing path (the path to the directory), dirs (the list of subdirectories) and files (the list of files in the directory that aren't themselves directories).


            • [… for … in os.walk(…) if …] filters the result of os.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.






            share|improve this answer


























              2














              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'' runs CODE on each matching file name and limits the matches to those for which CODE 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 containing path (the path to the directory), dirs (the list of subdirectories) and files (the list of files in the directory that aren't themselves directories).


              • [… for … in os.walk(…) if …] filters the result of os.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.






              share|improve this answer
























                2












                2








                2






                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'' runs CODE on each matching file name and limits the matches to those for which CODE 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 containing path (the path to the directory), dirs (the list of subdirectories) and files (the list of files in the directory that aren't themselves directories).


                • [… for … in os.walk(…) if …] filters the result of os.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.






                share|improve this answer












                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'' runs CODE on each matching file name and limits the matches to those for which CODE 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 containing path (the path to the directory), dirs (the list of subdirectories) and files (the list of files in the directory that aren't themselves directories).


                • [… for … in os.walk(…) if …] filters the result of os.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.







                share|improve this answer












                share|improve this answer



                share|improve this answer










                answered Dec 17 at 21:14









                Gilles

                528k12810581583




                528k12810581583

























                    0














                    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.






                    share|improve this answer























                    • 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
                      Dec 17 at 20:04






                    • 1




                      Sorry to spoil the fun, but the standard ls on macOS doesn't know about --ignore...
                      – nohillside
                      Dec 17 at 20:27






                    • 2




                      in that case, --ignore can be emulated with suitable 'grep -v'
                      – WerKater
                      Dec 17 at 20:33
















                    0














                    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.






                    share|improve this answer























                    • 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
                      Dec 17 at 20:04






                    • 1




                      Sorry to spoil the fun, but the standard ls on macOS doesn't know about --ignore...
                      – nohillside
                      Dec 17 at 20:27






                    • 2




                      in that case, --ignore can be emulated with suitable 'grep -v'
                      – WerKater
                      Dec 17 at 20:33














                    0












                    0








                    0






                    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.






                    share|improve this answer














                    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.







                    share|improve this answer














                    share|improve this answer



                    share|improve this answer








                    edited Dec 17 at 20:25

























                    answered Dec 17 at 20:02









                    WerKater

                    963




                    963












                    • 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
                      Dec 17 at 20:04






                    • 1




                      Sorry to spoil the fun, but the standard ls on macOS doesn't know about --ignore...
                      – nohillside
                      Dec 17 at 20:27






                    • 2




                      in that case, --ignore can be emulated with suitable 'grep -v'
                      – WerKater
                      Dec 17 at 20:33


















                    • 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
                      Dec 17 at 20:04






                    • 1




                      Sorry to spoil the fun, but the standard ls on macOS doesn't know about --ignore...
                      – nohillside
                      Dec 17 at 20:27






                    • 2




                      in that case, --ignore can be emulated with suitable 'grep -v'
                      – WerKater
                      Dec 17 at 20:33
















                    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
                    Dec 17 at 20:04




                    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
                    Dec 17 at 20:04




                    1




                    1




                    Sorry to spoil the fun, but the standard ls on macOS doesn't know about --ignore...
                    – nohillside
                    Dec 17 at 20:27




                    Sorry to spoil the fun, but the standard ls on macOS doesn't know about --ignore...
                    – nohillside
                    Dec 17 at 20:27




                    2




                    2




                    in that case, --ignore can be emulated with suitable 'grep -v'
                    – WerKater
                    Dec 17 at 20:33




                    in that case, --ignore can be emulated with suitable 'grep -v'
                    – WerKater
                    Dec 17 at 20:33


















                    draft saved

                    draft discarded




















































                    Thanks for contributing an answer to Unix & Linux Stack Exchange!


                    • Please be sure to answer the question. Provide details and share your research!

                    But avoid



                    • Asking for help, clarification, or responding to other answers.

                    • Making statements based on opinion; back them up with references or personal experience.


                    To learn more, see our tips on writing great answers.





                    Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


                    Please pay close attention to the following guidance:


                    • Please be sure to answer the question. Provide details and share your research!

                    But avoid



                    • Asking for help, clarification, or responding to other answers.

                    • Making statements based on opinion; back them up with references or personal experience.


                    To learn more, see our tips on writing great answers.




                    draft saved


                    draft discarded














                    StackExchange.ready(
                    function () {
                    StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2funix.stackexchange.com%2fquestions%2f489556%2ffind-all-empty-directories-and-directories-with-a-single-type-of-file%23new-answer', 'question_page');
                    }
                    );

                    Post as a guest















                    Required, but never shown





















































                    Required, but never shown














                    Required, but never shown












                    Required, but never shown







                    Required, but never shown

































                    Required, but never shown














                    Required, but never shown












                    Required, but never shown







                    Required, but never shown







                    Popular posts from this blog

                    Morgemoulin

                    Scott Moir

                    Souastre